Compare commits
460 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78be0e793e | ||
|
|
e547c15481 | ||
|
|
c621c35827 | ||
|
|
886e52dcff | ||
|
|
8e4da6cbcd | ||
|
|
bde9d01f76 | ||
|
|
5a511a2ba3 | ||
|
|
5c50964b1f | ||
|
|
dea8c9879e | ||
|
|
2034121798 | ||
|
|
23a900c433 | ||
|
|
93a7525d9b | ||
|
|
5670128236 | ||
|
|
dfd32435af | ||
|
|
49265160f8 | ||
|
|
70e1accafe | ||
|
|
1c7f9166f2 | ||
|
|
553913cde1 | ||
|
|
6bca378cec | ||
|
|
12f910c32d | ||
|
|
94c5f3674a | ||
|
|
28b9f93d13 | ||
|
|
69bebb5916 | ||
|
|
321350256e | ||
|
|
e5a9b99afe | ||
|
|
00f805f7ec | ||
|
|
d3c34ad1f5 | ||
|
|
59877a08d1 | ||
|
|
9c92251595 | ||
|
|
e408fdcc08 | ||
|
|
53c1a49b5b | ||
|
|
2aafcf8435 | ||
|
|
1c1af34374 | ||
|
|
52f18a73fb | ||
|
|
28f81f2cc7 | ||
|
|
4494bc66e4 | ||
|
|
679e0de044 | ||
|
|
80b495e50b | ||
|
|
69e5deedc7 | ||
|
|
7f36d7bbd0 | ||
|
|
b7b12ebe84 | ||
|
|
f77c88633e | ||
|
|
5f2ccca423 | ||
|
|
f0f6927273 | ||
|
|
0fb18c0c8b | ||
|
|
4dfed3320e | ||
|
|
7ac62c9840 | ||
|
|
fd80cdaf23 | ||
|
|
19c690d02b | ||
|
|
e119d34bca | ||
|
|
f545ebf0bd | ||
|
|
0b4bbaf23d | ||
|
|
e7dd0eeebe | ||
|
|
7ef9255437 | ||
|
|
7225e2b379 | ||
|
|
1ad038ece5 | ||
|
|
cb8b0225ca | ||
|
|
7620800cd5 | ||
|
|
6837db0015 | ||
|
|
e08e3b536e | ||
|
|
b711a467c1 | ||
|
|
d736b1eba5 | ||
|
|
58d577066a | ||
|
|
89a1e02713 | ||
|
|
6524a619f6 | ||
|
|
f8ccbb4953 | ||
|
|
31298b8857 | ||
|
|
11f5c0afd1 | ||
|
|
27dc211e2d | ||
|
|
2f828255ee | ||
|
|
339b2a24a2 | ||
|
|
6de3713049 | ||
|
|
79df863b75 | ||
|
|
af115c9966 | ||
|
|
1e30022ce7 | ||
|
|
4629276500 | ||
|
|
d42514d8c9 | ||
|
|
90c9a7b3bc | ||
|
|
e6dac93352 | ||
|
|
e4e638bd31 | ||
|
|
fe8c3ba216 | ||
|
|
4ecea144bb | ||
|
|
116b9b42d8 | ||
|
|
39c69db820 | ||
|
|
4d1851e6be | ||
|
|
596aa56b38 | ||
|
|
4850678d55 | ||
|
|
bc52a4e90c | ||
|
|
3e7b3604a4 | ||
|
|
f3f58c8fc7 | ||
|
|
4711094614 | ||
|
|
42ad3723fd | ||
|
|
b268b6edf7 | ||
|
|
b84854af92 | ||
|
|
cfebb3358d | ||
|
|
93e1b13278 | ||
|
|
0d4bfb7bd5 | ||
|
|
0aa5a123b7 | ||
|
|
2e156d4621 | ||
|
|
fdcf6cb6e1 | ||
|
|
01f2ed0892 | ||
|
|
c9abfe5438 | ||
|
|
8f9771adce | ||
|
|
b34f60b1b0 | ||
|
|
0fe608f91e | ||
|
|
5d911ef93f | ||
|
|
1d06ff9bf0 | ||
|
|
107927f5a1 | ||
|
|
d6eb5e3511 | ||
|
|
a6ae656c9f | ||
|
|
3af5730354 | ||
|
|
3306c3c2a2 | ||
|
|
cde0bd2246 | ||
|
|
354fe1948e | ||
|
|
ae1c9bacd6 | ||
|
|
d19cf435be | ||
|
|
1132028bd2 | ||
|
|
b7b4a0a8a2 | ||
|
|
ffd8687b4c | ||
|
|
bbb6f4471f | ||
|
|
b33b906784 | ||
|
|
567eeca782 | ||
|
|
3d46849673 | ||
|
|
6293f5f170 | ||
|
|
e5c5174f6f | ||
|
|
f1034c2e79 | ||
|
|
bbf03a0507 | ||
|
|
153818f7fb | ||
|
|
192b208883 | ||
|
|
824b3e657f | ||
|
|
a95e187b25 | ||
|
|
f888f35e35 | ||
|
|
24a5493ea5 | ||
|
|
0cd7cb8162 | ||
|
|
df4d8ac7e5 | ||
|
|
64fb2ce49b | ||
|
|
0c9b85a4f9 | ||
|
|
62a2104adc | ||
|
|
71dfefd4b7 | ||
|
|
682ce08314 | ||
|
|
c9a476caf7 | ||
|
|
9cee71004f | ||
|
|
81d97c3584 | ||
|
|
939338aaac | ||
|
|
067709bf4b | ||
|
|
69e4b575a8 | ||
|
|
07e6ecd3c3 | ||
|
|
325a6f7d66 | ||
|
|
cf5bb69fc8 | ||
|
|
18b004a2ba | ||
|
|
38323b1c2a | ||
|
|
8a5442f80d | ||
|
|
8598b92dea | ||
|
|
b2cd20c035 | ||
|
|
d4fc34ca2d | ||
|
|
c0323fe816 | ||
|
|
a32309827f | ||
|
|
ada678dfe2 | ||
|
|
cdbd38faaa | ||
|
|
d4653d0590 | ||
|
|
49f53f55f3 | ||
|
|
15eb56d4dd | ||
|
|
d7ea770d47 | ||
|
|
2a8d5e292d | ||
|
|
04da4b2268 | ||
|
|
34bacfd5b1 | ||
|
|
006d5abb78 | ||
|
|
480bad181b | ||
|
|
2afa4b4351 | ||
|
|
a2209ddd5e | ||
|
|
2cc6285a81 | ||
|
|
5dee839230 | ||
|
|
1ef8eb9219 | ||
|
|
d3ddb21716 | ||
|
|
8e80e889f0 | ||
|
|
4e5d14e4a2 | ||
|
|
0230698494 | ||
|
|
977cb34fc7 | ||
|
|
f62febbfb7 | ||
|
|
7ca20fdeb3 | ||
|
|
922f4f4ae5 | ||
|
|
df03f0b7d6 | ||
|
|
249f7c6b7c | ||
|
|
78a99fddfd | ||
|
|
dff2794501 | ||
|
|
3e0f74a894 | ||
|
|
2b3f681723 | ||
|
|
d3d6731d6f | ||
|
|
9bbcc08ff3 | ||
|
|
7aa957c684 | ||
|
|
eeb8554535 | ||
|
|
4eced52c5f | ||
|
|
b856e16998 | ||
|
|
1bdf9bf2e3 | ||
|
|
92b804dc9c | ||
|
|
1b5e5b56cb | ||
|
|
31371b5e3d | ||
|
|
ef1ab197b6 | ||
|
|
bccc35bc3f | ||
|
|
20d20f42c0 | ||
|
|
9d36e9adde | ||
|
|
3491956385 | ||
|
|
134e21765c | ||
|
|
c28990a176 | ||
|
|
131f481750 | ||
|
|
f393e9b2cf | ||
|
|
8612a1d0f8 | ||
|
|
7d53a147d8 | ||
|
|
5d6a98452a | ||
|
|
b4995ef9a7 | ||
|
|
ec7568d28e | ||
|
|
607ba3ee56 | ||
|
|
ae260fad9c | ||
|
|
d9b5344b24 | ||
|
|
bc02508102 | ||
|
|
f969c01f7e | ||
|
|
fb6e9b69ab | ||
|
|
c1a377d15d | ||
|
|
8c1c7ce727 | ||
|
|
2a940dfca6 | ||
|
|
2e7fd480f4 | ||
|
|
9e82561804 | ||
|
|
4427c1675d | ||
|
|
6f6d11fbb8 | ||
|
|
ff0440d7d2 | ||
|
|
c9e18e5b93 | ||
|
|
5e0b29bb6d | ||
|
|
73af5c2326 | ||
|
|
7da847a485 | ||
|
|
11a236a172 | ||
|
|
3b5d3114a6 | ||
|
|
66f15d2a08 | ||
|
|
0225a8b1fc | ||
|
|
d39972b3bf | ||
|
|
93b506a001 | ||
|
|
ebf2e472b3 | ||
|
|
e72841ba27 | ||
|
|
5fd91b4f92 | ||
|
|
10d6eaa5d1 | ||
|
|
319446fc15 | ||
|
|
36be41d0de | ||
|
|
ef9e406300 | ||
|
|
7b4acb53c9 | ||
|
|
dbf84773d4 | ||
|
|
324a69f7fa | ||
|
|
14c49867f7 | ||
|
|
395759cc0a | ||
|
|
ada5087f67 | ||
|
|
93a5bf8d29 | ||
|
|
356a442c6f | ||
|
|
9fd2cf9834 | ||
|
|
5a96075025 | ||
|
|
85b2c44ac7 | ||
|
|
108e4cb391 | ||
|
|
80858bab6b | ||
|
|
00194ebb90 | ||
|
|
f50d15d353 | ||
|
|
edcebf1336 | ||
|
|
fcfd131ccd | ||
|
|
ce5e9a9042 | ||
|
|
7884cb3bea | ||
|
|
d8fcc1f221 | ||
|
|
397a78f248 | ||
|
|
8baf53b09a | ||
|
|
4139bf9424 | ||
|
|
94deecb7b1 | ||
|
|
a4381b7827 | ||
|
|
866da75fa9 | ||
|
|
2b6e9ade07 | ||
|
|
6d1b0efd3b | ||
|
|
e03858f065 | ||
|
|
01929397cf | ||
|
|
496da3f877 | ||
|
|
407e4e003a | ||
|
|
fec61d315f | ||
|
|
f3a3a89f93 | ||
|
|
5b084f0851 | ||
|
|
a7eb173178 | ||
|
|
fa5117a098 | ||
|
|
2ef45c85b2 | ||
|
|
58b7a26b33 | ||
|
|
6e2a2ed946 | ||
|
|
7be1fe5555 | ||
|
|
0e94d52094 | ||
|
|
096dedffb1 | ||
|
|
b5d491a54c | ||
|
|
f6822d6c24 | ||
|
|
32c3ffd57b | ||
|
|
92570bee06 | ||
|
|
05bb399893 | ||
|
|
fe584940e1 | ||
|
|
78cdaef6d2 | ||
|
|
7b4a69f839 | ||
|
|
831aa69da8 | ||
|
|
a56cba6843 | ||
|
|
9228982632 | ||
|
|
38114784f1 | ||
|
|
b805f1486c | ||
|
|
7d31b7f480 | ||
|
|
a0298285e3 | ||
|
|
538a1d5cdf | ||
|
|
f1e973f0d2 | ||
|
|
b467b68f7b | ||
|
|
b895cbbb1e | ||
|
|
fc30eba247 | ||
|
|
b1d4c0c7fe | ||
|
|
7fe5bd32c8 | ||
|
|
43bbef9a11 | ||
|
|
eaea0f74a5 | ||
|
|
cb13a5a531 | ||
|
|
d267843e36 | ||
|
|
5ca67dd885 | ||
|
|
6b0d531758 | ||
|
|
1b3283bd69 | ||
|
|
2820f6a7b8 | ||
|
|
8925ae67bb | ||
|
|
4066c5df42 | ||
|
|
20aac6aa72 | ||
|
|
e1f799f9a1 | ||
|
|
4479de4b9b | ||
|
|
3a8f53a54c | ||
|
|
2f04a6186b | ||
|
|
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 | ||
|
|
90e6260d5e | ||
|
|
566d656c16 | ||
|
|
b729085d52 | ||
|
|
a94bcb02f1 | ||
|
|
e28be01dc2 | ||
|
|
7f7c1efac1 | ||
|
|
76df44e6bb | ||
|
|
fd13f3f98d | ||
|
|
269c3cafb0 | ||
|
|
662b5cace6 | ||
|
|
3c01b09fff | ||
|
|
1ce2db9929 | ||
|
|
c987ff718a | ||
|
|
9e295b105c | ||
|
|
37b3a9f8db | ||
|
|
490853e427 | ||
|
|
9ebedc4740 | ||
|
|
82730d9e08 | ||
|
|
3d2756f376 | ||
|
|
6db51bdec0 | ||
|
|
c238c8bddb | ||
|
|
60e1556871 | ||
|
|
e47bd31681 | ||
|
|
951df6b4e2 | ||
|
|
fcd3bc1133 | ||
|
|
f4db9ff212 | ||
|
|
87841886d4 | ||
|
|
cf883f5f05 | ||
|
|
677d75e173 | ||
|
|
cdccc2c433 | ||
|
|
070436a6ed | ||
|
|
69ded4b33e | ||
|
|
e5a8c0eb4a | ||
|
|
29815fb3f0 | ||
|
|
e930ac08aa | ||
|
|
fdb3764f5c | ||
|
|
937eb350b2 | ||
|
|
3b4ece6bd8 | ||
|
|
35a4fdacbe | ||
|
|
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 text eol=crlf
|
||||||
*.sh eol=lf
|
*.gradle text eol=lf
|
||||||
|
*.mk text eol=lf
|
||||||
|
*.sh text 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
|
||||||
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,6 @@
|
|||||||
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
|
The `termux/termux-app` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||||
|
|
||||||
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
|
### Exceptions
|
||||||
|
|
||||||
|
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
||||||
|
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
|
||||||
|
|||||||
156
README.md
@@ -1,63 +1,147 @@
|
|||||||
# 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)
|
||||||
|
[](https://jitpack.io/#termux/termux-app)
|
||||||
|
|
||||||
|
|
||||||
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
||||||
|
|
||||||
- [Termux Reddit community](https://reddit.com/r/termux)
|
Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages).
|
||||||
- [Termux Wiki](https://wiki.termux.com/wiki/)
|
|
||||||
- [Termux Twitter](http://twitter.com/termux/)
|
Quick how-to about Termux package management is available at [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management). It also has info on how to fix **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.**
|
||||||
|
|
||||||
|
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### Contents
|
||||||
|
- [Termux App and Plugins](#Termux-App-and-Plugins)
|
||||||
|
- [Installation](#Installation)
|
||||||
|
- [Uninstallation](#Uninstallation)
|
||||||
|
- [Important Links](#Important-Links)
|
||||||
|
- [For Devs and Contributors](#For-Devs-and-Contributors)
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Termux App and Plugins
|
||||||
|
|
||||||
|
The core [Termux](https://github.com/termux/termux-app) app comes with the following optional plugin apps.
|
||||||
|
|
||||||
|
- [Termux:API](https://github.com/termux/termux-api)
|
||||||
|
- [Termux:Boot](https://github.com/termux/termux-boot)
|
||||||
|
- [Termux:Float](https://github.com/termux/termux-float)
|
||||||
|
- [Termux:Styling](https://github.com/termux/termux-styling)
|
||||||
|
- [Termux:Tasker](https://github.com/termux/termux-tasker)
|
||||||
|
- [Termux:Widget](https://github.com/termux/termux-widget)
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
Note that this repository is for the app itself (the user interface and the
|
|
||||||
terminal emulation). For the packages installable inside the app, see
|
|
||||||
[termux/termux-packages](https://github.com/termux/termux-packages)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Termux:Widget application can be obtained from:
|
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).
|
||||||
|
|
||||||
- [Google Play](https://play.google.com/store/apps/details?id=com.termux)
|
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [sharedUserId](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from F-Droid and another one from a different source. Android Package Manager will also normally not allow installation of APKs with a different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
|
||||||
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
|
||||||
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
|
|
||||||
|
|
||||||
Additionally we offer development builds for those who want to try out latest
|
If you wish to install from a different source, then you must uninstall **any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#Uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation.
|
||||||
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
|
### F-Droid
|
||||||
installation source, you will have to uninstall the Termux application and
|
|
||||||
all currently installed plugins.
|
|
||||||
|
|
||||||
## Terminal resources
|
Termux application can be obtained from F-Droid [here](https://f-droid.org/en/packages/com.termux/). It usually takes a few days (or even a week or more) for updates to be available on F-Droid once an update has been released on Github. F-Droid releases are built and published by F-Droid once they detect a new Github release. The Termux maintainers **do not** have any control over building and publishing of Termux app on F-Droid. Moreover, the Termux maintainers also do not have access to the APK signing keys of F-Droid releases, so we cannot release an APK ourselves on Github that would be compatible with F-Droid releases.
|
||||||
|
|
||||||
|
### Debug Builds
|
||||||
|
|
||||||
|
For users who don't want to wait for F-Droid releases and want to try out the latest features immediately or want to test their pull requests can get the APKs from [Github Actions](https://github.com/termux/termux-app/actions) page from the workflow runs labeled `Build`. The APK will be listed under `Artifacts` section. These are published for each commit done to the repository. These APKs are [debuggable](https://developer.android.com/studio/debug) and are also not compatible with other sources.
|
||||||
|
|
||||||
|
### Google Playstore **(Deprecated)**
|
||||||
|
|
||||||
|
**Termux and its plugins are no longer updated on [Google playstore](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10).** The last version released for Android `>= 7` was `v0.101`. There are currently no immediate plans to resume updates on Google playstore. **It is highly recommended to not install Termux from playstore for now.** Any current users **should switch** to a different source like F-Droid.
|
||||||
|
|
||||||
|
If for some reason you don't want to switch, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Uninstallation
|
||||||
|
|
||||||
|
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#Installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation.
|
||||||
|
|
||||||
|
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins).
|
||||||
|
|
||||||
|
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if its available on your device and search `termux` in the applications list.
|
||||||
|
|
||||||
|
Even if you think you have not installed any of the plugins, its strongly suggesting to go through the application list in Android settings and double check.
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Important Links
|
||||||
|
|
||||||
|
### Community
|
||||||
|
All community links are available [here](https://wiki.termux.com/wiki/Community).
|
||||||
|
|
||||||
|
The main ones are the following.
|
||||||
|
|
||||||
|
- [Termux Reddit community](https://reddit.com/r/termux)
|
||||||
|
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
|
||||||
|
- [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im)
|
||||||
|
- [Termux Twitter](http://twitter.com/termux/)
|
||||||
|
- [Termux Reports Email](mailto:termuxreports@groups.io)
|
||||||
|
|
||||||
|
### Wikis
|
||||||
|
|
||||||
|
- [Termux Wiki](https://wiki.termux.com/wiki/)
|
||||||
|
- [Termux App Wiki](https://github.com/termux/termux-app/wiki)
|
||||||
|
- [Termux Packages Wiki](https://github.com/termux/termux-packages/wiki)
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
- [FAQ](https://wiki.termux.com/wiki/FAQ)
|
||||||
|
- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout)
|
||||||
|
- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux)
|
||||||
|
- [Package Management](https://wiki.termux.com/wiki/Package_Management)
|
||||||
|
- [Remote_Access](https://wiki.termux.com/wiki/Remote_Access)
|
||||||
|
- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux)
|
||||||
|
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
|
||||||
|
- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard)
|
||||||
|
- [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage)
|
||||||
|
- [Android APIs](https://wiki.termux.com/wiki/Termux:API)
|
||||||
|
- [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348)
|
||||||
|
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
|
||||||
|
|
||||||
|
### Terminal resources
|
||||||
|
|
||||||
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||||
- [vt100.net](http://vt100.net/)
|
- [vt100.net](http://vt100.net/)
|
||||||
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
||||||
|
|
||||||
## Terminal emulators
|
### Terminal emulators
|
||||||
|
|
||||||
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal.
|
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
|
||||||
[Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+),
|
|
||||||
and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
|
|
||||||
|
|
||||||
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2),
|
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
|
||||||
[Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html)
|
|
||||||
(which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
|
|
||||||
|
|
||||||
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository),
|
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
|
||||||
in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests),
|
|
||||||
[Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole)
|
|
||||||
and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
|
|
||||||
|
|
||||||
- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm),
|
- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
|
||||||
including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js),
|
|
||||||
and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
|
|
||||||
|
|
||||||
- xterm: The grandfather of terminal emulators.
|
- xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
||||||
[Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
|
||||||
|
|
||||||
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
||||||
|
|
||||||
- Android Terminal Emulator: Android terminal app which Termux terminal handling
|
- Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
||||||
is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
##
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## For Devs and Contributors
|
||||||
|
|
||||||
|
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of Termux app and its plugins. It was created to allow for removal of all hardcoded paths in Termux app. The termux plugins will hopefully use this in future as well. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted.
|
||||||
|
|
||||||
|
The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info.
|
||||||
|
|||||||
@@ -3,21 +3,40 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
|
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.0.1"
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
|
implementation "androidx.core:core:1.6.0-rc01"
|
||||||
|
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||||
|
implementation "androidx.preference:preference:1.1.1"
|
||||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
implementation "androidx.drawerlayout:drawerlayout:1.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 77
|
versionCode 116
|
||||||
versionName "0.77"
|
versionName "0.116"
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -62,10 +81,21 @@ android {
|
|||||||
path "src/main/cpp/Android.mk"
|
path "src/main/cpp/Android.mk"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'ProtectedPermissions'
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation "junit:junit:4.13.2"
|
||||||
|
testImplementation "org.robolectric:robolectric:4.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
task versionName {
|
task versionName {
|
||||||
@@ -74,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"
|
||||||
@@ -96,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()
|
||||||
@@ -123,19 +153,18 @@ clean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task downloadBootstraps(){
|
task downloadBootstraps() {
|
||||||
doLast {
|
doLast {
|
||||||
def version = 18
|
def version = "2021.06.30-r1"
|
||||||
downloadBootstrap("aarch64", "1a4c08a696d452b58f69102428239ec0c07521c0ca9f48b23ef70ae0e5e3d4f8", version)
|
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version)
|
||||||
downloadBootstrap("arm", "bff11f2c7e9c1055a22fc5f20bb7507b75f6034e0f5d591ec6725b3407981b85", version)
|
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version)
|
||||||
downloadBootstrap("i686", "6fb93020db2807337d82a1537e24612400cacbd10cf4bccaeb0714d51e653da1", version)
|
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version)
|
||||||
downloadBootstrap("x86_64", "a6067e5decc486dcad190c1ed9e15366c798e5e7d9b9b9ee6b4b8231290524c3", version)
|
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
android.applicationVariants.all { variant ->
|
android.applicationVariants.all { variant ->
|
||||||
variant.javaCompiler.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,101 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission-sdk-23 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" >
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity-alias
|
||||||
android:name="com.termux.app.TermuxHelpActivity"
|
android:name=".HomeActivity"
|
||||||
android:exported="false"
|
android:targetActivity=".app.TermuxActivity">
|
||||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
|
||||||
android:parentActivityName=".app.TermuxActivity"
|
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||||
android:resizeableActivity="true"
|
<intent-filter>
|
||||||
android:label="@string/application_name" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.IOT_LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
|
android:name=".app.activities.HelpActivity"
|
||||||
|
android:exported="false"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:taskAffinity="com.termux.filereceiver"
|
android:parentActivityName=".app.TermuxActivity"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:noHistory="true">
|
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".app.activities.SettingsActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/title_activity_termux_settings"
|
||||||
|
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".shared.activities.ReportActivity"
|
||||||
|
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||||
|
android:documentLaunchMode="intoExisting"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".filepicker.TermuxFileReceiverActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:label="@string/application_name"
|
||||||
|
android:noHistory="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
|
||||||
|
|
||||||
<!-- Accept multiple file types when sending. -->
|
<!-- Accept multiple file types when sending. -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="application/*" />
|
<data android:mimeType="application/*" />
|
||||||
<data android:mimeType="audio/*" />
|
<data android:mimeType="audio/*" />
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
@@ -74,35 +129,25 @@
|
|||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
|
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
|
||||||
<intent-filter tools:ignore="AppLinkUrlError">
|
<intent-filter tools:ignore="AppLinkUrlError">
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:mimeType="application/*" />
|
||||||
|
<data android:mimeType="audio/*" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="application/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 +155,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,220 +0,0 @@
|
|||||||
package com.termux.app;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
|
|
||||||
|
|
||||||
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) {
|
|
||||||
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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,445 +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) {
|
|
||||||
|
|
||||||
// Depending on DnD settings, value can be >1 but 0 means "disabled".
|
|
||||||
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) < 1) {
|
|
||||||
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_UP:
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
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;
|
|
||||||
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // special handle api 21
|
|
||||||
param.height = (int)(37.5 * getResources().getDisplayMetrics().density + 0.5); // 37.5 equal to R.id.viewpager layout_height / rows in DP
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
259
app/src/main/java/com/termux/app/RunCommandService.java
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
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.data.DataUtils;
|
||||||
|
import com.termux.shared.data.IntentUtils;
|
||||||
|
import com.termux.shared.file.TermuxFileUtils;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.app.utils.PluginUtils;
|
||||||
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
Error error;
|
||||||
|
String errmsg;
|
||||||
|
|
||||||
|
// If invalid action passed, then just return
|
||||||
|
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||||
|
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||||
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
|
||||||
|
|
||||||
|
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If intent was sent with `am` command, then normal comma characters may have been replaced
|
||||||
|
* with alternate characters if a normal comma existed in an argument itself to prevent it
|
||||||
|
* splitting into multiple arguments by `am` command.
|
||||||
|
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
|
||||||
|
* options can be used without passing the below extras, but native supports is helpful if
|
||||||
|
* they are not being used.
|
||||||
|
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
|
||||||
|
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
|
||||||
|
*/
|
||||||
|
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
|
||||||
|
if (replaceCommaAlternativeCharsInArguments) {
|
||||||
|
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
|
||||||
|
if (commaAlternativeCharsInArguments == null)
|
||||||
|
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
|
||||||
|
// Replace any commaAlternativeCharsInArguments characters with normal commas
|
||||||
|
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
||||||
|
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
||||||
|
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||||
|
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
|
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
|
||||||
|
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||||
|
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||||
|
executionCommand.isPluginExecutionCommand = true;
|
||||||
|
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||||
|
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||||
|
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
|
||||||
|
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
|
||||||
|
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
|
||||||
|
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
|
||||||
|
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If "allow-external-apps" property to not set to "true", then just return
|
||||||
|
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||||
|
// user knows someone tried to run a command in termux context, since it may be malicious
|
||||||
|
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
|
||||||
|
// also sent, then its creator is also logged and shown.
|
||||||
|
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||||
|
if (errmsg != null) {
|
||||||
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
|
||||||
|
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
|
||||||
|
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||||
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get canonical path of executable
|
||||||
|
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||||
|
|
||||||
|
// If executable is not a regular file, or is not readable or executable, then just return
|
||||||
|
// Setting of missing read and execute permissions is not done
|
||||||
|
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
|
||||||
|
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||||
|
false);
|
||||||
|
if (error != null) {
|
||||||
|
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
|
||||||
|
executionCommand.setStateFailed(error);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If workingDirectory is not null or empty
|
||||||
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||||
|
// Get canonical path of workingDirectory
|
||||||
|
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||||
|
|
||||||
|
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||||
|
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||||
|
// under allowed termux working directory paths.
|
||||||
|
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||||
|
// for working directories.
|
||||||
|
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
|
||||||
|
true, true, true,
|
||||||
|
false, true);
|
||||||
|
if (error != null) {
|
||||||
|
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
|
||||||
|
executionCommand.setStateFailed(error);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(TermuxFileUtils.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.resultConfig.resultPendingIntent);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start TERMUX_SERVICE and pass it execution intent
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
this.startForegroundService(execIntent);
|
||||||
|
} else {
|
||||||
|
this.startService(execIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
runStopForeground();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
app/src/main/java/com/termux/app/TermuxApplication.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
|
import com.termux.shared.crash.TermuxCrashUtils;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
public class TermuxApplication extends Application {
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
// Set crash handler for the app
|
||||||
|
TermuxCrashUtils.setCrashHandler(this);
|
||||||
|
|
||||||
|
// Set log level for the app
|
||||||
|
setLogLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLogLevel() {
|
||||||
|
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(getApplicationContext());
|
||||||
|
if (preferences == null) return;
|
||||||
|
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||||
|
Logger.logDebug("Starting Application");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,27 +4,26 @@ import android.app.Activity;
|
|||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.terminal.EmulatorDebug;
|
import com.termux.app.utils.CrashUtils;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.interact.MessageDialogUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
@@ -33,11 +32,11 @@ import java.util.zip.ZipInputStream;
|
|||||||
* Install the Termux bootstrap packages if necessary by following the below steps:
|
* Install the Termux bootstrap packages if necessary by following the below steps:
|
||||||
* <p/>
|
* <p/>
|
||||||
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
|
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
|
||||||
* broken $PREFIX folder below.
|
* broken $PREFIX directory below.
|
||||||
* <p/>
|
* <p/>
|
||||||
* (2) A progress dialog is shown with "Installing..." message and a spinner.
|
* (2) A progress dialog is shown with "Installing..." message and a spinner.
|
||||||
* <p/>
|
* <p/>
|
||||||
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
|
* (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below.
|
||||||
* <p/>
|
* <p/>
|
||||||
* (4) The zip file is loaded from a shared library.
|
* (4) The zip file is loaded from a shared library.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -50,22 +49,38 @@ import java.util.zip.ZipInputStream;
|
|||||||
*/
|
*/
|
||||||
final class TermuxInstaller {
|
final class TermuxInstaller {
|
||||||
|
|
||||||
/** Performs setup if necessary. */
|
private static final String LOG_TAG = "TermuxInstaller";
|
||||||
static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
|
|
||||||
|
/** Performs bootstrap setup if necessary. */
|
||||||
|
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
|
||||||
// Termux can only be run as the primary user (device owner) since only that
|
// Termux can only be run as the primary user (device owner) since only that
|
||||||
// account has the expected file system paths. Verify that:
|
// account has the expected file system paths. Verify that:
|
||||||
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
|
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
|
||||||
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
|
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
|
||||||
if (!isPrimaryUser) {
|
if (!isPrimaryUser) {
|
||||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
|
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||||
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||||
|
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
||||||
|
activity.getString(R.string.bootstrap_error_title),
|
||||||
|
bootstrapErrorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
|
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
||||||
if (PREFIX_FILE.isDirectory()) {
|
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
|
||||||
whenDone.run();
|
|
||||||
return;
|
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||||
|
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
||||||
|
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
||||||
|
// If prefix directory is empty or only contains the tmp directory
|
||||||
|
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
|
||||||
|
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||||
|
} else {
|
||||||
|
whenDone.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
||||||
|
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
||||||
@@ -73,13 +88,29 @@ final class TermuxInstaller {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
|
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
||||||
|
|
||||||
|
Error error;
|
||||||
|
|
||||||
|
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||||
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
||||||
|
|
||||||
if (STAGING_PREFIX_FILE.exists()) {
|
// Delete prefix staging directory or any file at its destination
|
||||||
deleteFolder(STAGING_PREFIX_FILE);
|
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete prefix directory or any file at its destination
|
||||||
|
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
||||||
|
|
||||||
final byte[] buffer = new byte[8096];
|
final byte[] buffer = new byte[8096];
|
||||||
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
||||||
|
|
||||||
@@ -98,14 +129,22 @@ final class TermuxInstaller {
|
|||||||
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
||||||
symlinks.add(Pair.create(oldPath, newPath));
|
symlinks.add(Pair.create(oldPath, newPath));
|
||||||
|
|
||||||
ensureDirectoryExists(new File(newPath).getParentFile());
|
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String zipEntryName = zipEntry.getName();
|
String zipEntryName = zipEntry.getName();
|
||||||
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
||||||
boolean isDirectory = zipEntry.isDirectory();
|
boolean isDirectory = zipEntry.isDirectory();
|
||||||
|
|
||||||
ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||||
@@ -128,27 +167,18 @@ final class TermuxInstaller {
|
|||||||
Os.symlink(symlink.first, symlink.second);
|
Os.symlink(symlink.first, symlink.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
|
||||||
|
|
||||||
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
|
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
|
||||||
throw new RuntimeException("Unable to rename staging folder");
|
throw new RuntimeException("Moving prefix staging to prefix directory failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||||
activity.runOnUiThread(whenDone);
|
activity.runOnUiThread(whenDone);
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
||||||
activity.runOnUiThread(() -> {
|
|
||||||
try {
|
|
||||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
|
||||||
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
activity.finish();
|
|
||||||
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
TermuxInstaller.setupIfNeeded(activity, whenDone);
|
|
||||||
}).show();
|
|
||||||
} catch (WindowManager.BadTokenException e1) {
|
|
||||||
// Activity already dismissed - ignore.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
activity.runOnUiThread(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -162,58 +192,51 @@ final class TermuxInstaller {
|
|||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ensureDirectoryExists(File directory) {
|
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
|
||||||
if (!directory.isDirectory() && !directory.mkdirs()) {
|
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
||||||
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] loadZipBytes() {
|
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
||||||
// Only load the shared library when necessary to save memory usage.
|
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true);
|
||||||
System.loadLibrary("termux-bootstrap");
|
|
||||||
return getZip();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static native byte[] getZip();
|
activity.runOnUiThread(() -> {
|
||||||
|
try {
|
||||||
/** Delete a folder and all its content or throw. Don't follow symlinks. */
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||||
static void deleteFolder(File fileOrDirectory) throws IOException {
|
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||||
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
|
dialog.dismiss();
|
||||||
File[] children = fileOrDirectory.listFiles();
|
activity.finish();
|
||||||
|
})
|
||||||
if (children != null) {
|
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||||
for (File child : children) {
|
dialog.dismiss();
|
||||||
deleteFolder(child);
|
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||||
}
|
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||||
|
}).show();
|
||||||
|
} catch (WindowManager.BadTokenException e1) {
|
||||||
|
// Activity already dismissed - ignore.
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
if (!fileOrDirectory.delete()) {
|
|
||||||
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setupStorageSymlinks(final Context context) {
|
static void setupStorageSymlinks(final Context context) {
|
||||||
final String LOG_TAG = "termux-storage";
|
final String LOG_TAG = "termux-storage";
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
||||||
|
|
||||||
new Thread() {
|
new Thread() {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
Error error;
|
||||||
|
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||||
|
|
||||||
if (storageDir.exists()) {
|
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
|
||||||
try {
|
if (error != null) {
|
||||||
deleteFolder(storageDir);
|
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||||
} catch (IOException e) {
|
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
|
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true);
|
||||||
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());
|
||||||
|
|
||||||
@@ -238,14 +261,31 @@ final class TermuxInstaller {
|
|||||||
File dir = dirs[i];
|
File dir = dirs[i];
|
||||||
if (dir == null) continue;
|
if (dir == null) continue;
|
||||||
String symlinkName = "external-" + i;
|
String symlinkName = "external-" + i;
|
||||||
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
|
||||||
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(LOG_TAG, "Error setting up link", e);
|
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
||||||
|
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Error ensureDirectoryExists(File directory) {
|
||||||
|
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] loadZipBytes() {
|
||||||
|
// Only load the shared library when necessary to save memory usage.
|
||||||
|
System.loadLibrary("termux-bootstrap");
|
||||||
|
return getZip();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native byte[] getZip();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import android.net.Uri;
|
|||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import com.termux.terminal.EmulatorDebug;
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -24,11 +24,13 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
public class TermuxOpenReceiver extends BroadcastReceiver {
|
public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxOpenReceiver";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
final Uri data = intent.getData();
|
final Uri data = intent.getData();
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Called without intent data");
|
Logger.logError(LOG_TAG, "termux-open: Called without intent data");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
// Ok.
|
// Ok.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +61,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
context.startActivity(urlIntent);
|
context.startActivity(urlIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
|
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final File fileToShare = new File(filePath);
|
final File fileToShare = new File(filePath);
|
||||||
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
contentTypeToUse = contentTypeExtra;
|
contentTypeToUse = contentTypeExtra;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri uriToShare = Uri.parse("content://com.termux.files" + fileToShare.getAbsolutePath());
|
Uri uriToShare = Uri.parse("content://" + TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY + fileToShare.getAbsolutePath());
|
||||||
|
|
||||||
if (Intent.ACTION_SEND.equals(intentAction)) {
|
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||||
@@ -103,7 +105,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
context.startActivity(sendIntent);
|
context.startActivity(sendIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
|
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +180,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
String path = file.getCanonicalPath();
|
String path = file.getCanonicalPath();
|
||||||
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||||
// See https://support.google.com/faqs/answer/7496913:
|
// See https://support.google.com/faqs/answer/7496913:
|
||||||
if (!(path.startsWith(TermuxService.FILES_PATH) || path.startsWith(storagePath))) {
|
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
|
||||||
throw new IllegalArgumentException("Invalid path: " + path);
|
throw new IllegalArgumentException("Invalid path: " + path);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -1,232 +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 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"));
|
|
||||||
|
|
||||||
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,281 +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 (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
|
||||||
// Do not steal dedicated buttons from a full external keyboard.
|
|
||||||
return false;
|
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
|
||||||
mVirtualControlKeyDown = down;
|
|
||||||
return true;
|
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
|
||||||
mVirtualFnKeyDown = down;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.termux.app;
|
package com.termux.app.activities;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
@@ -12,8 +12,10 @@ import android.webkit.WebViewClient;
|
|||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
/** Basic embedded browser for viewing help pages. */
|
/** Basic embedded browser for viewing help pages. */
|
||||||
public final class TermuxHelpActivity extends Activity {
|
public final class HelpActivity extends Activity {
|
||||||
|
|
||||||
WebView mWebView;
|
WebView mWebView;
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ public final class TermuxHelpActivity extends Activity {
|
|||||||
mWebView.setWebViewClient(new WebViewClient() {
|
mWebView.setWebViewClient(new WebViewClient() {
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
if (url.startsWith("https://wiki.termux.com")) {
|
if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
|
||||||
// Inline help.
|
// Inline help.
|
||||||
setContentView(progressLayout);
|
setContentView(progressLayout);
|
||||||
return false;
|
return false;
|
||||||
@@ -60,7 +62,7 @@ public final class TermuxHelpActivity extends Activity {
|
|||||||
setContentView(mWebView);
|
setContentView(mWebView);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
|
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.termux.app.activities;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.interact.ShareUtils;
|
||||||
|
import com.termux.shared.packages.PackageUtils;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
public class SettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
getSupportFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.settings, new RootPreferencesFragment())
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
actionBar.setDisplayShowHomeEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSupportNavigateUp() {
|
||||||
|
onBackPressed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RootPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||||
|
|
||||||
|
configureTermuxTaskerPreference(context);
|
||||||
|
configureAboutPreference(context);
|
||||||
|
configureDonatePreference(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureTermuxTaskerPreference(@NonNull Context context) {
|
||||||
|
Preference termuxTaskerPrefernce = findPreference("termux_tasker");
|
||||||
|
if (termuxTaskerPrefernce != null) {
|
||||||
|
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
|
||||||
|
// If failed to get app preferences, then likely app is not installed, so do not show its preference
|
||||||
|
termuxTaskerPrefernce.setVisible(preferences != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureAboutPreference(@NonNull Context context) {
|
||||||
|
Preference aboutPreference = findPreference("about");
|
||||||
|
if (aboutPreference != null) {
|
||||||
|
aboutPreference.setOnPreferenceClickListener(preference -> {
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String title = "About";
|
||||||
|
|
||||||
|
StringBuilder aboutString = new StringBuilder();
|
||||||
|
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, false));
|
||||||
|
|
||||||
|
String termuxPluginAppsInfo = TermuxUtils.getTermuxPluginAppsInfoMarkdownString(context);
|
||||||
|
if (termuxPluginAppsInfo != null)
|
||||||
|
aboutString.append("\n\n").append(termuxPluginAppsInfo);
|
||||||
|
|
||||||
|
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||||
|
|
||||||
|
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureDonatePreference(@NonNull Context context) {
|
||||||
|
Preference donatePreference = findPreference("donate");
|
||||||
|
if (donatePreference != null) {
|
||||||
|
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
|
||||||
|
if (signingCertificateSHA256Digest != null) {
|
||||||
|
// If APK is a Google Playstore release, then do not show the donation link
|
||||||
|
// since Termux isn't exempted from the playstore policy donation links restriction
|
||||||
|
// Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/
|
||||||
|
String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest);
|
||||||
|
if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) {
|
||||||
|
donatePreference.setVisible(false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
donatePreference.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
donatePreference.setOnPreferenceClickListener(preference -> {
|
||||||
|
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.termux.app.fragments.settings;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(TermuxPreferencesDataStore.getInstance(context));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.termux_preferences, rootKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TermuxPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static TermuxPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private TermuxPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized TermuxPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new TermuxPreferencesDataStore(context);
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.termux.app.fragments.settings;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(TermuxTaskerPreferencesDataStore.getInstance(context));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.termux_tasker_preferences, rootKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TermuxTaskerPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxTaskerAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static TermuxTaskerPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private TermuxTaskerPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized TermuxTaskerPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new TermuxTaskerPreferencesDataStore(context);
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.termux.app.fragments.settings.termux;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.PreferenceCategory;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.termux_debugging_preferences, rootKey);
|
||||||
|
|
||||||
|
configureLoggingPreferences(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureLoggingPreferences(@NonNull Context context) {
|
||||||
|
PreferenceCategory loggingCategory = findPreference("logging");
|
||||||
|
if (loggingCategory == null) return;
|
||||||
|
|
||||||
|
ListPreference logLevelListPreference = findPreference("log_level");
|
||||||
|
if (logLevelListPreference != null) {
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context, true);
|
||||||
|
if (preferences == null) return;
|
||||||
|
|
||||||
|
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel());
|
||||||
|
loggingCategory.addPreference(logLevelListPreference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) {
|
||||||
|
if (logLevelListPreference == null)
|
||||||
|
logLevelListPreference = new ListPreference(context);
|
||||||
|
|
||||||
|
CharSequence[] logLevels = Logger.getLogLevelsArray();
|
||||||
|
CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true);
|
||||||
|
|
||||||
|
logLevelListPreference.setEntryValues(logLevels);
|
||||||
|
logLevelListPreference.setEntries(logLevelLabels);
|
||||||
|
|
||||||
|
logLevelListPreference.setValue(String.valueOf(logLevel));
|
||||||
|
logLevelListPreference.setDefaultValue(Logger.DEFAULT_LOG_LEVEL);
|
||||||
|
|
||||||
|
return logLevelListPreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static DebuggingPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private DebuggingPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new DebuggingPreferencesDataStore(context);
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String getString(String key, @Nullable String defValue) {
|
||||||
|
if (mPreferences == null) return null;
|
||||||
|
if (key == null) return null;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "log_level":
|
||||||
|
return String.valueOf(mPreferences.getLogLevel());
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putString(String key, @Nullable String value) {
|
||||||
|
if (mPreferences == null) return;
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "log_level":
|
||||||
|
if (value != null) {
|
||||||
|
mPreferences.setLogLevel(mContext, Integer.parseInt(value));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putBoolean(String key, boolean value) {
|
||||||
|
if (mPreferences == null) return;
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "terminal_view_key_logging_enabled":
|
||||||
|
mPreferences.setTerminalViewKeyLoggingEnabled(value);
|
||||||
|
break;
|
||||||
|
case "plugin_error_notifications_enabled":
|
||||||
|
mPreferences.setPluginErrorNotificationsEnabled(value);
|
||||||
|
break;
|
||||||
|
case "crash_report_notifications_enabled":
|
||||||
|
mPreferences.setCrashReportNotificationsEnabled(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defValue) {
|
||||||
|
if (mPreferences == null) return false;
|
||||||
|
switch (key) {
|
||||||
|
case "terminal_view_key_logging_enabled":
|
||||||
|
return mPreferences.isTerminalViewKeyLoggingEnabled();
|
||||||
|
case "plugin_error_notifications_enabled":
|
||||||
|
return mPreferences.arePluginErrorNotificationsEnabled();
|
||||||
|
case "crash_report_notifications_enabled":
|
||||||
|
return mPreferences.areCrashReportNotificationsEnabled();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.termux.app.fragments.settings.termux;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(context));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.termux_terminal_io_preferences, rootKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TerminalIOPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static TerminalIOPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private TerminalIOPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new TerminalIOPreferencesDataStore(context);
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putBoolean(String key, boolean value) {
|
||||||
|
if (mPreferences == null) return;
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "soft_keyboard_enabled":
|
||||||
|
mPreferences.setSoftKeyboardEnabled(value);
|
||||||
|
break;
|
||||||
|
case "soft_keyboard_enabled_only_if_no_hardware":
|
||||||
|
mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defValue) {
|
||||||
|
if (mPreferences == null) return false;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "soft_keyboard_enabled":
|
||||||
|
return mPreferences.isSoftKeyboardEnabled();
|
||||||
|
case "soft_keyboard_enabled_only_if_no_hardware":
|
||||||
|
return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.termux.app.fragments.settings.termux_tasker;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.PreferenceCategory;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.termux_tasker_debugging_preferences, rootKey);
|
||||||
|
|
||||||
|
configureLoggingPreferences(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureLoggingPreferences(@NonNull Context context) {
|
||||||
|
PreferenceCategory loggingCategory = findPreference("logging");
|
||||||
|
if (loggingCategory == null) return;
|
||||||
|
|
||||||
|
ListPreference logLevelListPreference = findPreference("log_level");
|
||||||
|
if (logLevelListPreference != null) {
|
||||||
|
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, true);
|
||||||
|
if (preferences == null) return;
|
||||||
|
|
||||||
|
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
|
||||||
|
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
|
||||||
|
loggingCategory.addPreference(logLevelListPreference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxTaskerAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static DebuggingPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private DebuggingPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new DebuggingPreferencesDataStore(context);
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String getString(String key, @Nullable String defValue) {
|
||||||
|
if (mPreferences == null) return null;
|
||||||
|
if (key == null) return null;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "log_level":
|
||||||
|
return String.valueOf(mPreferences.getLogLevel(true));
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putString(String key, @Nullable String value) {
|
||||||
|
if (mPreferences == null) return;
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "log_level":
|
||||||
|
if (value != null) {
|
||||||
|
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
app/src/main/java/com/termux/app/models/UserAction.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.termux.app.models;
|
||||||
|
|
||||||
|
public enum UserAction {
|
||||||
|
|
||||||
|
ABOUT("about"),
|
||||||
|
CRASH_REPORT("crash report"),
|
||||||
|
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
|
||||||
|
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
UserAction(final String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.termux.app.settings.properties;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class TermuxAppSharedProperties extends TermuxSharedProperties {
|
||||||
|
|
||||||
|
private ExtraKeysInfo mExtraKeysInfo;
|
||||||
|
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxAppSharedProperties";
|
||||||
|
|
||||||
|
public TermuxAppSharedProperties(@Nonnull Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the termux properties from disk into an in-memory cache.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void loadTermuxPropertiesFromDisk() {
|
||||||
|
super.loadTermuxPropertiesFromDisk();
|
||||||
|
|
||||||
|
setExtraKeys();
|
||||||
|
setSessionShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the terminal extra keys and style.
|
||||||
|
*/
|
||||||
|
private void setExtraKeys() {
|
||||||
|
mExtraKeysInfo = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// The mMap stores the extra key and style string values while loading properties
|
||||||
|
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
|
||||||
|
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
||||||
|
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||||
|
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||||
|
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
||||||
|
|
||||||
|
try {
|
||||||
|
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||||
|
} catch (JSONException e2) {
|
||||||
|
Logger.showToast(mContext, "Can't create default extra keys",true);
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||||
|
mExtraKeysInfo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the terminal sessions shortcuts.
|
||||||
|
*/
|
||||||
|
private void setSessionShortcuts() {
|
||||||
|
if (mSessionShortcuts == null)
|
||||||
|
mSessionShortcuts = new ArrayList<>();
|
||||||
|
else
|
||||||
|
mSessionShortcuts.clear();
|
||||||
|
|
||||||
|
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
|
||||||
|
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
|
||||||
|
// The mMap stores the code points for the session shortcuts while loading properties
|
||||||
|
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
|
||||||
|
// If codePoint is null, then session shortcut did not exist in properties or was invalid
|
||||||
|
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
|
||||||
|
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
|
||||||
|
// add the code point to sessionShortcuts
|
||||||
|
if (codePoint != null)
|
||||||
|
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<KeyboardShortcut> getSessionShortcuts() {
|
||||||
|
return mSessionShortcuts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeysInfo getExtraKeysInfo() {
|
||||||
|
return mExtraKeysInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk.
|
||||||
|
*/
|
||||||
|
public static int getTerminalTranscriptRows(Context context) {
|
||||||
|
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.view.ViewUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
|
||||||
|
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
|
||||||
|
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
|
||||||
|
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
|
||||||
|
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
|
||||||
|
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
|
||||||
|
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
|
||||||
|
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
|
||||||
|
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
|
||||||
|
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
|
||||||
|
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
|
||||||
|
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
|
||||||
|
*
|
||||||
|
* With the above configuration, the additional clipboard suggestions row partially covers the
|
||||||
|
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
|
||||||
|
* in the Android OS where it does not consider the candidate's view height in its calculation to push
|
||||||
|
* up the view or because Gboard does not include the candidate's view height in the height reported
|
||||||
|
* to android that should be used, hence causing an overlap.
|
||||||
|
*
|
||||||
|
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
|
||||||
|
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
|
||||||
|
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
|
||||||
|
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
|
||||||
|
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
|
||||||
|
* Also check https://github.com/termux/termux-app/issues/1539.
|
||||||
|
*
|
||||||
|
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
|
||||||
|
* like number row, etc.
|
||||||
|
*
|
||||||
|
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
|
||||||
|
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
|
||||||
|
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
|
||||||
|
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
|
||||||
|
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
|
||||||
|
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
|
||||||
|
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
|
||||||
|
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
|
||||||
|
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
|
||||||
|
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
|
||||||
|
*/
|
||||||
|
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
|
||||||
|
public TermuxActivity mActivity;
|
||||||
|
public Integer marginBottom;
|
||||||
|
public Integer lastMarginBottom;
|
||||||
|
public long lastMarginBottomTime;
|
||||||
|
public long lastMarginBottomExtraTime;
|
||||||
|
|
||||||
|
/** Log root view events. */
|
||||||
|
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxActivityRootView";
|
||||||
|
|
||||||
|
private static int mStatusBarHeight;
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActivity(TermuxActivity activity) {
|
||||||
|
mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether root view logging is enabled or not.
|
||||||
|
*
|
||||||
|
* @param value The boolean value that defines the state.
|
||||||
|
*/
|
||||||
|
public void setIsRootViewLoggingEnabled(boolean value) {
|
||||||
|
ROOT_VIEW_LOGGING_ENABLED = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
|
||||||
|
if (marginBottom != null) {
|
||||||
|
if (ROOT_VIEW_LOGGING_ENABLED)
|
||||||
|
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
|
||||||
|
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
|
||||||
|
params.setMargins(0, 0, 0, marginBottom);
|
||||||
|
setLayoutParams(params);
|
||||||
|
marginBottom = null;
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGlobalLayout() {
|
||||||
|
if (mActivity == null || !mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView();
|
||||||
|
if (bottomSpaceView == null) return;
|
||||||
|
|
||||||
|
boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED;
|
||||||
|
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:");
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
// Get the position Rects of the bottom space view and the main window holding it
|
||||||
|
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight);
|
||||||
|
if (windowAndViewRects == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Rect windowAvailableRect = windowAndViewRects[0];
|
||||||
|
Rect bottomSpaceViewRect = windowAndViewRects[1];
|
||||||
|
|
||||||
|
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
|
||||||
|
//boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape
|
||||||
|
boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect);
|
||||||
|
boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0;
|
||||||
|
boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0);
|
||||||
|
|
||||||
|
if (root_view_logging_enabled) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "windowAvailableRect " + ViewUtils.toRectString(windowAvailableRect) + ", bottomSpaceViewRect " + ViewUtils.toRectString(bottomSpaceViewRect));
|
||||||
|
Logger.logVerbose(LOG_TAG, "windowAvailableRect.bottom " + windowAvailableRect.bottom +
|
||||||
|
", bottomSpaceViewRect.bottom " +bottomSpaceViewRect.bottom +
|
||||||
|
", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin +
|
||||||
|
", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) +
|
||||||
|
", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the bottomSpaceViewRect is visible, then remove the margin if needed
|
||||||
|
if (isVisible) {
|
||||||
|
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
|
||||||
|
// and a margin has been added
|
||||||
|
// Necessary so that we don't get stuck in an infinite loop since setting margin
|
||||||
|
// will call OnGlobalLayoutListener again and next time bottom space view
|
||||||
|
// will be visible and margin will be set to 0, which again will call
|
||||||
|
// OnGlobalLayoutListener...
|
||||||
|
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
|
||||||
|
// set appropriate margins when views are changed quickly since some changes
|
||||||
|
// may be missed.
|
||||||
|
if (isVisibleBecauseMargin) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Visible due to margin");
|
||||||
|
|
||||||
|
// Once the view has been redrawn with new margin, we set margin back to 0 so that
|
||||||
|
// when next time onMeasure() is called, margin 0 is used. This is necessary for
|
||||||
|
// cases when view has been redrawn with new margin because bottom space view was
|
||||||
|
// hidden by keyboard and then view was redrawn again due to layout change (like
|
||||||
|
// keyboard symbol view is switched to), android will add margin below its new position
|
||||||
|
// if its greater than 0, which was already above the keyboard creating x2x margin.
|
||||||
|
// Adding time check since moving split screen divider in landscape causes jitter
|
||||||
|
// and prevents some infinite loops
|
||||||
|
if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) {
|
||||||
|
lastMarginBottomTime = System.currentTimeMillis();
|
||||||
|
marginBottom = 0;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Ignoring restoring marginBottom to 0 since called to quickly");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean setMargin = params.bottomMargin != 0;
|
||||||
|
|
||||||
|
// If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect
|
||||||
|
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||||
|
// onGlobalLayout: Bottom margin already equals 0
|
||||||
|
if (isVisibleBecauseExtraMargin) {
|
||||||
|
// Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar
|
||||||
|
if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Resetting margin since visible due to extra margin");
|
||||||
|
lastMarginBottomExtraTime = System.currentTimeMillis();
|
||||||
|
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
|
||||||
|
lastMarginBottom = null;
|
||||||
|
setMargin = true;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Ignoring resetting margin since visible due to extra margin since called to quickly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setMargin) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Setting bottom margin to 0");
|
||||||
|
params.setMargins(0, 0, 0, 0);
|
||||||
|
setLayoutParams(params);
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Bottom margin already equals 0");
|
||||||
|
// This is done so that when next time onMeasure() is called, lastMarginBottom is used.
|
||||||
|
// This is done since we **expect** the keyboard to have same dimensions next time layout
|
||||||
|
// changes, so best set margin while view is drawn the first time, otherwise it will
|
||||||
|
// cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the
|
||||||
|
// likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic
|
||||||
|
// works fine for all cases.
|
||||||
|
marginBottom = lastMarginBottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
|
||||||
|
else {
|
||||||
|
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
|
||||||
|
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
|
||||||
|
|
||||||
|
boolean setMargin = params.bottomMargin != pxHidden;
|
||||||
|
|
||||||
|
// If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect
|
||||||
|
// is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener
|
||||||
|
// again, so that margins are set properly. May happen when toolbar/extra keys is disabled
|
||||||
|
// and enabled from left drawer, just like case for isVisibleBecauseExtraMargin.
|
||||||
|
// onMeasure: Setting bottom margin to 176
|
||||||
|
// onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||||
|
// onGlobalLayout: Bottom margin already equals 176
|
||||||
|
if (pxHidden > 0 && params.bottomMargin > 0) {
|
||||||
|
if (pxHidden != params.bottomMargin) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since not visible due to wrong margin");
|
||||||
|
pxHidden = 0;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Force setting margin since not visible despite required margin");
|
||||||
|
}
|
||||||
|
setMargin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pxHidden < 0) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative");
|
||||||
|
pxHidden = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (setMargin) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Setting bottom margin to " + pxHidden);
|
||||||
|
params.setMargins(0, 0, 0, pxHidden);
|
||||||
|
setLayoutParams(params);
|
||||||
|
lastMarginBottom = pxHidden;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
|
||||||
|
@Override
|
||||||
|
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||||
|
mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||||
|
// Let view window handle insets however it wants
|
||||||
|
return v.onApplyWindowInsets(insets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.shell.TermuxSession;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession> implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
|
||||||
|
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||||
|
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
||||||
|
|
||||||
|
public TermuxSessionsListViewController(TermuxActivity activity, List<TermuxSession> sessionList) {
|
||||||
|
super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, sessionList);
|
||||||
|
this.mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||||
|
View sessionRowView = convertView;
|
||||||
|
if (sessionRowView == null) {
|
||||||
|
LayoutInflater inflater = mActivity.getLayoutInflater();
|
||||||
|
sessionRowView = inflater.inflate(R.layout.item_terminal_sessions_list, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
|
||||||
|
|
||||||
|
TerminalSession sessionAtRow = getItem(position).getTerminalSession();
|
||||||
|
if (sessionAtRow == null) {
|
||||||
|
sessionTitleView.setText("null session");
|
||||||
|
return sessionRowView;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||||
|
|
||||||
|
if (isUsingBlackUI) {
|
||||||
|
sessionTitleView.setBackground(
|
||||||
|
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = sessionAtRow.mSessionName;
|
||||||
|
String sessionTitle = sessionAtRow.getTitle();
|
||||||
|
|
||||||
|
String numberPart = "[" + (position + 1) + "] ";
|
||||||
|
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
|
||||||
|
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
|
||||||
|
|
||||||
|
String fullSessionTitle = numberPart + sessionNamePart + sessionTitlePart;
|
||||||
|
SpannableString fullSessionTitleStyled = new SpannableString(fullSessionTitle);
|
||||||
|
fullSessionTitleStyled.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
fullSessionTitleStyled.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), fullSessionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
sessionTitleView.setText(fullSessionTitleStyled);
|
||||||
|
|
||||||
|
boolean sessionRunning = sessionAtRow.isRunning();
|
||||||
|
|
||||||
|
if (sessionRunning) {
|
||||||
|
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
|
||||||
|
} else {
|
||||||
|
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||||
|
}
|
||||||
|
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||||
|
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||||
|
sessionTitleView.setTextColor(color);
|
||||||
|
return sessionRowView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
TermuxSession clickedSession = getItem(position);
|
||||||
|
mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession());
|
||||||
|
mActivity.getDrawer().closeDrawers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
final TermuxSession selectedSession = getItem(position);
|
||||||
|
mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.getTerminalSession());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.media.AudioAttributes;
|
||||||
|
import android.media.SoundPool;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.widget.ListView;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.shell.TermuxSession;
|
||||||
|
import com.termux.shared.interact.TextInputDialogUtils;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.app.TermuxService;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.app.terminal.io.BellHandler;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.terminal.TerminalColors;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
import com.termux.terminal.TextStyle;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
||||||
|
|
||||||
|
private final TermuxActivity mActivity;
|
||||||
|
|
||||||
|
private static final int MAX_SESSIONS = 8;
|
||||||
|
|
||||||
|
private SoundPool mBellSoundPool;
|
||||||
|
|
||||||
|
private int mBellSoundId;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
||||||
|
|
||||||
|
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onCreate() is called
|
||||||
|
*/
|
||||||
|
public void onCreate() {
|
||||||
|
// Set terminal fonts and colors
|
||||||
|
checkForFontAndColors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onStart() is called
|
||||||
|
*/
|
||||||
|
public void onStart() {
|
||||||
|
// The service has connected, but data may have changed since we were last in the foreground.
|
||||||
|
// Get the session stored in shared preferences stored by {@link #onStop} if its valid,
|
||||||
|
// otherwise get the last session currently running.
|
||||||
|
if (mActivity.getTermuxService() != null) {
|
||||||
|
setCurrentSession(getCurrentStoredSessionOrLast());
|
||||||
|
termuxSessionListNotifyUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current terminal session may have changed while being away, force
|
||||||
|
// a refresh of the displayed terminal.
|
||||||
|
mActivity.getTerminalView().onScreenUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onResume() is called
|
||||||
|
*/
|
||||||
|
public void onResume() {
|
||||||
|
// Just initialize the mBellSoundPool and load the sound, otherwise bell might not run
|
||||||
|
// the first time bell key is pressed and play() is called, since sound may not be loaded
|
||||||
|
// quickly enough before the call to play(). https://stackoverflow.com/questions/35435625
|
||||||
|
getBellSoundPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onStop() is called
|
||||||
|
*/
|
||||||
|
public void onStop() {
|
||||||
|
// Store current session in shared preferences so that it can be restored later in
|
||||||
|
// {@link #onStart} if needed.
|
||||||
|
setCurrentStoredSession();
|
||||||
|
|
||||||
|
// Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown
|
||||||
|
// java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds
|
||||||
|
// Bell is not played in background anyways
|
||||||
|
// Related: https://stackoverflow.com/a/28708351/14686958
|
||||||
|
releaseBellSoundPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.reloadActivityStyling() is called
|
||||||
|
*/
|
||||||
|
public void onReload() {
|
||||||
|
// Set terminal fonts and colors
|
||||||
|
checkForFontAndColors();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(TerminalSession changedSession) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTitleChanged(TerminalSession updatedSession) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
if (updatedSession != mActivity.getCurrentSession()) {
|
||||||
|
// Only show toast for other sessions than the current one, since the user
|
||||||
|
// probably consciously caused the title change to change in the current session
|
||||||
|
// and don't want an annoying toast for that.
|
||||||
|
mActivity.showToast(toToastTitle(updatedSession), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
termuxSessionListNotifyUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
|
||||||
|
if (service == null || service.wantsToStop()) {
|
||||||
|
// The service wants to stop as soon as possible.
|
||||||
|
mActivity.finishActivityIfNotFinishing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
|
||||||
|
// Show toast for non-current sessions that exit.
|
||||||
|
int indexOfSession = service.getIndexOfSession(finishedSession);
|
||||||
|
// Verify that session was not removed before we got told about it finishing:
|
||||||
|
if (indexOfSession >= 0)
|
||||||
|
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
|
// On Android TV devices we need to use older behaviour because we may
|
||||||
|
// not be able to have multiple launcher icons.
|
||||||
|
if (service.getTermuxSessionsSize() > 1) {
|
||||||
|
removeFinishedSession(finishedSession);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Once we have a separate launcher icon for the failsafe session, it
|
||||||
|
// should be safe to auto-close session on exit code '0' or '130'.
|
||||||
|
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
|
||||||
|
removeFinishedSession(finishedSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClipboardText(TerminalSession session, String text) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBell(TerminalSession session) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||||
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
|
||||||
|
BellHandler.getInstance(mActivity).doBell();
|
||||||
|
break;
|
||||||
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
||||||
|
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||||
|
break;
|
||||||
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||||
|
// Ignore the bell character.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onColorsChanged(TerminalSession changedSession) {
|
||||||
|
if (mActivity.getCurrentSession() == changedSession)
|
||||||
|
updateBackgroundColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTerminalCursorStateChange(boolean enabled) {
|
||||||
|
// Do not start cursor blinking thread if activity is not visible
|
||||||
|
if (enabled && !mActivity.isVisible()) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cursor is to enabled now, then start cursor blinking if blinking is enabled
|
||||||
|
// otherwise stop cursor blinking
|
||||||
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onResetTerminalSession() is called
|
||||||
|
*/
|
||||||
|
public void onResetTerminalSession() {
|
||||||
|
// Ensure blinker starts again after reset if cursor blinking was disabled before reset like
|
||||||
|
// with "tput civis" which would have called onTerminalCursorStateChange()
|
||||||
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getTerminalCursorStyle() {
|
||||||
|
return mActivity.getProperties().getTerminalCursorStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Initialize and get mBellSoundPool */
|
||||||
|
private synchronized SoundPool getBellSoundPool() {
|
||||||
|
if (mBellSoundPool == null) {
|
||||||
|
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||||
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||||
|
|
||||||
|
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mBellSoundPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release mBellSoundPool resources */
|
||||||
|
private synchronized void releaseBellSoundPool() {
|
||||||
|
if (mBellSoundPool != null) {
|
||||||
|
mBellSoundPool.release();
|
||||||
|
mBellSoundPool = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Try switching to session. */
|
||||||
|
public void setCurrentSession(TerminalSession session) {
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
if (mActivity.getTerminalView().attachSession(session)) {
|
||||||
|
// notify about switched session if not already displaying the session
|
||||||
|
notifyOfSessionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We call the following even when the session is already being displayed since config may
|
||||||
|
// be stale, like current session not selected or scrolled to.
|
||||||
|
checkAndScrollToSession(session);
|
||||||
|
updateBackgroundColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyOfSessionChange() {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
mActivity.showToast(toToastTitle(session), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void switchToSession(boolean forward) {
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return;
|
||||||
|
|
||||||
|
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
|
||||||
|
int index = service.getIndexOfSession(currentTerminalSession);
|
||||||
|
int size = service.getTermuxSessionsSize();
|
||||||
|
if (forward) {
|
||||||
|
if (++index >= size) index = 0;
|
||||||
|
} else {
|
||||||
|
if (--index < 0) index = size - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||||
|
if (termuxSession != null)
|
||||||
|
setCurrentSession(termuxSession.getTerminalSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void switchToSession(int index) {
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return;
|
||||||
|
|
||||||
|
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||||
|
if (termuxSession != null)
|
||||||
|
setCurrentSession(termuxSession.getTerminalSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
public void renameSession(final TerminalSession sessionToRename) {
|
||||||
|
if (sessionToRename == null) return;
|
||||||
|
|
||||||
|
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||||
|
sessionToRename.mSessionName = text;
|
||||||
|
termuxSessionListNotifyUpdated();
|
||||||
|
}, -1, null, -1, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return;
|
||||||
|
|
||||||
|
if (service.getTermuxSessionsSize() >= MAX_SESSIONS) {
|
||||||
|
new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
|
||||||
|
.setPositiveButton(android.R.string.ok, null).show();
|
||||||
|
} else {
|
||||||
|
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||||
|
|
||||||
|
String workingDirectory;
|
||||||
|
if (currentSession == null) {
|
||||||
|
workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory();
|
||||||
|
} else {
|
||||||
|
workingDirectory = currentSession.getCwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
|
||||||
|
if (newTermuxSession == null) return;
|
||||||
|
|
||||||
|
TerminalSession newTerminalSession = newTermuxSession.getTerminalSession();
|
||||||
|
setCurrentSession(newTerminalSession);
|
||||||
|
|
||||||
|
mActivity.getDrawer().closeDrawers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentStoredSession() {
|
||||||
|
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||||
|
if (currentSession != null)
|
||||||
|
mActivity.getPreferences().setCurrentSession(currentSession.mHandle);
|
||||||
|
else
|
||||||
|
mActivity.getPreferences().setCurrentSession(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The current session as stored or the last one if that does not exist. */
|
||||||
|
public TerminalSession getCurrentStoredSessionOrLast() {
|
||||||
|
TerminalSession stored = getCurrentStoredSession();
|
||||||
|
|
||||||
|
if (stored != null) {
|
||||||
|
// If a stored session is in the list of currently running sessions, then return it
|
||||||
|
return stored;
|
||||||
|
} else {
|
||||||
|
// Else return the last session currently running
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return null;
|
||||||
|
|
||||||
|
TermuxSession termuxSession = service.getLastTermuxSession();
|
||||||
|
if (termuxSession != null)
|
||||||
|
return termuxSession.getTerminalSession();
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerminalSession getCurrentStoredSession() {
|
||||||
|
String sessionHandle = mActivity.getPreferences().getCurrentSession();
|
||||||
|
|
||||||
|
// If no session is stored in shared preferences
|
||||||
|
if (sessionHandle == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Check if the session handle found matches one of the currently running sessions
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return null;
|
||||||
|
|
||||||
|
return service.getTerminalSessionForHandle(sessionHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFinishedSession(TerminalSession finishedSession) {
|
||||||
|
// Return pressed with finished session - remove it.
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return;
|
||||||
|
|
||||||
|
int index = service.removeTermuxSession(finishedSession);
|
||||||
|
|
||||||
|
int size = service.getTermuxSessionsSize();
|
||||||
|
if (size == 0) {
|
||||||
|
// There are no sessions to show, so finish the activity.
|
||||||
|
mActivity.finishActivityIfNotFinishing();
|
||||||
|
} else {
|
||||||
|
if (index >= size) {
|
||||||
|
index = size - 1;
|
||||||
|
}
|
||||||
|
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||||
|
if (termuxSession != null)
|
||||||
|
setCurrentSession(termuxSession.getTerminalSession());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void termuxSessionListNotifyUpdated() {
|
||||||
|
mActivity.termuxSessionListNotifyUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkAndScrollToSession(TerminalSession session) {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return;
|
||||||
|
|
||||||
|
final int indexOfSession = service.getIndexOfSession(session);
|
||||||
|
if (indexOfSession < 0) return;
|
||||||
|
final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list);
|
||||||
|
if (termuxSessionsListView == null) return;
|
||||||
|
|
||||||
|
termuxSessionsListView.setItemChecked(indexOfSession, true);
|
||||||
|
// Delay is necessary otherwise sometimes scroll to newly added session does not happen
|
||||||
|
termuxSessionsListView.postDelayed(() -> termuxSessionsListView.smoothScrollToPosition(indexOfSession), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String toToastTitle(TerminalSession session) {
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return null;
|
||||||
|
|
||||||
|
final int indexOfSession = service.getIndexOfSession(session);
|
||||||
|
if (indexOfSession < 0) return null;
|
||||||
|
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
|
||||||
|
if (!TextUtils.isEmpty(session.mSessionName)) {
|
||||||
|
toastTitle.append(" ").append(session.mSessionName);
|
||||||
|
}
|
||||||
|
String title = session.getTitle();
|
||||||
|
if (!TextUtils.isEmpty(title)) {
|
||||||
|
// Space to "[${NR}] or newline after session name:
|
||||||
|
toastTitle.append(session.mSessionName == null ? " " : "\n");
|
||||||
|
toastTitle.append(title);
|
||||||
|
}
|
||||||
|
return toastTitle.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void checkForFontAndColors() {
|
||||||
|
try {
|
||||||
|
File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE;
|
||||||
|
File fontFile = TermuxConstants.TERMUX_FONT_FILE;
|
||||||
|
|
||||||
|
final Properties props = new Properties();
|
||||||
|
if (colorsFile.isFile()) {
|
||||||
|
try (InputStream in = new FileInputStream(colorsFile)) {
|
||||||
|
props.load(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalColors.COLOR_SCHEME.updateWith(props);
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session != null && session.getEmulator() != null) {
|
||||||
|
session.getEmulator().mColors.reset();
|
||||||
|
}
|
||||||
|
updateBackgroundColor();
|
||||||
|
|
||||||
|
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
||||||
|
mActivity.getTerminalView().setTypeface(newTypeface);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateBackgroundColor() {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session != null && session.getEmulator() != null) {
|
||||||
|
mActivity.getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,692 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.data.UrlUtils;
|
||||||
|
import com.termux.shared.shell.ShellUtils;
|
||||||
|
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
import com.termux.shared.view.KeyboardUtils;
|
||||||
|
import com.termux.shared.view.ViewUtils;
|
||||||
|
import com.termux.terminal.KeyHandler;
|
||||||
|
import com.termux.terminal.TerminalEmulator;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
|
||||||
|
public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
|
||||||
|
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||||
|
|
||||||
|
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||||
|
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||||
|
|
||||||
|
private Runnable mShowSoftKeyboardRunnable;
|
||||||
|
|
||||||
|
private boolean mShowSoftKeyboardIgnoreOnce;
|
||||||
|
private boolean mShowSoftKeyboardWithDelayOnce;
|
||||||
|
|
||||||
|
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
||||||
|
|
||||||
|
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onCreate() is called
|
||||||
|
*/
|
||||||
|
public void onCreate() {
|
||||||
|
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||||
|
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onStart() is called
|
||||||
|
*/
|
||||||
|
public void onStart() {
|
||||||
|
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
|
||||||
|
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
|
||||||
|
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
|
||||||
|
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
|
|
||||||
|
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
|
||||||
|
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
|
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onResume() is called
|
||||||
|
*/
|
||||||
|
public void onResume() {
|
||||||
|
// Show the soft keyboard if required
|
||||||
|
setSoftKeyboardState(true, false);
|
||||||
|
|
||||||
|
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||||
|
|
||||||
|
if (mActivity.getTerminalView().mEmulator != null) {
|
||||||
|
// Start terminal cursor blinking if enabled
|
||||||
|
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
|
||||||
|
// event to start it. This is needed since onEmulatorSet() may not be called after
|
||||||
|
// TermuxActivity is started after device display timeout with double tap and not power button.
|
||||||
|
setTerminalCursorBlinkerState(true);
|
||||||
|
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onStop() is called
|
||||||
|
*/
|
||||||
|
public void onStop() {
|
||||||
|
// Stop terminal cursor blinking if enabled
|
||||||
|
setTerminalCursorBlinkerState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.reloadActivityStyling() is called
|
||||||
|
*/
|
||||||
|
public void onReload() {
|
||||||
|
// Show the soft keyboard if required
|
||||||
|
setSoftKeyboardState(false, true);
|
||||||
|
|
||||||
|
// Start terminal cursor blinking if enabled
|
||||||
|
setTerminalCursorBlinkerState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when {@link com.termux.view.TerminalView#mEmulator}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onEmulatorSet() {
|
||||||
|
if (!mTerminalCursorBlinkerStateAlreadySet) {
|
||||||
|
// Start terminal cursor blinking if enabled
|
||||||
|
// We need to wait for the first session to be attached that's set in
|
||||||
|
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
|
||||||
|
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
|
||||||
|
// blinker will not start again if TermuxActivity is started again after exiting it with
|
||||||
|
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
|
||||||
|
setTerminalCursorBlinkerState(true);
|
||||||
|
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float onScale(float scale) {
|
||||||
|
if (scale < 0.9f || scale > 1.1f) {
|
||||||
|
boolean increase = scale > 1.f;
|
||||||
|
changeFontSize(increase);
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleTapUp(MotionEvent e) {
|
||||||
|
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
|
||||||
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
else
|
||||||
|
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldBackButtonBeMappedToEscape() {
|
||||||
|
return mActivity.getProperties().isBackKeyTheEscapeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldEnforceCharBasedInput() {
|
||||||
|
return mActivity.getProperties().isEnforcingCharBasedInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldUseCtrlSpaceWorkaround() {
|
||||||
|
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyModeChanged(boolean copyMode) {
|
||||||
|
// Disable drawer while copying.
|
||||||
|
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("RtlHardcoded")
|
||||||
|
@Override
|
||||||
|
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
||||||
|
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||||
|
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||||
|
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
||||||
|
return true;
|
||||||
|
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
||||||
|
// Get the unmodified code point:
|
||||||
|
int unicodeChar = e.getUnicodeChar(0);
|
||||||
|
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(true);
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(false);
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||||
|
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||||
|
mActivity.getDrawer().closeDrawers();
|
||||||
|
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||||
|
onToggleSoftKeyboardRequest();
|
||||||
|
} else if (unicodeChar == 'm'/* menu */) {
|
||||||
|
mActivity.getTerminalView().showContextMenu();
|
||||||
|
} else if (unicodeChar == 'r'/* rename */) {
|
||||||
|
mTermuxTerminalSessionClient.renameSession(currentSession);
|
||||||
|
} else if (unicodeChar == 'c'/* create */) {
|
||||||
|
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||||
|
} else if (unicodeChar == 'u' /* urls */) {
|
||||||
|
showUrlSelection();
|
||||||
|
} else if (unicodeChar == 'v') {
|
||||||
|
doPaste();
|
||||||
|
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
|
||||||
|
// We also check for the shifted char here since shift may be required to produce '+',
|
||||||
|
// see https://github.com/termux/termux-api/issues/2
|
||||||
|
changeFontSize(true);
|
||||||
|
} else if (unicodeChar == '-') {
|
||||||
|
changeFontSize(false);
|
||||||
|
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||||
|
int index = unicodeChar - '1';
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(index);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||||
|
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
|
||||||
|
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
|
||||||
|
mActivity.finishActivityIfNotFinishing();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleVirtualKeys(keyCode, e, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||||
|
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||||
|
InputDevice inputDevice = event.getDevice();
|
||||||
|
if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) {
|
||||||
|
return false;
|
||||||
|
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||||
|
// Do not steal dedicated buttons from a full external keyboard.
|
||||||
|
return false;
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||||
|
mVirtualControlKeyDown = down;
|
||||||
|
return true;
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
|
mVirtualFnKeyDown = down;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readControlKey() {
|
||||||
|
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readAltKey() {
|
||||||
|
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onLongPress(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||||
|
if (mVirtualFnKeyDown) {
|
||||||
|
int resultingKeyCode = -1;
|
||||||
|
int resultingCodePoint = -1;
|
||||||
|
boolean altDown = false;
|
||||||
|
int lowerCase = Character.toLowerCase(codePoint);
|
||||||
|
switch (lowerCase) {
|
||||||
|
// Arrow keys.
|
||||||
|
case 'w':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Page up and down.
|
||||||
|
case 'p':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Some special keys:
|
||||||
|
case 't':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_TAB;
|
||||||
|
break;
|
||||||
|
case 'i':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
resultingCodePoint = '~';
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Special characters to input.
|
||||||
|
case 'u':
|
||||||
|
resultingCodePoint = '_';
|
||||||
|
break;
|
||||||
|
case 'l':
|
||||||
|
resultingCodePoint = '|';
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Function keys.
|
||||||
|
case '1':
|
||||||
|
case '2':
|
||||||
|
case '3':
|
||||||
|
case '4':
|
||||||
|
case '5':
|
||||||
|
case '6':
|
||||||
|
case '7':
|
||||||
|
case '8':
|
||||||
|
case '9':
|
||||||
|
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
|
||||||
|
break;
|
||||||
|
case '0':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_F10;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Other special keys.
|
||||||
|
case 'e':
|
||||||
|
resultingCodePoint = /*Escape*/ 27;
|
||||||
|
break;
|
||||||
|
case '.':
|
||||||
|
resultingCodePoint = /*^.*/ 28;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'b': // alt+b, jumping backward in readline.
|
||||||
|
case 'f': // alf+f, jumping forward in readline.
|
||||||
|
case 'x': // alt+x, common in emacs.
|
||||||
|
resultingCodePoint = lowerCase;
|
||||||
|
altDown = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Volume control.
|
||||||
|
case 'v':
|
||||||
|
resultingCodePoint = -1;
|
||||||
|
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Writing mode:
|
||||||
|
case 'q':
|
||||||
|
case 'k':
|
||||||
|
mActivity.toggleTerminalToolbar();
|
||||||
|
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultingKeyCode != -1) {
|
||||||
|
TerminalEmulator term = session.getEmulator();
|
||||||
|
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
|
||||||
|
} else if (resultingCodePoint != -1) {
|
||||||
|
session.writeCodePoint(altDown, resultingCodePoint);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (ctrlDown) {
|
||||||
|
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||||
|
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
||||||
|
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||||
|
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||||
|
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||||
|
KeyboardShortcut shortcut = shortcuts.get(i);
|
||||||
|
if (codePointLowerCase == shortcut.codePoint) {
|
||||||
|
switch (shortcut.shortcutAction) {
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(true);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(false);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void changeFontSize(boolean increase) {
|
||||||
|
mActivity.getPreferences().changeFontSize(increase);
|
||||||
|
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in
|
||||||
|
* drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut.
|
||||||
|
*/
|
||||||
|
public void onToggleSoftKeyboardRequest() {
|
||||||
|
// If soft keyboard toggle behaviour is enable/disabled
|
||||||
|
if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) {
|
||||||
|
// If soft keyboard is visible
|
||||||
|
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle");
|
||||||
|
mActivity.getPreferences().setSoftKeyboardEnabled(false);
|
||||||
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
} else {
|
||||||
|
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
|
||||||
|
// switching back from another app if keyboard was previously disabled by user.
|
||||||
|
// Also request focus, since it wouldn't have been requested at startup by
|
||||||
|
// setSoftKeyboardState if keyboard was disabled. #2112
|
||||||
|
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
|
||||||
|
mActivity.getPreferences().setSoftKeyboardEnabled(true);
|
||||||
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||||
|
if(mShowSoftKeyboardWithDelayOnce) {
|
||||||
|
mShowSoftKeyboardWithDelayOnce = false;
|
||||||
|
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
} else
|
||||||
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If soft keyboard toggle behaviour is show/hide
|
||||||
|
else {
|
||||||
|
// If soft keyboard is disabled by user for Termux
|
||||||
|
if (!mActivity.getPreferences().isSoftKeyboardEnabled()) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle");
|
||||||
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
} else {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Showing/Hiding soft keyboard on toggle");
|
||||||
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||||
|
KeyboardUtils.toggleSoftKeyboard(mActivity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
||||||
|
boolean noRequestFocus = false;
|
||||||
|
|
||||||
|
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
||||||
|
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
||||||
|
mActivity.getPreferences().isSoftKeyboardEnabled(),
|
||||||
|
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
||||||
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
noRequestFocus = true;
|
||||||
|
// Delay is only required if onCreate() is called like when Termux app is exited with
|
||||||
|
// double back press, not when Termux app is switched back from another app and keyboard
|
||||||
|
// toggle is pressed to enable keyboard
|
||||||
|
if (isStartup && mActivity.isOnResumeAfterOnCreate())
|
||||||
|
mShowSoftKeyboardWithDelayOnce = true;
|
||||||
|
} else {
|
||||||
|
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
|
||||||
|
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
|
||||||
|
|
||||||
|
// Clear any previous flags to disable soft keyboard in case setting updated
|
||||||
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||||
|
|
||||||
|
// If soft keyboard is to be hidden on startup
|
||||||
|
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
|
||||||
|
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
// Required to keep keyboard hidden when Termux app is switched back from another app
|
||||||
|
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
||||||
|
noRequestFocus = true;
|
||||||
|
// Required to keep keyboard hidden on app startup
|
||||||
|
mShowSoftKeyboardIgnoreOnce = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onFocusChange(View view, boolean hasFocus) {
|
||||||
|
// Force show soft keyboard if TerminalView or toolbar text input view has
|
||||||
|
// focus and close it if they don't
|
||||||
|
boolean textInputViewHasFocus = false;
|
||||||
|
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
|
||||||
|
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
|
||||||
|
|
||||||
|
if (hasFocus || textInputViewHasFocus) {
|
||||||
|
if (mShowSoftKeyboardIgnoreOnce) {
|
||||||
|
mShowSoftKeyboardIgnoreOnce = false; return;
|
||||||
|
}
|
||||||
|
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
||||||
|
} else {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
||||||
|
// or soft keyboard is to be hidden or is disabled
|
||||||
|
if (!isReloadTermuxProperties && !noRequestFocus) {
|
||||||
|
// Request focus for TerminalView
|
||||||
|
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
|
||||||
|
// had focus on startup to show the keyboard, like when opening url with context menu
|
||||||
|
// "Select URL" long press and returning to Termux app with back button. This
|
||||||
|
// will also show keyboard even if it was closed before opening url. #2111
|
||||||
|
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable getShowSoftKeyboardRunnable() {
|
||||||
|
if (mShowSoftKeyboardRunnable == null) {
|
||||||
|
mShowSoftKeyboardRunnable = () -> {
|
||||||
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mShowSoftKeyboardRunnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void setTerminalCursorBlinkerState(boolean start) {
|
||||||
|
if (start) {
|
||||||
|
// If set/update the cursor blinking rate is successful, then enable cursor blinker
|
||||||
|
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
|
||||||
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||||
|
else
|
||||||
|
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
|
||||||
|
} else {
|
||||||
|
// Disable cursor blinker
|
||||||
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void shareSessionTranscript() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||||
|
if (transcriptText == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// See https://github.com/termux/termux-app/issues/1166.
|
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
intent.setType("text/plain");
|
||||||
|
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||||
|
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
||||||
|
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showUrlSelection() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||||
|
|
||||||
|
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
|
||||||
|
if (urlSet.isEmpty()) {
|
||||||
|
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
|
||||||
|
Collections.reverse(Arrays.asList(urls)); // Latest first.
|
||||||
|
|
||||||
|
// Click to copy url to clipboard:
|
||||||
|
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||||
|
String url = (String) urls[which];
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||||
|
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||||
|
}).setTitle(R.string.title_select_url_dialog).create();
|
||||||
|
|
||||||
|
// Long press to open URL:
|
||||||
|
dialog.setOnShowListener(di -> {
|
||||||
|
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
|
||||||
|
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
String url = (String) urls[position];
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
try {
|
||||||
|
mActivity.startActivity(i, null);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
// If no applications match, Android displays a system message.
|
||||||
|
mActivity.startActivity(Intent.createChooser(i, null));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportIssueFromTranscript() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||||
|
if (transcriptText == null) return;
|
||||||
|
|
||||||
|
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
String transcriptTextTruncated = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||||
|
|
||||||
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
||||||
|
|
||||||
|
reportString.append("## Transcript\n");
|
||||||
|
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true));
|
||||||
|
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||||
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
||||||
|
|
||||||
|
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||||
|
if (termuxAptInfo != null)
|
||||||
|
reportString.append("\n\n").append(termuxAptInfo);
|
||||||
|
|
||||||
|
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(), TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doPaste() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
if (!session.isRunning()) return;
|
||||||
|
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clipData = clipboard.getPrimaryClip();
|
||||||
|
if (clipData == null) return;
|
||||||
|
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||||
|
if (!TextUtils.isEmpty(paste))
|
||||||
|
session.getEmulator().paste(paste.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.termux.app;
|
package com.termux.app.terminal.io;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@@ -6,15 +6,15 @@ import android.os.Looper;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.os.Vibrator;
|
import android.os.Vibrator;
|
||||||
|
|
||||||
public class BellUtil {
|
public class BellHandler {
|
||||||
private static BellUtil instance = null;
|
private static BellHandler instance = null;
|
||||||
private static final Object lock = new Object();
|
private static final Object lock = new Object();
|
||||||
|
|
||||||
public static BellUtil getInstance(Context context) {
|
public static BellHandler getInstance(Context context) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new BellUtil((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ public class BellUtil {
|
|||||||
private long lastBell = 0;
|
private long lastBell = 0;
|
||||||
private final Runnable bellRunnable;
|
private final Runnable bellRunnable;
|
||||||
|
|
||||||
private BellUtil(final Vibrator vibrator) {
|
private BellHandler(final Vibrator vibrator) {
|
||||||
bellRunnable = new Runnable() {
|
bellRunnable = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -47,7 +47,7 @@ public class BellUtil {
|
|||||||
if (timeSinceLastBell < 0) {
|
if (timeSinceLastBell < 0) {
|
||||||
// there is a next bell pending; don't schedule another one
|
// there is a next bell pending; don't schedule another one
|
||||||
} else if (timeSinceLastBell < MIN_PAUSE) {
|
} else if (timeSinceLastBell < MIN_PAUSE) {
|
||||||
// there was a bell recently, scheudle the next one
|
// there was a bell recently, schedule the next one
|
||||||
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
||||||
lastBell = lastBell + MIN_PAUSE;
|
lastBell = lastBell + MIN_PAUSE;
|
||||||
} else {
|
} else {
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.termux.app.terminal.io;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible.
|
||||||
|
* This class is derived from:
|
||||||
|
* https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible
|
||||||
|
* and has some additional tweaks
|
||||||
|
* ---
|
||||||
|
* For more information, see https://issuetracker.google.com/issues/36911528
|
||||||
|
*/
|
||||||
|
public class FullScreenWorkAround {
|
||||||
|
private final View mChildOfContent;
|
||||||
|
private int mUsableHeightPrevious;
|
||||||
|
private final ViewGroup.LayoutParams mViewGroupLayoutParams;
|
||||||
|
|
||||||
|
private final int mNavBarHeight;
|
||||||
|
|
||||||
|
|
||||||
|
public static void apply(TermuxActivity activity) {
|
||||||
|
new FullScreenWorkAround(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FullScreenWorkAround(TermuxActivity activity) {
|
||||||
|
ViewGroup content = activity.findViewById(android.R.id.content);
|
||||||
|
mChildOfContent = content.getChildAt(0);
|
||||||
|
mViewGroupLayoutParams = mChildOfContent.getLayoutParams();
|
||||||
|
mNavBarHeight = activity.getNavBarHeight();
|
||||||
|
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void possiblyResizeChildOfContent() {
|
||||||
|
int usableHeightNow = computeUsableHeight();
|
||||||
|
if (usableHeightNow != mUsableHeightPrevious) {
|
||||||
|
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
|
||||||
|
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
|
||||||
|
if (heightDifference > (usableHeightSansKeyboard / 4)) {
|
||||||
|
// keyboard probably just became visible
|
||||||
|
|
||||||
|
// ensures that usable layout space does not extend behind the
|
||||||
|
// soft keyboard, causing the extra keys to not be visible
|
||||||
|
mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight();
|
||||||
|
} else {
|
||||||
|
// keyboard probably just became hidden
|
||||||
|
mViewGroupLayoutParams.height = usableHeightSansKeyboard;
|
||||||
|
}
|
||||||
|
mChildOfContent.requestLayout();
|
||||||
|
mUsableHeightPrevious = usableHeightNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getNavBarHeight() {
|
||||||
|
return mNavBarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int computeUsableHeight() {
|
||||||
|
Rect r = new Rect();
|
||||||
|
mChildOfContent.getWindowVisibleDisplayFrame(r);
|
||||||
|
return (r.bottom - r.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.termux.app.terminal.io;
|
||||||
|
|
||||||
|
public class KeyboardShortcut {
|
||||||
|
|
||||||
|
public final int codePoint;
|
||||||
|
public final int shortcutAction;
|
||||||
|
|
||||||
|
public KeyboardShortcut(int codePoint, int shortcutAction) {
|
||||||
|
this.codePoint = codePoint;
|
||||||
|
this.shortcutAction = shortcutAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.termux.app.terminal.io;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
|
public class TerminalToolbarViewPager {
|
||||||
|
|
||||||
|
public static class PageAdapter extends PagerAdapter {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
String mSavedTextInput;
|
||||||
|
|
||||||
|
public PageAdapter(TermuxActivity activity, String savedTextInput) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
this.mSavedTextInput = savedTextInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||||
|
return view == object;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(mActivity);
|
||||||
|
View layout;
|
||||||
|
if (position == 0) {
|
||||||
|
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
||||||
|
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||||
|
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
|
||||||
|
mActivity.setExtraKeysView(extraKeysView);
|
||||||
|
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||||
|
|
||||||
|
// apply extra keys fix if enabled in prefs
|
||||||
|
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
||||||
|
FullScreenWorkAround.apply(mActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
layout = inflater.inflate(R.layout.view_terminal_toolbar_text_input, collection, false);
|
||||||
|
final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input);
|
||||||
|
|
||||||
|
if (mSavedTextInput != null) {
|
||||||
|
editText.setText(mSavedTextInput);
|
||||||
|
mSavedTextInput = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
editText.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session != null) {
|
||||||
|
if (session.isRunning()) {
|
||||||
|
String textToSend = editText.getText().toString();
|
||||||
|
if (textToSend.length() == 0) textToSend = "\r";
|
||||||
|
session.write(textToSend);
|
||||||
|
} else {
|
||||||
|
mActivity.getTermuxTerminalSessionClient().removeFinishedSession(session);
|
||||||
|
}
|
||||||
|
editText.setText("");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
collection.addView(layout);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
|
||||||
|
collection.removeView((View) view);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static class OnPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
final ViewPager mTerminalToolbarViewPager;
|
||||||
|
|
||||||
|
public OnPageChangeListener(TermuxActivity activity, ViewPager viewPager) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
this.mTerminalToolbarViewPager = viewPager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageSelected(int position) {
|
||||||
|
if (position == 0) {
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
} else {
|
||||||
|
final EditText editText = mTerminalToolbarViewPager.findViewById(R.id.terminal_toolbar_text_input);
|
||||||
|
if (editText != null) editText.requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.termux.app.terminal.io.extrakeys;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class ExtraKeyButton {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key that will be sent to the terminal, either a control character
|
||||||
|
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||||
|
* some text.
|
||||||
|
*/
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||||
|
*/
|
||||||
|
private final boolean macro;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text that will be shown on the button.
|
||||||
|
*/
|
||||||
|
private final String display;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The information of the popup (triggered by swipe up).
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private ExtraKeyButton popup;
|
||||||
|
|
||||||
|
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||||
|
this(charDisplayMap, config, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
|
||||||
|
String keyFromConfig = config.optString("key", null);
|
||||||
|
String macroFromConfig = config.optString("macro", null);
|
||||||
|
String[] keys;
|
||||||
|
if (keyFromConfig != null && macroFromConfig != null) {
|
||||||
|
throw new JSONException("Both key and macro can't be set for the same key");
|
||||||
|
} else if (keyFromConfig != null) {
|
||||||
|
keys = new String[]{keyFromConfig};
|
||||||
|
this.macro = false;
|
||||||
|
} else if (macroFromConfig != null) {
|
||||||
|
keys = macroFromConfig.split(" ");
|
||||||
|
this.macro = true;
|
||||||
|
} else {
|
||||||
|
throw new JSONException("All keys have to specify either key or macro");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.key = TextUtils.join(" ", keys);
|
||||||
|
|
||||||
|
String displayFromConfig = config.optString("display", null);
|
||||||
|
if (displayFromConfig != null) {
|
||||||
|
this.display = displayFromConfig;
|
||||||
|
} else {
|
||||||
|
this.display = Arrays.stream(keys)
|
||||||
|
.map(key -> charDisplayMap.get(key, key))
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.popup = popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMacro() {
|
||||||
|
return macro;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplay() {
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ExtraKeyButton getPopup() {
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package com.termux.app.terminal.io.extrakeys;
|
||||||
|
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
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:
|
||||||
|
if (!TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(style))
|
||||||
|
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + style + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
|
||||||
|
return defaultCharDisplay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,388 @@
|
|||||||
|
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.widget.Button;
|
||||||
|
import android.widget.GridLayout;
|
||||||
|
import android.widget.PopupWindow;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||||
|
import com.termux.view.TerminalView;
|
||||||
|
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
if(mTermuxTerminalViewClient != null)
|
||||||
|
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
|
||||||
|
} else if ("DRAWER".equals(keyName)) {
|
||||||
|
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||||
|
drawer.openDrawer(Gravity.LEFT);
|
||||||
|
} else if (keyCodesForString.containsKey(keyName)) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
|
||||||
|
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
188
app/src/main/java/com/termux/app/utils/CrashUtils.java
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package com.termux.app.utils;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
public class CrashUtils {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "CrashUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the user of an app crash at last run by reading the crash info from the crash log file
|
||||||
|
* at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||||
|
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||||
|
*
|
||||||
|
* If the crash log file exists and is not empty and
|
||||||
|
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
|
||||||
|
* enabled, then a notification will be shown for the crash on the
|
||||||
|
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
|
||||||
|
*
|
||||||
|
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTagParam The log tag to use for logging.
|
||||||
|
*/
|
||||||
|
public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||||
|
if (preferences == null) return;
|
||||||
|
|
||||||
|
// If user has disabled notifications for crashes
|
||||||
|
if (!preferences.areCrashReportNotificationsEnabled())
|
||||||
|
return;
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
|
||||||
|
|
||||||
|
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Error error;
|
||||||
|
StringBuilder reportStringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
// Read report string from crash log file
|
||||||
|
error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||||
|
if (error != null) {
|
||||||
|
Logger.logErrorExtended(logTag, error.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move crash log file to backup location if it exists
|
||||||
|
error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||||
|
if (error != null) {
|
||||||
|
Logger.logErrorExtended(logTag, error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
String reportString = reportStringBuilder.toString();
|
||||||
|
|
||||||
|
if (reportString.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
|
||||||
|
|
||||||
|
sendCrashReportNotification(context, logTag, reportString, false);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param reportString The text for the crash report.
|
||||||
|
* @param forceNotification If set to {@code true}, then a notification will be shown
|
||||||
|
* regardless of if pending intent is {@code null} or
|
||||||
|
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
|
||||||
|
* is {@code false}.
|
||||||
|
*/
|
||||||
|
public static void sendCrashReportNotification(final Context context, String logTag, String reportString, boolean forceNotification) {
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||||
|
if (preferences == null) return;
|
||||||
|
|
||||||
|
// If user has disabled notifications for crashes
|
||||||
|
if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification)
|
||||||
|
return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||||
|
// to show the details of the crash
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupCrashReportsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param title The title for the notification.
|
||||||
|
* @param notificationText The second line text of the notification.
|
||||||
|
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||||
|
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||||
|
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||||
|
* @return Returns the {@link Notification.Builder}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||||
|
|
||||||
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||||
|
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||||
|
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||||
|
|
||||||
|
if (builder == null) return null;
|
||||||
|
|
||||||
|
// Enable timestamp
|
||||||
|
builder.setShowWhen(true);
|
||||||
|
|
||||||
|
// Set notification icon
|
||||||
|
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||||
|
|
||||||
|
// Set background color for small notification icon
|
||||||
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
// Dismiss on click
|
||||||
|
builder.setAutoCancel(true);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the notification channel for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} and
|
||||||
|
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
*/
|
||||||
|
public static void setupCrashReportsNotificationChannel(final Context context) {
|
||||||
|
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
|
||||||
|
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
307
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
package com.termux.app.utils;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.file.TermuxFileUtils;
|
||||||
|
import com.termux.shared.models.ResultConfig;
|
||||||
|
import com.termux.shared.models.ResultData;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||||
|
import com.termux.shared.shell.ResultSender;
|
||||||
|
import com.termux.shared.shell.ShellUtils;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||||
|
import com.termux.shared.settings.properties.SharedProperties;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.models.ReportInfo;
|
||||||
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
public class PluginUtils {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "PluginUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link ExecutionCommand} result.
|
||||||
|
*
|
||||||
|
* The ExecutionCommand currentState must be greater or equal to
|
||||||
|
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
||||||
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
|
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||||
|
* is not {@code null}, then the result of commands are sent back to the command caller.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} to process.
|
||||||
|
*/
|
||||||
|
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
||||||
|
if (executionCommand == null) return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
Error error = null;
|
||||||
|
ResultData resultData = executionCommand.resultData;
|
||||||
|
|
||||||
|
if (!executionCommand.hasExecuted()) {
|
||||||
|
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||||
|
|
||||||
|
// Log the output. ResultData should not be logged if pending result since ResultSender will do it
|
||||||
|
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||||
|
|
||||||
|
// If execution command was started by a plugin which expects the result back
|
||||||
|
if (isPluginExecutionCommandWithPendingResult) {
|
||||||
|
// Set variables which will be used by sendCommandResultData to send back the result
|
||||||
|
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||||
|
setPluginResultPendingIntentVariables(executionCommand);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||||
|
setPluginResultDirectoryVariables(executionCommand);
|
||||||
|
|
||||||
|
// Send result to caller
|
||||||
|
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||||
|
if (error != null) {
|
||||||
|
// error will be added to existing Errors
|
||||||
|
resultData.setStateFailed(error);
|
||||||
|
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||||
|
|
||||||
|
// Flash and send notification for the error
|
||||||
|
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||||
|
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executionCommand.isStateFailed() && error == null)
|
||||||
|
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link ExecutionCommand} error.
|
||||||
|
*
|
||||||
|
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
||||||
|
* The {@link ResultData#getErrCode()} must have been set to a value greater than
|
||||||
|
* {@link Errno#ERRNO_SUCCESS}.
|
||||||
|
* The {@link ResultData#errorsList} must also be set with appropriate error info.
|
||||||
|
*
|
||||||
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
|
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||||
|
* is not {@code null}, then the errors of commands are sent back to the command caller.
|
||||||
|
*
|
||||||
|
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||||
|
* enabled, then a flash and a notification will be shown for the error as well
|
||||||
|
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
|
||||||
|
* the error.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||||
|
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
||||||
|
* regardless of if pending intent is {@code null} or
|
||||||
|
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||||
|
* is {@code false}.
|
||||||
|
*/
|
||||||
|
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
||||||
|
if (context == null || executionCommand == null) return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
Error error;
|
||||||
|
ResultData resultData = executionCommand.resultData;
|
||||||
|
|
||||||
|
if (!executionCommand.isStateFailed()) {
|
||||||
|
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||||
|
|
||||||
|
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
|
||||||
|
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||||
|
|
||||||
|
// If execution command was started by a plugin which expects the result back
|
||||||
|
if (isPluginExecutionCommandWithPendingResult) {
|
||||||
|
// Set variables which will be used by sendCommandResultData to send back the result
|
||||||
|
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||||
|
setPluginResultPendingIntentVariables(executionCommand);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||||
|
setPluginResultDirectoryVariables(executionCommand);
|
||||||
|
|
||||||
|
// Send result to caller
|
||||||
|
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||||
|
if (error != null) {
|
||||||
|
// error will be added to existing Errors
|
||||||
|
resultData.setStateFailed(error);
|
||||||
|
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||||
|
forceNotification = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
||||||
|
if (!forceNotification) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||||
|
if (preferences == null) return;
|
||||||
|
|
||||||
|
// If user has disabled notifications for plugin commands, then just return
|
||||||
|
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Flash and send notification for the error
|
||||||
|
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||||
|
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||||
|
* to send back the result via {@link ResultConfig#resultPendingIntent}. */
|
||||||
|
public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) {
|
||||||
|
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||||
|
|
||||||
|
resultConfig.resultBundleKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE;
|
||||||
|
resultConfig.resultStdoutKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT;
|
||||||
|
resultConfig.resultStdoutOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH;
|
||||||
|
resultConfig.resultStderrKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR;
|
||||||
|
resultConfig.resultStderrOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH;
|
||||||
|
resultConfig.resultExitCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE;
|
||||||
|
resultConfig.resultErrCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR;
|
||||||
|
resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||||
|
* to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */
|
||||||
|
public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) {
|
||||||
|
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||||
|
|
||||||
|
resultConfig.resultDirectoryPath = TermuxFileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null, true);
|
||||||
|
resultConfig.resultDirectoryAllowedParentPath = TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(resultConfig.resultDirectoryPath);
|
||||||
|
|
||||||
|
// Set default resultFileBasename if resultSingleFile is true to `<executable_basename>-<timestamp>.log`
|
||||||
|
if (resultConfig.resultSingleFile && resultConfig.resultFileBasename == null)
|
||||||
|
resultConfig.resultFileBasename = ShellUtils.getExecutableBasename(executionCommand.executable) + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp() + ".log";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||||
|
* @param notificationTextString The text of the notification.
|
||||||
|
*/
|
||||||
|
public static void sendPluginCommandErrorNotification(Context context, String logTag, ExecutionCommand executionCommand, String notificationTextString) {
|
||||||
|
// Send a notification to show the error which when clicked will open the ReportActivity
|
||||||
|
// to show the details of the error
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||||
|
|
||||||
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
|
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||||
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND.getName(), logTag, title, null, reportString.toString(), null,true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupPluginCommandErrorsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Use markdown in notification
|
||||||
|
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
|
||||||
|
//CharSequence notificationTextCharSequence = notificationTextString;
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param title The title for the notification.
|
||||||
|
* @param notificationText The second line text of the notification.
|
||||||
|
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||||
|
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||||
|
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||||
|
* @return Returns the {@link Notification.Builder}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||||
|
|
||||||
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||||
|
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||||
|
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||||
|
|
||||||
|
if (builder == null) return null;
|
||||||
|
|
||||||
|
// Enable timestamp
|
||||||
|
builder.setShowWhen(true);
|
||||||
|
|
||||||
|
// Set notification icon
|
||||||
|
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||||
|
|
||||||
|
// Set background color for small notification icon
|
||||||
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
// Dismiss on click
|
||||||
|
builder.setAutoCancel(true);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the notification channel for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
|
||||||
|
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
*/
|
||||||
|
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
||||||
|
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
||||||
|
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} to get error string.
|
||||||
|
* @return Returns the {@code error} if policy is violated, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||||
|
String errmsg = null;
|
||||||
|
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
|
||||||
|
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errmsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import android.provider.DocumentsProvider;
|
|||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -22,7 +22,7 @@ import java.util.LinkedList;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A document provider for the Storage Access Framework which exposes the files in the
|
* A document provider for the Storage Access Framework which exposes the files in the
|
||||||
* $HOME/ folder to other apps.
|
* $HOME/ directory to other apps.
|
||||||
* <p/>
|
* <p/>
|
||||||
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
|
|
||||||
private static final String ALL_MIME_TYPES = "*/*";
|
private static final String ALL_MIME_TYPES = "*/*";
|
||||||
|
|
||||||
private static final File BASE_DIR = new File(TermuxService.HOME_PATH);
|
private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR;
|
||||||
|
|
||||||
|
|
||||||
// The default columns to return information about a root if no specific
|
// The default columns to return information about a root if no specific
|
||||||
@@ -63,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.TextInputDialogUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.app.TermuxService;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -21,12 +23,13 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
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
|
||||||
@@ -36,6 +39,13 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
*/
|
*/
|
||||||
boolean mFinishOnDismissNameDialog = true;
|
boolean mFinishOnDismissNameDialog = true;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxFileReceiverActivity";
|
||||||
|
|
||||||
|
static boolean isSharedTextAnUrl(String sharedText) {
|
||||||
|
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||||
|
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@@ -50,7 +60,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
|
|
||||||
if (sharedText != null) {
|
if (sharedText != null) {
|
||||||
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
|
if (isSharedTextAnUrl(sharedText)) {
|
||||||
handleUrlAndFinish(sharedText);
|
handleUrlAndFinish(sharedText);
|
||||||
} else {
|
} else {
|
||||||
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||||
@@ -103,12 +113,12 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
promptNameAndSave(in, attachmentFileName);
|
promptNameAndSave(in, attachmentFileName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
||||||
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
|
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||||
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, text -> {
|
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||||
File outFile = saveStreamWithName(in, text);
|
File outFile = saveStreamWithName(in, text);
|
||||||
if (outFile == null) return;
|
if (outFile == null) return;
|
||||||
|
|
||||||
@@ -125,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();
|
||||||
@@ -163,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,9 +192,9 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
|
|
||||||
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
||||||
|
|
||||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
|
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
|
||||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
|
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
|
||||||
startService(executeIntent);
|
startService(executeIntent);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:height="108dp"
|
|
||||||
android:width="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:pathData="M18,54
|
|
||||||
A36,36 0 1,1 90,54
|
|
||||||
A36,36 0 1,1 18,54 Z" />
|
|
||||||
|
|
||||||
<!-- Keep in sync with adaptive ic_foreground.xml: -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M34,38
|
|
||||||
h6
|
|
||||||
l12,16
|
|
||||||
l-12,16
|
|
||||||
h-6
|
|
||||||
l12,-16
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M56,66
|
|
||||||
h18
|
|
||||||
v4
|
|
||||||
h-18
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -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>
|
||||||
9
app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</LinearLayout>
|
||||||
109
app/src/main/res/layout/activity_termux.xml
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/activity_termux_root_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.drawerlayout.widget.DrawerLayout
|
||||||
|
android:id="@+id/drawer_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.termux.view.TerminalView
|
||||||
|
android:id="@+id/terminal_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="3dp"
|
||||||
|
android:layout_marginLeft="3dp"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:autofillHints="password" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/left_drawer"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:choiceMode="singleChoice"
|
||||||
|
android:divider="@android:color/transparent"
|
||||||
|
android:dividerHeight="0dp"
|
||||||
|
android:descendantFocusability="blocksDescendants"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/settings_button"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@drawable/ic_settings"
|
||||||
|
android:background="@null"
|
||||||
|
android:contentDescription="@string/action_open_settings" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/terminal_sessions_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:choiceMode="singleChoice"
|
||||||
|
android:longClickable="true" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
style="?android:attr/buttonBarStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/toggle_keyboard_button"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/action_toggle_soft_keyboard" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/new_session_button"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/action_new_session" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
|
|
||||||
|
<androidx.viewpager.widget.ViewPager
|
||||||
|
android:id="@+id/terminal_toolbar_view_pager"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="37.5dp"
|
||||||
|
android:background="@android:drawable/screen_background_dark_transparent"
|
||||||
|
android:layout_alignParentBottom="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/activity_termux_bottom_space_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="@android:color/transparent" />
|
||||||
|
|
||||||
|
</com.termux.app.terminal.TermuxActivityRootView>
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:fitsSystemWindows="true">
|
|
||||||
|
|
||||||
<androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
android:id="@+id/drawer_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_above="@+id/viewpager"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.termux.view.TerminalView
|
|
||||||
android:id="@+id/terminal_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginRight="3dp"
|
|
||||||
android:layout_marginLeft="3dp"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
|
||||||
android:scrollbars="vertical" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/left_drawer"
|
|
||||||
android:layout_width="240dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="start"
|
|
||||||
android:background="@android:color/white"
|
|
||||||
android:choiceMode="singleChoice"
|
|
||||||
android:divider="@android:color/transparent"
|
|
||||||
android:dividerHeight="0dp"
|
|
||||||
android:descendantFocusability="blocksDescendants"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/left_drawer_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_gravity="top"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:choiceMode="singleChoice"
|
|
||||||
android:longClickable="true" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
style="?android:attr/buttonBarStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/toggle_keyboard_button"
|
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/toggle_soft_keyboard" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/new_session_button"
|
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/new_session" />
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
|
||||||
android:id="@+id/viewpager"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="37.5dp"
|
|
||||||
android:background="@android:drawable/screen_background_dark_transparent"
|
|
||||||
android:layout_alignParentBottom="true" />
|
|
||||||
</RelativeLayout>
|
|
||||||
@@ -1,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" />
|
||||||
20
app/src/main/res/layout/preference_markdown_text.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-appcompat-release/preference/preference/res/layout/preference.xml
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView android:id="@android:id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:textColor="?android:attr/textColorPrimary" />
|
||||||
|
|
||||||
|
<include android:id="@android:id/summary" layout="@layout/markdown_adapter_node_default" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.termux.app.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/extra_keys"
|
android:id="@+id/terminal_toolbar_extra_keys"
|
||||||
style="?android:attr/buttonBarStyle"
|
style="?android:attr/buttonBarStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/text_input"
|
android:id="@+id/terminal_toolbar_text_input"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:imeOptions="actionSend|flagNoFullscreen"
|
android:imeOptions="actionSend|flagNoFullscreen"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
|
android:importantForAutofill="no"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textColorHighlight="@android:color/darker_gray"
|
android:textColorHighlight="@android:color/darker_gray"
|
||||||
android:paddingTop="0dp"
|
android:paddingTop="0dp"
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@android:color/black"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
3
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
@@ -1,51 +1,182 @@
|
|||||||
<?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="msg_generating_report">Generating Report</string>
|
||||||
|
|
||||||
|
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
|
||||||
|
<string name="action_styling_install">Install</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Notifications -->
|
||||||
<string name="notification_action_exit">Exit</string>
|
<string name="notification_action_exit">Exit</string>
|
||||||
<string name="notification_action_wake_lock">Acquire wakelock</string>
|
<string name="notification_action_wake_lock">Acquire wakelock</string>
|
||||||
<string name="notification_action_wake_unlock">Release wakelock</string>
|
<string name="notification_action_wake_unlock">Release wakelock</string>
|
||||||
|
|
||||||
<string name="file_received_title">Save file in ~/downloads/</string>
|
|
||||||
<string name="file_received_edit_button">Edit</string>
|
|
||||||
<string name="file_received_open_folder_button">Open folder</string>
|
<!-- TermuxService -->
|
||||||
|
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux RunCommandService -->
|
||||||
|
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||||
|
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
||||||
|
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
|
||||||
|
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Execution Commands -->
|
||||||
|
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
|
||||||
|
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux File Receiver -->
|
||||||
|
<string name="title_file_received">Save file in ~/downloads/</string>
|
||||||
|
<string name="action_file_received_edit">Edit</string>
|
||||||
|
<string name="action_file_received_open_directory">Open directory</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Settings -->
|
||||||
|
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
|
||||||
|
|
||||||
|
<!-- Termux App Preferences -->
|
||||||
|
<string name="termux_preferences_title">&TERMUX_APP_NAME;</string>
|
||||||
|
<string name="termux_preferences_summary">Preferences for &TERMUX_APP_NAME; app</string>
|
||||||
|
|
||||||
|
<!-- Debugging Preferences -->
|
||||||
|
<string name="termux_debugging_preferences_title">Debugging</string>
|
||||||
|
<string name="termux_debugging_preferences_summary">Preferences for debugging</string>
|
||||||
|
|
||||||
|
<!-- Logging Category -->
|
||||||
|
<string name="termux_logging_header">Logging</string>
|
||||||
|
|
||||||
|
<!-- Log Level -->
|
||||||
|
<string name="termux_log_level_title">Log Level</string>
|
||||||
|
|
||||||
|
<!-- Terminal View Key Logging -->
|
||||||
|
<string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string>
|
||||||
|
<string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string>
|
||||||
|
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
|
||||||
|
|
||||||
|
<!-- Plugin Error Notifications -->
|
||||||
|
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
|
||||||
|
<string name="termux_plugin_error_notifications_enabled_off">Disable flashes and notifications for plugin errors.</string>
|
||||||
|
<string name="termux_plugin_error_notifications_enabled_on">Show flashes and notifications for plugin errors. (Default)</string>
|
||||||
|
|
||||||
|
<!-- Crash Report Notifications -->
|
||||||
|
<string name="termux_crash_report_notifications_enabled_title">Crash Report Notifications</string>
|
||||||
|
<string name="termux_crash_report_notifications_enabled_off">Disable notifications for crash reports.</string>
|
||||||
|
<string name="termux_crash_report_notifications_enabled_on">Show notifications for crash reports. (Default)</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Terminal IO Preferences -->
|
||||||
|
<string name="termux_terminal_io_preferences_title">Terminal I/O</string>
|
||||||
|
<string name="termux_terminal_io_preferences_summary">Preferences for terminal I/O</string>
|
||||||
|
|
||||||
|
<!-- Keyboard Category -->
|
||||||
|
<string name="termux_keyboard_header">Keyboard</string>
|
||||||
|
|
||||||
|
<!-- Soft Keyboard -->
|
||||||
|
<string name="termux_soft_keyboard_enabled_title">Soft Keyboard Enabled</string>
|
||||||
|
<string name="termux_soft_keyboard_enabled_off">Soft keyboard will be disabled.</string>
|
||||||
|
<string name="termux_soft_keyboard_enabled_on">Soft keyboard will be enabled. (Default)</string>
|
||||||
|
|
||||||
|
<!-- Soft Keyboard Only If No Hardware-->
|
||||||
|
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string>
|
||||||
|
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if hardware keyboard is connected. (Default)</string>
|
||||||
|
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if no hardware keyboard is connected.</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Tasker App Preferences -->
|
||||||
|
<string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string>
|
||||||
|
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- About Preference -->
|
||||||
|
<string name="about_preference_title">About</string>
|
||||||
|
|
||||||
|
<!-- Donate Preference -->
|
||||||
|
<string name="donate_preference_title">Donate</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -21,10 +21,6 @@
|
|||||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
|
||||||
<!-- Seen in buttons on alert dialog: -->
|
|
||||||
<item name="android:colorAccent">#212121</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
||||||
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||||
@@ -46,4 +42,11 @@
|
|||||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||||
|
<!-- Seen in buttons on alert dialog: -->
|
||||||
|
<item name="android:colorAccent">#212121</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
28
app/src/main/res/xml/root_preferences.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:key="termux"
|
||||||
|
app:title="@string/termux_preferences_title"
|
||||||
|
app:summary="@string/termux_preferences_summary"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:key="termux_tasker"
|
||||||
|
app:title="@string/termux_tasker_preferences_title"
|
||||||
|
app:summary="@string/termux_tasker_preferences_summary"
|
||||||
|
app:isPreferenceVisible="false"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:key="about"
|
||||||
|
app:title="@string/about_preference_title"
|
||||||
|
app:persistent="false"/>
|
||||||
|
<!-- app:layout="@layout/preference_markdown_text" -->
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:key="donate"
|
||||||
|
app:title="@string/donate_preference_title"
|
||||||
|
app:persistent="false"
|
||||||
|
app:isPreferenceVisible="false"/>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
@@ -1,30 +1,54 @@
|
|||||||
<shortcuts xmlns:tools="http://schemas.android.com/tools"
|
<shortcuts xmlns:tools="http://schemas.android.com/tools"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
For shortcut.xml:
|
||||||
|
If applicationId in build.gradle is changed from "com.termux", then targetPackage will
|
||||||
|
need to be manually patched since ${applicationId} variable or resource string does not work.
|
||||||
|
If package name in AndroidManifest is changed from "com.termux", then targetClass will
|
||||||
|
need to be manually patched since dot (.) prefix does not work to automatically prefix the
|
||||||
|
package name.
|
||||||
|
-->
|
||||||
|
|
||||||
<shortcut
|
<shortcut
|
||||||
android:shortcutId="new_session"
|
android:shortcutId="new_session"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_new_session"
|
android:icon="@drawable/ic_new_session"
|
||||||
android:shortcutShortLabel="@string/new_session"
|
android:shortcutShortLabel="@string/action_new_session"
|
||||||
tools:targetApi="n_mr1">
|
tools:targetApi="n_mr1">
|
||||||
<intent
|
<intent
|
||||||
android:action="android.intent.action.RUN"
|
android:action="android.intent.action.RUN"
|
||||||
android:targetPackage="com.termux"
|
android:targetPackage="com.termux"
|
||||||
android:targetClass="com.termux.app.TermuxActivity"/>
|
android:targetClass="com.termux.app.TermuxActivity"
|
||||||
|
android:name="android.shortcut.conversation"/>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
|
||||||
<shortcut
|
<shortcut
|
||||||
android:shortcutId="new_failsafe_session"
|
android:shortcutId="new_failsafe_session"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_new_session"
|
android:icon="@drawable/ic_new_session"
|
||||||
android:shortcutShortLabel="@string/new_session_failsafe"
|
android:shortcutShortLabel="@string/action_new_session_failsafe"
|
||||||
tools:targetApi="n_mr1">
|
tools:targetApi="n_mr1">
|
||||||
<intent
|
<intent
|
||||||
android:action="android.intent.action.RUN"
|
android:action="android.intent.action.RUN"
|
||||||
android:targetPackage="com.termux"
|
android:targetPackage="com.termux"
|
||||||
android:targetClass="com.termux.app.TermuxActivity">
|
android:targetClass="com.termux.app.TermuxActivity"
|
||||||
<extra android:name="com.termux.app.failsafe_session" android:value="true" />
|
android:name="android.shortcut.conversation">
|
||||||
|
<extra android:name="com.termux.app.failsafe_session" android:value="true"/>
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
|
||||||
|
<shortcut
|
||||||
|
android:shortcutId="settings"
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_settings"
|
||||||
|
android:shortcutShortLabel="@string/action_open_settings"
|
||||||
|
tools:targetApi="n_mr1">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetPackage="com.termux"
|
||||||
|
android:targetClass="com.termux.app.activities.SettingsActivity"
|
||||||
|
android:name="android.shortcut.conversation"/>
|
||||||
|
</shortcut>
|
||||||
|
|
||||||
</shortcuts>
|
</shortcuts>
|
||||||
|
|||||||
33
app/src/main/res/xml/termux_debugging_preferences.xml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:key="logging"
|
||||||
|
app:title="@string/termux_logging_header">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
app:defaultValue="1"
|
||||||
|
app:key="log_level"
|
||||||
|
app:title="@string/termux_log_level_title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="terminal_view_key_logging_enabled"
|
||||||
|
app:summaryOff="@string/termux_terminal_view_key_logging_enabled_off"
|
||||||
|
app:summaryOn="@string/termux_terminal_view_key_logging_enabled_on"
|
||||||
|
app:title="@string/termux_terminal_view_key_logging_enabled_title" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="plugin_error_notifications_enabled"
|
||||||
|
app:summaryOff="@string/termux_plugin_error_notifications_enabled_off"
|
||||||
|
app:summaryOn="@string/termux_plugin_error_notifications_enabled_on"
|
||||||
|
app:title="@string/termux_plugin_error_notifications_enabled_title" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="crash_report_notifications_enabled"
|
||||||
|
app:summaryOff="@string/termux_crash_report_notifications_enabled_off"
|
||||||
|
app:summaryOn="@string/termux_crash_report_notifications_enabled_on"
|
||||||
|
app:title="@string/termux_crash_report_notifications_enabled_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
13
app/src/main/res/xml/termux_preferences.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:title="@string/termux_debugging_preferences_title"
|
||||||
|
app:summary="@string/termux_debugging_preferences_summary"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:title="@string/termux_terminal_io_preferences_title"
|
||||||
|
app:summary="@string/termux_terminal_io_preferences_summary"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.termux.TerminalIOPreferencesFragment"/>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
15
app/src/main/res/xml/termux_tasker_debugging_preferences.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:key="logging"
|
||||||
|
app:title="@string/termux_logging_header">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
app:defaultValue="1"
|
||||||
|
app:key="log_level"
|
||||||
|
app:title="@string/termux_log_level_title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
8
app/src/main/res/xml/termux_tasker_preferences.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:title="@string/termux_debugging_preferences_title"
|
||||||
|
app:summary="@string/termux_debugging_preferences_summary"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.termux_tasker.DebuggingPreferencesFragment"/>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
21
app/src/main/res/xml/termux_terminal_io_preferences.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:key="keyboard"
|
||||||
|
app:title="@string/termux_keyboard_header">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="soft_keyboard_enabled"
|
||||||
|
app:summaryOff="@string/termux_soft_keyboard_enabled_off"
|
||||||
|
app:summaryOn="@string/termux_soft_keyboard_enabled_on"
|
||||||
|
app:title="@string/termux_soft_keyboard_enabled_title" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="soft_keyboard_enabled_only_if_no_hardware"
|
||||||
|
app:summaryOff="@string/termux_soft_keyboard_enabled_only_if_no_hardware_off"
|
||||||
|
app:summaryOn="@string/termux_soft_keyboard_enabled_only_if_no_hardware_on"
|
||||||
|
app:title="@string/termux_soft_keyboard_enabled_only_if_no_hardware_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
@@ -1,25 +1,32 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
import com.termux.shared.data.UrlUtils;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
|
||||||
public class TermuxActivityTest extends TestCase {
|
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);
|
||||||
assertEquals(expected, TermuxActivity.extractUrls(text));
|
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testExtractUrls() {
|
@Test
|
||||||
assertUrlsAre("hello http://example.com world", "http://example.com");
|
public void testExtractUrls() {
|
||||||
|
assertUrlsAre("hello http://example.com world", "http://example.com");
|
||||||
|
|
||||||
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
|
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
|
||||||
|
|
||||||
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");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.termux.filepicker;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public class TermuxFileReceiverActivityTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsSharedTextAnUrl() {
|
||||||
|
List<String> validUrls = new ArrayList<>();
|
||||||
|
validUrls.add("http://example.com");
|
||||||
|
validUrls.add("https://example.com");
|
||||||
|
validUrls.add("https://example.com/path/parameter=foo");
|
||||||
|
validUrls.add("magnet:?xt=urn:btih:d540fc48eb12f2833163eed6421d449dd8f1ce1f&dn=Ubuntu+desktop+19.04+%2864bit%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.ccc.de%3A80");
|
||||||
|
for (String url : validUrls) {
|
||||||
|
Assert.assertTrue(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> invalidUrls = new ArrayList<>();
|
||||||
|
invalidUrls.add("a test with example.com");
|
||||||
|
for (String url : invalidUrls) {
|
||||||
|
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
BIN
art/ic_launcher2.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
art/ic_launcher2_round.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -1,17 +1,18 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Termux is a terminal emulator application enhanced with a large set of command line utilities ported to Android OS. The main goal is to bring a Linux command line experience to users of mobile devices with no rooting or other special setup required.
|
||||||
|
|
||||||
|
* Enjoy the Bash and Zsh shells.
|
||||||
|
* Edit files with nano and vim.
|
||||||
|
* Access servers over SSH.
|
||||||
|
* Compile C/C++ code with clang.
|
||||||
|
* Use the Python console as a pocket calculator.
|
||||||
|
* Check out projects with Git and Subversion.
|
||||||
|
* Run text-based games with frotz.
|
||||||
|
|
||||||
|
At first start a small base system is being configured. The GNU Bash, Coreutils, Findutils and other core utilities are available out-of-box. Additionally, we provide more than 1000 other packages installable by using the 'pkg' utility which currently is a frontend for the 'apt' package manager. All provided software has been patched and compiled with Android NDK to provide max compatibility with Android OS.
|
||||||
|
|
||||||
|
To learn more about application usage tips and tricks, long-press anywhere on the terminal and select the Help menu option to access Termux Wiki. This resource is also accessible directly in a web browser: https://wiki.termux.com/wiki/Main_Page.
|
||||||
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Terminal emulator app with a large set of command line utilities
|
||||||
@@ -13,3 +13,12 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
minSdkVersion=24
|
||||||
|
targetSdkVersion=28
|
||||||
|
ndkVersion=22.1.7171670
|
||||||
|
compileSdkVersion=30
|
||||||
|
|
||||||
|
markwonVersion=4.6.2
|
||||||
|
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
|||||||
#Sun Aug 25 01:57:11 CEST 2019
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
|
||||||
|
|||||||
53
gradlew
vendored
@@ -1,5 +1,21 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
##
|
||||||
## Gradle start up script for UN*X
|
## Gradle start up script for UN*X
|
||||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
|||||||
APP_BASE_NAME=`basename "$0"`
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD="maximum"
|
||||||
@@ -66,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
|
||||||
@@ -109,10 +126,11 @@ if $darwin; then
|
|||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if $cygwin ; 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
|
||||||
@@ -138,19 +156,19 @@ if $cygwin ; then
|
|||||||
else
|
else
|
||||||
eval `echo args$i`="\"$arg\""
|
eval `echo args$i`="\"$arg\""
|
||||||
fi
|
fi
|
||||||
i=$((i+1))
|
i=`expr $i + 1`
|
||||||
done
|
done
|
||||||
case $i in
|
case $i in
|
||||||
(0) set -- ;;
|
0) set -- ;;
|
||||||
(1) set -- "$args0" ;;
|
1) set -- "$args0" ;;
|
||||||
(2) set -- "$args0" "$args1" ;;
|
2) set -- "$args0" "$args1" ;;
|
||||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -159,14 +177,9 @@ save () {
|
|||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
echo " "
|
echo " "
|
||||||
}
|
}
|
||||||
APP_ARGS=$(save "$@")
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
|
||||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|||||||
173
gradlew.bat
vendored
@@ -1,84 +1,89 @@
|
|||||||
@if "%DEBUG%" == "" @echo off
|
@rem
|
||||||
@rem ##########################################################################
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@rem
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem ##########################################################################
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
set DIRNAME=%~dp0
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
set APP_BASE_NAME=%~n0
|
@rem See the License for the specific language governing permissions and
|
||||||
set APP_HOME=%DIRNAME%
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
@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"
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
@rem Find java.exe
|
@rem
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
set JAVA_EXE=java.exe
|
@rem ##########################################################################
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
set DIRNAME=%~dp0
|
||||||
echo.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
set APP_BASE_NAME=%~n0
|
||||||
echo location of your Java installation.
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
goto fail
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
set JAVA_EXE=java.exe
|
||||||
echo.
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
echo location of your Java installation.
|
|
||||||
|
echo.
|
||||||
goto fail
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
:init
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
@rem Get command-line arguments, handling Windows variants
|
echo location of your Java installation.
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
goto fail
|
||||||
|
|
||||||
:win9xME_args
|
:findJavaFromJavaHome
|
||||||
@rem Slurp the command line arguments.
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set CMD_LINE_ARGS=
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
set _SKIP=2
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
:win9xME_args_slurp
|
|
||||||
if "x%~1" == "x" goto execute
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
set CMD_LINE_ARGS=%*
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
:execute
|
echo location of your Java installation.
|
||||||
@rem Setup the command line
|
|
||||||
|
goto fail
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
:execute
|
||||||
@rem Execute Gradle
|
@rem Setup the command line
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
:end
|
||||||
rem the _cmd.exe /c_ return code!
|
@rem End local scope for the variables with windows NT shell
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
exit /b 1
|
|
||||||
|
:fail
|
||||||
:mainEnd
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
:omega
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
|
|||||||
2
jitpack.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
env:
|
||||||
|
JITPACK_NDK_VERSION: "21.1.6352462"
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
|
|
||||||
group = publishedGroupId // Maven Group ID for the artifact
|
|
||||||
install {
|
|
||||||
repositories.mavenInstaller {
|
|
||||||
pom {
|
|
||||||
project {
|
|
||||||
packaging 'aar'
|
|
||||||
groupId publishedGroupId
|
|
||||||
artifactId artifact
|
|
||||||
|
|
||||||
name libraryName
|
|
||||||
description libraryDescription
|
|
||||||
url siteUrl
|
|
||||||
|
|
||||||
licenses {
|
|
||||||
license {
|
|
||||||
name 'GNU General Public License version 3'
|
|
||||||
url 'https://opensource.org/licenses/gpl-3.0.html'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
developers {
|
|
||||||
developer {
|
|
||||||
id 'fornwall'
|
|
||||||
name 'Fredrik Fornwall'
|
|
||||||
email 'fredrik@fornwall.net'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scm {
|
|
||||||
connection gitUrl
|
|
||||||
developerConnection gitUrl
|
|
||||||
url siteUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// End https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
|
|
||||||
|
|
||||||
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle
|
|
||||||
apply plugin: 'com.jfrog.bintray'
|
|
||||||
|
|
||||||
version = libraryVersion
|
|
||||||
|
|
||||||
task sourcesJar(type: Jar) {
|
|
||||||
classifier = 'sources'
|
|
||||||
from android.sourceSets.main.java.srcDirs
|
|
||||||
}
|
|
||||||
|
|
||||||
artifacts {
|
|
||||||
archives sourcesJar
|
|
||||||
}
|
|
||||||
|
|
||||||
bintray {
|
|
||||||
user = System.getenv('BINTRAY_USER')
|
|
||||||
key = System.getenv('BINTRAY_API_KEY')
|
|
||||||
|
|
||||||
configurations = ['archives']
|
|
||||||
pkg {
|
|
||||||
repo = 'maven'
|
|
||||||
name = bintrayName
|
|
||||||
userOrg = 'termux'
|
|
||||||
desc = libraryDescription
|
|
||||||
websiteUrl = siteUrl
|
|
||||||
vcsUrl = gitUrl
|
|
||||||
licenses = ['GPL-3.0']
|
|
||||||
publish = true
|
|
||||||
publicDownloadNumbers = true
|
|
||||||
version {
|
|
||||||
desc = libraryDescription
|
|
||||||
gpg {
|
|
||||||
sign = false //Determines whether to GPG sign the files. The default is false
|
|
||||||
// passphrase = properties.getProperty("bintray.gpg.password")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||