Compare commits
486 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3b969a0077 | ||
|
|
ae717d8f5f | ||
|
|
6c55c3c1be | ||
|
|
c0a0822843 | ||
|
|
08b08d1c47 | ||
|
|
c76cec186a | ||
|
|
5ba3f7cf6d | ||
|
|
468f878a38 | ||
|
|
c50a367063 | ||
|
|
4189f598b9 | ||
|
|
fdb3764f5c | ||
|
|
cb67e805de | ||
|
|
3f83368f42 | ||
|
|
8c7462678f | ||
|
|
6f11390cc4 | ||
|
|
33d390a228 | ||
|
|
937eb350b2 | ||
|
|
3b4ece6bd8 | ||
|
|
35a4fdacbe | ||
|
|
0332779d6a | ||
|
|
f19575b942 | ||
|
|
6a95809da3 | ||
|
|
ac8dab70ef | ||
|
|
243285f7c2 | ||
|
|
b6182216d5 | ||
|
|
e206121bbc | ||
|
|
deceffad00 | ||
|
|
c19909cef1 | ||
|
|
5b7e40638c | ||
|
|
a3673d1af5 | ||
|
|
d5f9cf85c9 | ||
|
|
549f09573f | ||
|
|
94e5bc86fb | ||
|
|
370ac2bd89 | ||
|
|
7041f41981 | ||
|
|
dd6a21257b | ||
|
|
445da0c4ea | ||
|
|
92980824b1 | ||
|
|
7d42b07a32 | ||
|
|
e502b9169f | ||
|
|
9f2d887ca0 | ||
|
|
e6aacc5f08 | ||
|
|
ff935be54a | ||
|
|
4e76162346 | ||
|
|
5fa4f2647b | ||
|
|
81b5889a26 | ||
|
|
514f59258a | ||
|
|
9f79393aa5 | ||
|
|
e49d514236 | ||
|
|
4e1462326c | ||
|
|
b0f1773a92 | ||
|
|
af9f28c010 | ||
|
|
fef0c66868 | ||
|
|
7a5da83ce2 | ||
|
|
97f01387b9 | ||
|
|
012ff83a06 | ||
|
|
a082c63849 | ||
|
|
8c27084086 | ||
|
|
f4fb45cbd6 | ||
|
|
0605fc4843 | ||
|
|
3bbd61f9d7 | ||
|
|
42fc0ea1cd | ||
|
|
6218d08037 | ||
|
|
10fe238ddb | ||
|
|
fe41cd486f | ||
|
|
bda80547ad | ||
|
|
70a786613d | ||
|
|
e4220a7ab1 | ||
|
|
bd45837d93 | ||
|
|
ddf5341b86 | ||
|
|
85a48a79a3 | ||
|
|
e3512c957d | ||
|
|
330301899a | ||
|
|
2a36b915cb | ||
|
|
c986a46fb9 | ||
|
|
ed8bd4b569 | ||
|
|
7f4df8328c | ||
|
|
ad1ce585d9 | ||
|
|
158908a3bf | ||
|
|
d8f066dec4 | ||
|
|
743225ab6a | ||
|
|
b38ae24908 | ||
|
|
70c1bddae0 | ||
|
|
4df285013e | ||
|
|
1e4fa8c045 | ||
|
|
31f2dc35bc | ||
|
|
fbb048ce56 | ||
|
|
8c3a30027e | ||
|
|
a8c270f3c1 | ||
|
|
829414ac19 | ||
|
|
8e0b8623bc | ||
|
|
e2bda2bb52 | ||
|
|
8c7b1a60d2 | ||
|
|
cb0710bb83 | ||
|
|
b54c8276b4 | ||
|
|
7056945f5d | ||
|
|
063ef02f2b | ||
|
|
6e6e4fd212 | ||
|
|
406438e391 | ||
|
|
d1977479bd | ||
|
|
b4563023f6 | ||
|
|
095ed8b54f | ||
|
|
af445f9618 | ||
|
|
82f977fbf1 | ||
|
|
4b52cfbb93 | ||
|
|
b61da23be7 | ||
|
|
35df3f72c9 | ||
|
|
d584502fef | ||
|
|
a691333c5e | ||
|
|
e053cf82ca | ||
|
|
076176a5f6 | ||
|
|
08c72f9a65 | ||
|
|
7593ddde38 | ||
|
|
6dce32aece | ||
|
|
5ff801c914 | ||
|
|
221046732b | ||
|
|
07d6c9bd5e | ||
|
|
a9eddce672 | ||
|
|
26c95f1397 | ||
|
|
4eca639212 | ||
|
|
beb8a004e6 | ||
|
|
3693e3c1b6 | ||
|
|
99e8ffcf90 | ||
|
|
0807600a2d | ||
|
|
f74293e8fb | ||
|
|
b99d092305 | ||
|
|
af7515247b | ||
|
|
ec77be00dc | ||
|
|
a854960476 | ||
|
|
9db8948f23 | ||
|
|
c24167f6a5 | ||
|
|
49c051c8b7 | ||
|
|
55efdb2f56 | ||
|
|
d03e420e75 | ||
|
|
06968a9295 | ||
|
|
244248b1a0 | ||
|
|
7187ed6950 | ||
|
|
b3eabd9bad | ||
|
|
b51dd4f558 | ||
|
|
f88b9c4629 | ||
|
|
f0eeb4781b | ||
|
|
57a3a9b111 | ||
|
|
8df73a3006 | ||
|
|
963663e0cd | ||
|
|
68a83ccf37 | ||
|
|
db13ea02b6 | ||
|
|
61a44dbfa8 | ||
|
|
aaa92279ca | ||
|
|
365f9723cc | ||
|
|
fdae272214 | ||
|
|
b7864d6ac2 | ||
|
|
d1f0c76db3 | ||
|
|
5652624fc2 | ||
|
|
35a9101f84 | ||
|
|
2b6a10712b | ||
|
|
c80e126b8f | ||
|
|
89048274dd | ||
|
|
d3b4d35b2a | ||
|
|
4080f41cc7 | ||
|
|
b4e2c99d46 | ||
|
|
c5923201a4 | ||
|
|
bafd21bb39 | ||
|
|
0f20fab02c | ||
|
|
c5ae5bb06a | ||
|
|
bbd46a763c | ||
|
|
dc145d65f8 | ||
|
|
ce2d1c0d88 | ||
|
|
f2f7f963e6 | ||
|
|
2e53ef038e | ||
|
|
8c82f43dce | ||
|
|
3a16f461e7 | ||
|
|
594a5dfe25 | ||
|
|
d5e007dbb3 | ||
|
|
f4335d3824 | ||
|
|
90b6f93697 | ||
|
|
17c4a45212 | ||
|
|
6d9ffb6922 | ||
|
|
8472fce8ba | ||
|
|
69d954a583 | ||
|
|
ec1087d56f | ||
|
|
be6a73d862 | ||
|
|
80c81b274d | ||
|
|
cd9dbac548 | ||
|
|
0c837796f0 | ||
|
|
3417e37e8d | ||
|
|
31ba36e0fa | ||
|
|
c444f1fd28 | ||
|
|
9f36ed06b8 | ||
|
|
c2ab5bcd50 | ||
|
|
48fab33b79 | ||
|
|
8d7a67645b | ||
|
|
186b49d429 | ||
|
|
40a2775f52 | ||
|
|
f802d6001d | ||
|
|
50f66a12da | ||
|
|
d8e6fd21d1 | ||
|
|
0964d83572 | ||
|
|
cee0466dd7 | ||
|
|
de4f334e24 | ||
|
|
ab205ae05b | ||
|
|
b29b24f507 | ||
|
|
b3472e9e62 | ||
|
|
ab59e08959 | ||
|
|
8b566485e8 | ||
|
|
231c02a0c7 | ||
|
|
65f30e77ba | ||
|
|
686677ae45 | ||
|
|
85037a75a6 | ||
|
|
6025afc2c0 | ||
|
|
a4e4f76775 | ||
|
|
02af113dda | ||
|
|
fc4ef838bf | ||
|
|
86bd3ca21b | ||
|
|
367398bafb | ||
|
|
dd502e55f8 | ||
|
|
798125ef7a | ||
|
|
5126f06e06 | ||
|
|
0bdbf314ef | ||
|
|
2deb9899bd | ||
|
|
694ccc38c4 | ||
|
|
c949940374 | ||
|
|
d09f70de1e | ||
|
|
ac338ce2c5 | ||
|
|
092a83a688 | ||
|
|
3533b13de8 | ||
|
|
d68a0f05be | ||
|
|
9e5293a08e | ||
|
|
3f04a0a0d5 | ||
|
|
e558f824c5 | ||
|
|
8ff72ddd8b | ||
|
|
3e05356a69 | ||
|
|
82673d3f82 | ||
|
|
f2757fdb76 | ||
|
|
81fb669d0e | ||
|
|
4a45b1b617 | ||
|
|
62b08b3cf1 | ||
|
|
1e83ea3151 | ||
|
|
9c42fdb3d6 | ||
|
|
42e3163e92 | ||
|
|
68912139f6 | ||
|
|
edc92bbcbb | ||
|
|
d7520642de | ||
|
|
adae111d5c | ||
|
|
dc9272790b | ||
|
|
05ea2ea238 | ||
|
|
ad293562bc | ||
|
|
f9a565d1e0 | ||
|
|
5b3909cb73 | ||
|
|
7913e765d5 | ||
|
|
ed3a3269d8 | ||
|
|
dc3994d2cf | ||
|
|
c972377bd1 | ||
|
|
d4be782c03 | ||
|
|
c29909726c | ||
|
|
45bac89298 | ||
|
|
c06770c353 | ||
|
|
52a627efc8 | ||
|
|
2e9383720c | ||
|
|
58f9f1be71 | ||
|
|
058441dda6 | ||
|
|
888802a519 | ||
|
|
1a09b6d2a6 | ||
|
|
9d3f7734a1 | ||
|
|
61f766b59f | ||
|
|
3f08376881 | ||
|
|
cf06e70429 | ||
|
|
0714e435cb | ||
|
|
c26315185f | ||
|
|
1be5139253 | ||
|
|
adc43c40c5 | ||
|
|
779b1ca1f8 | ||
|
|
e375f99b0c | ||
|
|
0ac55cd77a | ||
|
|
4aefa5049b | ||
|
|
5b14124258 | ||
|
|
1a9c38374c | ||
|
|
eefd504f08 | ||
|
|
19f838e3d1 | ||
|
|
63adb2b132 | ||
|
|
7d3a988d2f | ||
|
|
5cd705d6d1 | ||
|
|
af8b269b51 | ||
|
|
8c220eaaea | ||
|
|
0142e1ed0e | ||
|
|
ac0a349ed9 | ||
|
|
548b0916b6 | ||
|
|
009de5a3ee | ||
|
|
41d0d60017 | ||
|
|
12ac0fa73c | ||
|
|
835dfc0276 | ||
|
|
f60316835f | ||
|
|
f74a091be6 | ||
|
|
dd6cb5221d | ||
|
|
cab6df5c0e | ||
|
|
f6f0809558 | ||
|
|
11ed7e45d8 | ||
|
|
ed1874db05 | ||
|
|
cb60803a80 | ||
|
|
fc92a27cb2 | ||
|
|
29e62e608f | ||
|
|
8a7f93d722 | ||
|
|
420683fe65 | ||
|
|
a3256ed551 | ||
|
|
57add98e3c | ||
|
|
6e5c04e04f | ||
|
|
528a05ef61 | ||
|
|
cb2f892dc5 | ||
|
|
8b6e8d7fdd | ||
|
|
d0eeaa9fc3 | ||
|
|
b793913481 | ||
|
|
d48b438c40 | ||
|
|
11afe895e1 | ||
|
|
bc96f71a2d | ||
|
|
87dfded5e6 | ||
|
|
7c0ae4cb54 | ||
|
|
b917acbbfa | ||
|
|
7d9d6fb797 | ||
|
|
74040dd37f | ||
|
|
e94f06d0f7 | ||
|
|
af21b6dc3e | ||
|
|
e0e8257f1c | ||
|
|
743b067cae | ||
|
|
23333c074a | ||
|
|
f11644fa51 | ||
|
|
212be59fca | ||
|
|
e3a1f8224f | ||
|
|
4f40d5a26a | ||
|
|
df92896eef | ||
|
|
4c93cb42f1 | ||
|
|
34afb9de43 | ||
|
|
b6ea29d260 | ||
|
|
289d58a2f0 | ||
|
|
0501ce924b | ||
|
|
d12256f5e5 | ||
|
|
fcbc036f92 | ||
|
|
70d5839334 | ||
|
|
9c19540759 | ||
|
|
9fe0e49473 | ||
|
|
357b17e972 | ||
|
|
6334470f81 | ||
|
|
b8cdd59c68 | ||
|
|
6cf36fffd7 | ||
|
|
f10ecd4db5 | ||
|
|
26457e8443 | ||
|
|
6702846c7c | ||
|
|
0c6180bbb1 | ||
|
|
fcf07f6a19 | ||
|
|
e272e3b3b2 | ||
|
|
cdf0e72145 | ||
|
|
70245eb78c | ||
|
|
ee7631dfac | ||
|
|
5ecf5d12d1 | ||
|
|
0c8cd90f4e | ||
|
|
e1ea68913f | ||
|
|
dde854eba7 | ||
|
|
07a4607c04 | ||
|
|
883be37b98 | ||
|
|
d939d3d927 | ||
|
|
a0fa51eb92 | ||
|
|
755513bb33 | ||
|
|
8ad7a6669c | ||
|
|
60f7aada9e | ||
|
|
6aa0492434 | ||
|
|
019aa44837 | ||
|
|
8d3d5e147f | ||
|
|
44197b90e2 | ||
|
|
794c7ee333 | ||
|
|
0457ddbc69 | ||
|
|
283792af5e | ||
|
|
be7cfa603a | ||
|
|
3480bf7346 | ||
|
|
8314a2756c | ||
|
|
4de82d9fe0 | ||
|
|
d658e16801 | ||
|
|
26dcd5af88 | ||
|
|
8056013082 | ||
|
|
8e90545c4b | ||
|
|
426ddbacbd | ||
|
|
7e1f8a551f | ||
|
|
e169af0447 | ||
|
|
a2cb3fafee | ||
|
|
166710f14a | ||
|
|
c1a9b7726f | ||
|
|
afb339e9d8 | ||
|
|
64c23f498f | ||
|
|
1dc92b2a12 | ||
|
|
990a957383 | ||
|
|
7bb64d724c | ||
|
|
4609dd71c6 | ||
|
|
8d00f22d4c | ||
|
|
5532421ab2 | ||
|
|
d2b27978e2 | ||
|
|
30b05e9ab2 | ||
|
|
c350318c77 | ||
|
|
c9b49cef58 | ||
|
|
f9c642c672 | ||
|
|
c0a5e5f57a | ||
|
|
dfdc9b37e1 | ||
|
|
dfb22e6050 | ||
|
|
b95d84fe13 |
@@ -14,3 +14,7 @@ insert_final_newline = true
|
|||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.y{a,}ml]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|||||||
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
* text=auto
|
||||||
|
*.bat eol=crlf
|
||||||
|
*.sh eol=lf
|
||||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve Termux application
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
IMPORTANT:
|
||||||
|
|
||||||
|
1. Support of Android 5.x - 6.x is finished.
|
||||||
|
2. Fill the template AFTER comments.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Problem 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 the behavior. Please post all necessary
|
||||||
|
commands that are needed to reproduce the issue.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
<!--
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Additional information**
|
||||||
|
|
||||||
|
* Termux application version:
|
||||||
|
* Android OS version:
|
||||||
|
* Device model:
|
||||||
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
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**
|
||||||
|
<!--
|
||||||
|
Describe the feature and why you want it.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Reference implementation**
|
||||||
|
|
||||||
|
Does another app/terminal emulator have this feature?
|
||||||
|
Provide links to more background information.
|
||||||
24
.github/workflows/debug_build.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
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@v1
|
||||||
|
with:
|
||||||
|
name: termux-app
|
||||||
|
path: ./app/build/outputs/apk/debug/app-debug.apk
|
||||||
17
.github/workflows/gradle-wrapper-validation.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: "Validate Gradle Wrapper"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validation:
|
||||||
|
name: "Validation"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
19
.github/workflows/run_tests.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Unit tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Execute tests
|
||||||
|
run: |
|
||||||
|
./gradlew test
|
||||||
19
.gitignore
vendored
@@ -6,6 +6,9 @@
|
|||||||
build/
|
build/
|
||||||
*.apk
|
*.apk
|
||||||
*.so
|
*.so
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
*.zip
|
||||||
|
|
||||||
# Crashlytics configuations
|
# Crashlytics configuations
|
||||||
com_crashlytics_export_strings.xml
|
com_crashlytics_export_strings.xml
|
||||||
@@ -19,19 +22,8 @@ local.properties
|
|||||||
# Signing files
|
# Signing files
|
||||||
.signing/
|
.signing/
|
||||||
|
|
||||||
# User-specific configurations
|
# Intellij
|
||||||
.idea/libraries/
|
.idea/
|
||||||
.idea/workspace.xml
|
|
||||||
.idea/tasks.xml
|
|
||||||
.idea/.name
|
|
||||||
.idea/compiler.xml
|
|
||||||
.idea/copyright/profiles_settings.xml
|
|
||||||
.idea/encodings.xml
|
|
||||||
.idea/misc.xml
|
|
||||||
.idea/modules.xml
|
|
||||||
.idea/scopes/scope_settings.xml
|
|
||||||
.idea/vcs.xml
|
|
||||||
.idea/dictionaries/
|
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# OS-specific files
|
# OS-specific files
|
||||||
@@ -40,5 +32,6 @@ local.properties
|
|||||||
._*
|
._*
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
.Trashes
|
.Trashes
|
||||||
|
.swp
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|||||||
229
.idea/codeStyleSettings.xml
generated
@@ -1,229 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectCodeStyleSettingsManager">
|
|
||||||
<option name="PER_PROJECT_SETTINGS">
|
|
||||||
<value>
|
|
||||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
|
||||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
|
||||||
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
|
||||||
<value />
|
|
||||||
</option>
|
|
||||||
<option name="IMPORT_LAYOUT_TABLE">
|
|
||||||
<value>
|
|
||||||
<package name="android" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="com" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="junit" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="net" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="org" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="java" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="javax" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="true" />
|
|
||||||
<emptyLine />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="RIGHT_MARGIN" value="100" />
|
|
||||||
<AndroidXmlCodeStyleSettings>
|
|
||||||
<option name="USE_CUSTOM_SETTINGS" value="true" />
|
|
||||||
</AndroidXmlCodeStyleSettings>
|
|
||||||
<Objective-C-extensions>
|
|
||||||
<option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" />
|
|
||||||
<option name="RELEASE_STYLE" value="IVAR" />
|
|
||||||
<option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" />
|
|
||||||
<file>
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
|
|
||||||
</file>
|
|
||||||
<class>
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
|
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
|
|
||||||
</class>
|
|
||||||
<extensions>
|
|
||||||
<pair source="cpp" header="h" />
|
|
||||||
<pair source="c" header="h" />
|
|
||||||
</extensions>
|
|
||||||
</Objective-C-extensions>
|
|
||||||
<XML>
|
|
||||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
|
||||||
</XML>
|
|
||||||
<codeStyleSettings language="XML">
|
|
||||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
|
||||||
<indentOptions>
|
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
|
||||||
</indentOptions>
|
|
||||||
<arrangement>
|
|
||||||
<rules>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>xmlns:android</NAME>
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>xmlns:.*</NAME>
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:id</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:name</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>name</NAME>
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>style</NAME>
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_width</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_height</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_.*</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:width</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:height</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
</rules>
|
|
||||||
</arrangement>
|
|
||||||
</codeStyleSettings>
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
24
.idea/gradle.xml
generated
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GradleSettings">
|
|
||||||
<option name="linkedExternalProjectsSettings">
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="disableWrapperSourceDistributionNotification" value="true" />
|
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$" />
|
|
||||||
<option value="$PROJECT_DIR$/app" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
<option name="myModules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$" />
|
|
||||||
<option value="$PROJECT_DIR$/app" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
71
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,71 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="AndroidLintLogConditional" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="DeprecatedAPI" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="DuplicateSwitchCase" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="EmptyStatementBody" enabled="false" level="WARNING" enabled_by_default="false">
|
|
||||||
<option name="m_reportEmptyBlocks" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="EndlessLoop" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="EqualityInConditionalOperator" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="Finalize" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoreTrivialFinalizers" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="FinalizeNotProtected" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="FormatSpecifiers" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="FunctionImplicitDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="HidesUpperScope" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="HidingNonVirtualFunction" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="ImplicitIntegerAndEnumConversion" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="ImplicitPointerAndIntegerConversion" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="IncompatibleEnums" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="IncompatibleInitializers" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="IncompatiblePointers" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="InstanceofChain" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoreInstanceofOnLibraryClasses" value="false" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="KRUnspecifiedParameters" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="LocalValueEscapesScope" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
|
|
||||||
<option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
|
|
||||||
<option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="MissingReturn" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="MissingSwitchCase" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="NotImplementedFunctions" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="NotInitializedVariable" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="NotSuperclass" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="OCDFAInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="OCLoopDoesntUseConditionVariableInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="OCSimplifyInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="OCUnusedGlobalDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="OCUnusedMacroInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="OCUnusedStructInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="OCUnusedTemplateParameterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="OnDemandImport" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="PrivateMemberAccessBetweenOuterAndInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="ResourceNotFoundInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="SamePackageImport" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="SignednessMismatch" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
|
||||||
<option name="processCode" value="true" />
|
|
||||||
<option name="processLiterals" value="true" />
|
|
||||||
<option name="processComments" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="m_ignoreJavadoc" value="false" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="UnreachableCode" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="UnusedExpressionResult" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="UnusedImportStatement" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="UnusedLocalVariable" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="UnusedLocalization" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="UnusedParameter" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="UnusedValue" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="ValueMayNotFitIntoReceiver" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="VariableNotUsedInsideIf" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
7
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="PROJECT_PROFILE" value="Project Default" />
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="true" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RunConfigurationProducerService">
|
|
||||||
<option name="ignoredProducers">
|
|
||||||
<set>
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
|
||||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
30
.travis.yml
@@ -1,30 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: android
|
|
||||||
jdk: oraclejdk8
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
# The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
|
|
||||||
# via the "travis encrypt" command using the project repo's public key
|
|
||||||
- secure: "ACnFJxw0VusS2lnGXL+epP/CNJmftWS39YcPdgN2EurWw5ZfXSo7vi+zpMB+11IBS3LQyLFFUambi2N9L4lbReZkHVkoVcZFGZlwbXNTAeqT8CABPTcuOyEOZU4bJwqeYU87ztYipENMLNECaZrgWx5odbWLKnSJQw7Zkb4ArCstfXfYk9u8q49ThRxQyGwHW2xKp1an5aa+3Y6IY+ywsSHw6AvXbyFH078Kolxy86caagczcfmKcMi15QYzwAvFggUphvsO3M5PHJMQXuaNlQxDcQRGUEXsK8aZE0dPH5PB97SFjDALZqI7NEpjZAk5htWjX48ssW064LDbjcBg/ZLgDd8R8uhA159NVZgvcnP2czCn6pmggx1sW5MBmcj7i+bJS2ejaMO+KoovWlVvsch742H5QR6rQaNkjDZRsGVLYvJaR1gBLs898UoT1hcHWoqLVR22r2VFo7OWWCRfNRvZuZDR2HIrYRdFvn8P3nWVMkvXwgsOlxWG5sN+yQqW+6lZS7hivsFhtYs4CkRdoZIan3Qvi/CkY8Lg+ESkZ3IJ0NnId8qOWH+8Xl1sqZ7xlsWTd1sYYHlpvkdvqw1HNLP22EpwwKW5Kb5zBEd/qs3o1OO0Tqa0MR6JpgGdHHRk1iZ25+qTfRVP06vO2RXsgAx4SZfO7DyB0QZn8tGNMMI="
|
|
||||||
|
|
||||||
android:
|
|
||||||
components:
|
|
||||||
- platform-tools
|
|
||||||
- tools
|
|
||||||
- build-tools-24.0.1
|
|
||||||
- android-24
|
|
||||||
- extra-android-m2repository
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./gradlew testDebugUnitTest
|
|
||||||
|
|
||||||
addons:
|
|
||||||
coverity_scan:
|
|
||||||
project:
|
|
||||||
name: "termux/termux-app"
|
|
||||||
description: "Terminal emulator and Linux environment for Android"
|
|
||||||
notification_email: fredrik@fornwall.net
|
|
||||||
build_command_prepend: "./gradlew clean"
|
|
||||||
build_command: "./gradlew assemble"
|
|
||||||
branch_pattern: coverity_scan
|
|
||||||
3
LICENSE.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
|
||||||
|
|
||||||
|
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
|
||||||
88
README.md
@@ -1,39 +1,65 @@
|
|||||||
Termux app
|
# Termux application
|
||||||
==========
|
|
||||||
[](https://travis-ci.org/termux/termux-app)
|
[](https://github.com/termux/termux-app/actions)
|
||||||
|
[](https://github.com/termux/termux-app/actions)
|
||||||
[](https://gitter.im/termux/termux)
|
[](https://gitter.im/termux/termux)
|
||||||
|
|
||||||
|
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
||||||
|
|
||||||
Termux is an Android terminal app and Linux environment.
|
- [Termux Reddit community](https://reddit.com/r/termux)
|
||||||
|
- [Termux Wiki](https://wiki.termux.com/wiki/)
|
||||||
|
- [Termux Twitter](http://twitter.com/termux/)
|
||||||
|
|
||||||
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux)
|
Note that this repository is for the app itself (the user interface and the
|
||||||
* [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux)
|
terminal emulation). For the packages installable inside the app, see
|
||||||
* [termux.com](http://termux.com)
|
[termux/termux-packages](https://github.com/termux/termux-packages)
|
||||||
* [Termux Help](http://termux.com/help/)
|
|
||||||
* [Termux app on GitHub](https://github.com/termux/termux-app)
|
|
||||||
* [Termux packages on GitHub](https://github.com/termux/termux-packages)
|
|
||||||
* [Termux Google+ community](http://termux.com/community/)
|
|
||||||
|
|
||||||
License
|
## Installation
|
||||||
=======
|
|
||||||
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Contains code from `Terminal Emulator for Android` which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
|
|
||||||
|
|
||||||
Building JNI libraries
|
Termux:Widget application can be obtained from:
|
||||||
======================
|
|
||||||
Execute the `build-jnilibs.sh` script to build the required JNI libraries.
|
|
||||||
|
|
||||||
Terminal resources
|
- [Google Play](https://play.google.com/store/apps/details?id=com.termux)
|
||||||
==================
|
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
||||||
* [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
|
||||||
* [vt100.net](http://vt100.net/)
|
|
||||||
* [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
|
||||||
|
|
||||||
Terminal emulators
|
Additionally we provide per-commit debug builds for those who want to try
|
||||||
==================
|
out the latest features or test their pull request. This build can be obtained
|
||||||
* 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).
|
from one of the workflow runs listed on [Github Actions](https://github.com/termux/termux-app/actions)
|
||||||
* 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)).
|
page.
|
||||||
* Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
|
|
||||||
* hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
|
Signature keys of all offered builds are different. Before you switch the
|
||||||
* xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
installation source, you will have to uninstall the Termux application and
|
||||||
* Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
all currently installed plugins.
|
||||||
* Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
|
||||||
|
## Terminal resources
|
||||||
|
|
||||||
|
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||||
|
- [vt100.net](http://vt100.net/)
|
||||||
|
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
||||||
|
|
||||||
|
## Terminal emulators
|
||||||
|
|
||||||
|
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal.
|
||||||
|
[Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+),
|
||||||
|
and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
|
||||||
|
|
||||||
|
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2),
|
||||||
|
[Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html)
|
||||||
|
(which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
|
||||||
|
|
||||||
|
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository),
|
||||||
|
in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests),
|
||||||
|
[Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole)
|
||||||
|
and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
|
||||||
|
|
||||||
|
- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm),
|
||||||
|
including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js),
|
||||||
|
and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
|
||||||
|
|
||||||
|
- xterm: The grandfather of terminal emulators.
|
||||||
|
[Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
||||||
|
|
||||||
|
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
||||||
|
|
||||||
|
- Android Terminal Emulator: Android terminal app which Termux terminal handling
|
||||||
|
is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
||||||
|
|||||||
137
app/build.gradle
@@ -1,25 +1,43 @@
|
|||||||
apply plugin: 'com.android.application'
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 24
|
compileSdkVersion 28
|
||||||
buildToolsVersion "24.0.1"
|
ndkVersion '21.3.6528147'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'com.android.support:support-annotations:24.1.1'
|
implementation "androidx.annotation:annotation:1.1.0"
|
||||||
compile "com.android.support:support-v4:24.1.1"
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
|
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
|
||||||
|
implementation project(":terminal-view")
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion 21
|
minSdkVersion 24
|
||||||
targetSdkVersion 24
|
targetSdkVersion 28
|
||||||
versionCode 37
|
versionCode 95
|
||||||
versionName "0.37"
|
versionName "0.95"
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
moduleName "libtermux"
|
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
}
|
||||||
cFlags "-std=c11 -Wall -Wextra -Os -fno-stack-protector -nostdlib -Wl,--gc-sections"
|
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
debug {
|
||||||
|
storeFile file('dev_keystore.jks')
|
||||||
|
keyAlias 'alias'
|
||||||
|
storePassword 'xrj45yWGLbsO7W0v'
|
||||||
|
keyPassword 'xrj45yWGLbsO7W0v'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,9 +47,102 @@ android {
|
|||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
path "src/main/cpp/Android.mk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
includeAndroidResources = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testCompile 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.13'
|
||||||
|
testImplementation 'org.robolectric:robolectric:4.3.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
task versionName {
|
||||||
|
doLast {
|
||||||
|
print android.defaultConfig.versionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def downloadBootstrap(String arch, String expectedChecksum, int version) {
|
||||||
|
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||||
|
|
||||||
|
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
|
||||||
|
def file = new File(projectDir, localUrl)
|
||||||
|
if (file.exists()) {
|
||||||
|
def buffer = new byte[8192]
|
||||||
|
def input = new FileInputStream(file)
|
||||||
|
while (true) {
|
||||||
|
def readBytes = input.read(buffer)
|
||||||
|
if (readBytes < 0) break
|
||||||
|
digest.update(buffer, 0, readBytes)
|
||||||
|
}
|
||||||
|
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
||||||
|
if (checksum == expectedChecksum) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip"
|
||||||
|
logger.quiet("Downloading " + remoteUrl + " ...")
|
||||||
|
|
||||||
|
file.parentFile.mkdirs()
|
||||||
|
def out = new BufferedOutputStream(new FileOutputStream(file))
|
||||||
|
|
||||||
|
def connection = new URL(remoteUrl).openConnection()
|
||||||
|
connection.setInstanceFollowRedirects(true)
|
||||||
|
def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
|
||||||
|
out << digestStream
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
||||||
|
if (checksum != expectedChecksum) {
|
||||||
|
file.delete()
|
||||||
|
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clean {
|
||||||
|
doLast {
|
||||||
|
def tree = fileTree(new File(projectDir, 'src/main/cpp'))
|
||||||
|
tree.include 'bootstrap-*.zip'
|
||||||
|
tree.each { it.delete() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task downloadBootstraps(){
|
||||||
|
doLast {
|
||||||
|
def version = 25
|
||||||
|
downloadBootstrap("aarch64", "633baa1f7edfd81f6064338a68d1149aa203d4b24cbc4f7c64283aaca109609e", version)
|
||||||
|
downloadBootstrap("arm", "a581a22e0d79a0e8cef9395b1bd951ba066ac2d688522e17cca0b3e1c0649daa", version)
|
||||||
|
downloadBootstrap("i686", "8288e13f0a6ddeb2ff9406d8f968a8930a58e9318d09fadb2b7c8970a034cfdc", version)
|
||||||
|
downloadBootstrap("x86_64", "c99b80a18d6bbb64c24c5a64d6ee6b8d4306729ebd172662b807bcb4a46dd39a", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
android.applicationVariants.all { variant ->
|
||||||
|
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/dev_keystore.jks
Normal file
10
app/proguard-rules.pro
vendored
@@ -7,11 +7,5 @@
|
|||||||
# 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
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
-renamesourcefileattribute SourceFile
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.termux;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.test.ApplicationTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
|
||||||
*/
|
|
||||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
|
||||||
public ApplicationTest() {
|
|
||||||
super(Application.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<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"
|
||||||
package="com.termux"
|
package="com.termux"
|
||||||
android:installLocation="internalOnly"
|
android:installLocation="internalOnly"
|
||||||
android:sharedUserId="com.termux"
|
android:sharedUserId="com.termux"
|
||||||
@@ -7,25 +8,40 @@
|
|||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
||||||
|
<permission android:name="com.termux.permission.RUN_COMMAND"
|
||||||
|
android:label="@string/run_command_permission_label"
|
||||||
|
android:description="@string/run_command_permission_description"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:protectionLevel="dangerous" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<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.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:extractNativeLibs="true"
|
||||||
android:fullBackupContent="@xml/backupscheme"
|
android:allowBackup="false"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:banner="@drawable/banner"
|
android:banner="@drawable/banner"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:theme="@style/Theme.Termux"
|
android:theme="@style/Theme.Termux"
|
||||||
android:supportsRtl="false" >
|
android:supportsRtl="false" >
|
||||||
|
|
||||||
|
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
||||||
|
mark the app with "This app is optimized to run in full screen." -->
|
||||||
|
<meta-data android:name="android.max_aspect" android:value="10.0" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.termux.app.TermuxActivity"
|
android:name="com.termux.app.TermuxActivity"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
android:label="@string/application_name"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
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" />
|
||||||
@@ -34,6 +50,7 @@
|
|||||||
<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" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@@ -41,6 +58,7 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||||
android:parentActivityName=".app.TermuxActivity"
|
android:parentActivityName=".app.TermuxActivity"
|
||||||
|
android:resizeableActivity="true"
|
||||||
android:label="@string/application_name" />
|
android:label="@string/application_name" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@@ -48,6 +66,7 @@
|
|||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:taskAffinity="com.termux.filereceiver"
|
android:taskAffinity="com.termux.filereceiver"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
android:noHistory="true">
|
android:noHistory="true">
|
||||||
<!-- Accept multiple file types when sending. -->
|
<!-- Accept multiple file types when sending. -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -62,10 +81,11 @@
|
|||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
|
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
|
||||||
<intent-filter>
|
<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="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
|
<data android:mimeType="application/*log*" />
|
||||||
<data android:mimeType="application/json" />
|
<data android:mimeType="application/json" />
|
||||||
<data android:mimeType="application/*xml*" />
|
<data android:mimeType="application/*xml*" />
|
||||||
<data android:mimeType="application/*latex*" />
|
<data android:mimeType="application/*latex*" />
|
||||||
@@ -73,6 +93,18 @@
|
|||||||
</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="com.termux.documents"
|
||||||
@@ -88,6 +120,24 @@
|
|||||||
android:name="com.termux.app.TermuxService"
|
android:name="com.termux.app.TermuxService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".app.RunCommandService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="com.termux.permission.RUN_COMMAND" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.termux.RUN_COMMAND" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver android:name=".app.TermuxOpenReceiver" />
|
||||||
|
|
||||||
|
<provider android:authorities="com.termux.files"
|
||||||
|
android:readPermission="android.permission.permRead"
|
||||||
|
android:exported="true"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
|
||||||
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
|
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
5
app/src/main/cpp/Android.mk
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
LOCAL_PATH:= $(call my-dir)
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE := libtermux-bootstrap
|
||||||
|
LOCAL_SRC_FILES := termux-bootstrap-zip.S termux-bootstrap.c
|
||||||
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
18
app/src/main/cpp/termux-bootstrap-zip.S
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.global blob
|
||||||
|
.global blob_size
|
||||||
|
.section .rodata
|
||||||
|
blob:
|
||||||
|
#if defined __i686__
|
||||||
|
.incbin "bootstrap-i686.zip"
|
||||||
|
#elif defined __x86_64__
|
||||||
|
.incbin "bootstrap-x86_64.zip"
|
||||||
|
#elif defined __aarch64__
|
||||||
|
.incbin "bootstrap-aarch64.zip"
|
||||||
|
#elif defined __arm__
|
||||||
|
.incbin "bootstrap-arm.zip"
|
||||||
|
#else
|
||||||
|
# error Unsupported arch
|
||||||
|
#endif
|
||||||
|
1:
|
||||||
|
blob_size:
|
||||||
|
.int 1b - blob
|
||||||
11
app/src/main/cpp/termux-bootstrap.c
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
extern jbyte blob[];
|
||||||
|
extern int blob_size;
|
||||||
|
|
||||||
|
JNIEXPORT jbyteArray JNICALL Java_com_termux_app_TermuxInstaller_getZip(JNIEnv *env, __attribute__((__unused__)) jobject This)
|
||||||
|
{
|
||||||
|
jbyteArray ret = (*env)->NewByteArray(env, blob_size);
|
||||||
|
(*env)->SetByteArrayRegion(env, ret, 0, blob_size, blob);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
@@ -1,68 +1,61 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.nio.charset.StandardCharsets;
|
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.
|
* A background job launched by Termux.
|
||||||
*/
|
*/
|
||||||
public final class BackgroundJob {
|
public final class BackgroundJob {
|
||||||
|
|
||||||
private static final String LOG_TAG = "termux-background";
|
private static final String LOG_TAG = "termux-task";
|
||||||
|
|
||||||
final Process mProcess;
|
final Process mProcess;
|
||||||
|
|
||||||
public BackgroundJob(File cwd, File fileToExecute, String[] args) throws IOException {
|
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service){
|
||||||
String[] env = buildEnvironment(false, cwd.getAbsolutePath());
|
this(cwd, fileToExecute, args, service, null);
|
||||||
|
}
|
||||||
|
|
||||||
String[] progArray = new String[args.length + 1];
|
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) {
|
||||||
|
String[] env = buildEnvironment(false, cwd);
|
||||||
|
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
||||||
|
|
||||||
mProcess = Runtime.getRuntime().exec(progArray, env, cwd);
|
final String[] progArray = setupProcessArgs(fileToExecute, args);
|
||||||
|
final String processDescription = Arrays.toString(progArray);
|
||||||
|
|
||||||
new Thread() {
|
Process process;
|
||||||
@Override
|
try {
|
||||||
public void run() {
|
process = Runtime.getRuntime().exec(progArray, env, new File(cwd));
|
||||||
while (true) {
|
} catch (IOException e) {
|
||||||
try {
|
mProcess = null;
|
||||||
int exitCode = mProcess.waitFor();
|
// TODO: Visible error message?
|
||||||
if (exitCode == 0) {
|
Log.e(LOG_TAG, "Failed running background job: " + processDescription, e);
|
||||||
Log.i(LOG_TAG, "exited normally");
|
return;
|
||||||
return;
|
}
|
||||||
} else {
|
|
||||||
Log.i(LOG_TAG, "exited with exit code: " + exitCode);
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
|
|
||||||
new Thread() {
|
mProcess = process;
|
||||||
@Override
|
final int pid = getPid(mProcess);
|
||||||
public void run() {
|
final Bundle result = new Bundle();
|
||||||
InputStream stdout = mProcess.getInputStream();
|
final StringBuilder outResult = new StringBuilder();
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
final StringBuilder errResult = new StringBuilder();
|
||||||
String line;
|
|
||||||
try {
|
|
||||||
// FIXME: Long lines.
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
Log.i(LOG_TAG, line);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
|
|
||||||
|
Thread errThread = new Thread() {
|
||||||
new Thread() {
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
InputStream stderr = mProcess.getErrorStream();
|
InputStream stderr = mProcess.getErrorStream();
|
||||||
@@ -71,42 +64,188 @@ public final class BackgroundJob {
|
|||||||
try {
|
try {
|
||||||
// FIXME: Long lines.
|
// FIXME: Long lines.
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
Log.e(LOG_TAG, line);
|
errResult.append(line).append('\n');
|
||||||
|
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
errThread.start();
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
|
||||||
|
InputStream stdout = mProcess.getInputStream();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
String line;
|
||||||
|
try {
|
||||||
|
// FIXME: Long lines.
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
|
||||||
|
outResult.append(line).append('\n');
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(LOG_TAG, "Error reading output", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int exitCode = mProcess.waitFor();
|
||||||
|
service.onBackgroundJobExited(BackgroundJob.this);
|
||||||
|
if (exitCode == 0) {
|
||||||
|
Log.i(LOG_TAG, "[" + pid + "] exited normally");
|
||||||
|
} else {
|
||||||
|
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.putString("stdout", outResult.toString());
|
||||||
|
result.putInt("exitCode", exitCode);
|
||||||
|
|
||||||
|
errThread.join();
|
||||||
|
result.putString("stderr", errResult.toString());
|
||||||
|
|
||||||
|
Intent data = new Intent();
|
||||||
|
data.putExtra("result", result);
|
||||||
|
|
||||||
|
if(pendingIntent != null) {
|
||||||
|
try {
|
||||||
|
pendingIntent.send(service.getApplicationContext(), Activity.RESULT_OK, data);
|
||||||
|
} catch (PendingIntent.CanceledException e) {
|
||||||
|
// The caller doesn't want the result? That's fine, just ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String[] buildEnvironment(boolean failSafe, String cwd) {
|
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();
|
new File(TermuxService.HOME_PATH).mkdirs();
|
||||||
|
|
||||||
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
||||||
|
|
||||||
final String termEnv = "TERM=xterm-256color";
|
List<String> environment = new ArrayList<>();
|
||||||
final String homeEnv = "HOME=" + TermuxService.HOME_PATH;
|
|
||||||
final String prefixEnv = "PREFIX=" + TermuxService.PREFIX_PATH;
|
environment.add("TERM=xterm-256color");
|
||||||
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
|
environment.add("HOME=" + TermuxService.HOME_PATH);
|
||||||
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
|
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
|
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||||
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
|
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
||||||
String[] env;
|
// 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) {
|
if (failSafe) {
|
||||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||||
final String pathEnv = "PATH=" + System.getenv("PATH");
|
environment.add("PATH= " + System.getenv("PATH"));
|
||||||
return new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
|
|
||||||
} else {
|
} else {
|
||||||
final String ps1Env = "PS1=$ ";
|
if (shouldAddLdLibraryPath()) {
|
||||||
final String ldEnv = "LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib";
|
environment.add("LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib");
|
||||||
final String langEnv = "LANG=en_US.UTF-8";
|
}
|
||||||
final String pathEnv = "PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets";
|
environment.add("LANG=en_US.UTF-8");
|
||||||
final String pwdEnv = "PWD=" + cwd;
|
environment.add("PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets");
|
||||||
|
environment.add("PWD=" + cwd);
|
||||||
|
environment.add("TMPDIR=" + TermuxService.PREFIX_PATH + "/tmp");
|
||||||
|
}
|
||||||
|
|
||||||
return new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv};
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
63
app/src/main/java/com/termux/app/BellUtil.java
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
|
||||||
|
public class BellUtil {
|
||||||
|
private static BellUtil instance = null;
|
||||||
|
private static final Object lock = new Object();
|
||||||
|
|
||||||
|
public static BellUtil getInstance(Context context) {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new BellUtil((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long DURATION = 50;
|
||||||
|
private static final long MIN_PAUSE = 3 * DURATION;
|
||||||
|
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
private long lastBell = 0;
|
||||||
|
private final Runnable bellRunnable;
|
||||||
|
|
||||||
|
private BellUtil(final Vibrator vibrator) {
|
||||||
|
bellRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (vibrator != null) {
|
||||||
|
vibrator.vibrate(DURATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void doBell() {
|
||||||
|
long now = now();
|
||||||
|
long timeSinceLastBell = now - lastBell;
|
||||||
|
|
||||||
|
if (timeSinceLastBell < 0) {
|
||||||
|
// there is a next bell pending; don't schedule another one
|
||||||
|
} else if (timeSinceLastBell < MIN_PAUSE) {
|
||||||
|
// there was a bell recently, scheudle the next one
|
||||||
|
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
||||||
|
lastBell = lastBell + MIN_PAUSE;
|
||||||
|
} else {
|
||||||
|
// the last bell was long ago, do it now
|
||||||
|
bellRunnable.run();
|
||||||
|
lastBell = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long now() {
|
||||||
|
return SystemClock.uptimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,9 +9,6 @@ import android.view.KeyEvent;
|
|||||||
import android.view.ViewGroup.LayoutParams;
|
import android.view.ViewGroup.LayoutParams;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import android.R;
|
|
||||||
|
|
||||||
public final class DialogUtils {
|
public final class DialogUtils {
|
||||||
|
|
||||||
@@ -33,13 +30,10 @@ public final class DialogUtils {
|
|||||||
|
|
||||||
final AlertDialog[] dialogHolder = new AlertDialog[1];
|
final AlertDialog[] dialogHolder = new AlertDialog[1];
|
||||||
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
|
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
|
||||||
input.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
input.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
@Override
|
onPositive.onTextSet(input.getText().toString());
|
||||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
dialogHolder[0].dismiss();
|
||||||
onPositive.onTextSet(input.getText().toString());
|
return true;
|
||||||
dialogHolder[0].dismiss();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
|
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
|
||||||
@@ -55,31 +49,16 @@ public final class DialogUtils {
|
|||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
||||||
.setTitle(titleText).setView(layout)
|
.setTitle(titleText).setView(layout)
|
||||||
.setPositiveButton(positiveButtonText, new DialogInterface.OnClickListener() {
|
.setPositiveButton(positiveButtonText, (d, whichButton) -> onPositive.onTextSet(input.getText().toString()));
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface d, int whichButton) {
|
|
||||||
onPositive.onTextSet(input.getText().toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (onNeutral != null) {
|
if (onNeutral != null) {
|
||||||
builder.setNeutralButton(neutralButtonText, new DialogInterface.OnClickListener() {
|
builder.setNeutralButton(neutralButtonText, (dialog, which) -> onNeutral.onTextSet(input.getText().toString()));
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
onNeutral.onTextSet(input.getText().toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onNegative == null) {
|
if (onNegative == null) {
|
||||||
builder.setNegativeButton(R.string.cancel, null);
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
} else {
|
} else {
|
||||||
builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() {
|
builder.setNegativeButton(negativeButtonText, (dialog, which) -> onNegative.onTextSet(input.getText().toString()));
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
onNegative.onTextSet(input.getText().toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onDismiss != null) builder.setOnDismissListener(onDismiss);
|
if (onDismiss != null) builder.setOnDismissListener(onDismiss);
|
||||||
|
|||||||
338
app/src/main/java/com/termux/app/ExtraKeysInfos.java
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class ExtraKeysInfos {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matrix of buttons displayed
|
||||||
|
*/
|
||||||
|
private ExtraKeyButton[][] buttons;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This corresponds to one of the CharMapDisplay below
|
||||||
|
*/
|
||||||
|
private String style = "default";
|
||||||
|
|
||||||
|
public ExtraKeysInfos(String propertiesInfo, String style) throws JSONException {
|
||||||
|
this.style = style;
|
||||||
|
|
||||||
|
// Convert String propertiesInfo to Array of Arrays
|
||||||
|
JSONArray arr = new JSONArray(propertiesInfo);
|
||||||
|
Object[][] matrix = new Object[arr.length()][];
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONArray line = arr.getJSONArray(i);
|
||||||
|
matrix[i] = new Object[line.length()];
|
||||||
|
for (int j = 0; j < line.length(); j++) {
|
||||||
|
matrix[i][j] = line.get(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert matrix to buttons
|
||||||
|
this.buttons = new ExtraKeyButton[matrix.length][];
|
||||||
|
for (int i = 0; i < matrix.length; i++) {
|
||||||
|
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
|
||||||
|
for (int j = 0; j < matrix[i].length; j++) {
|
||||||
|
Object key = matrix[i][j];
|
||||||
|
|
||||||
|
JSONObject jobject = normalizeKeyConfig(key);
|
||||||
|
|
||||||
|
ExtraKeyButton button;
|
||||||
|
|
||||||
|
if(! jobject.has("popup")) {
|
||||||
|
// no popup
|
||||||
|
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||||
|
} else {
|
||||||
|
// a popup
|
||||||
|
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
|
||||||
|
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
|
||||||
|
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buttons[i][j] = button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "hello" -> {"key": "hello"}
|
||||||
|
*/
|
||||||
|
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||||
|
JSONObject jobject;
|
||||||
|
if(key instanceof String) {
|
||||||
|
jobject = new JSONObject();
|
||||||
|
jobject.put("key", key);
|
||||||
|
} else if(key instanceof JSONObject) {
|
||||||
|
jobject = (JSONObject) key;
|
||||||
|
} else {
|
||||||
|
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||||
|
}
|
||||||
|
return jobject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeyButton[][] getMatrix() {
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HashMap that implements Python dict.get(key, default) function.
|
||||||
|
* Default java.util .get(key) is then the same as .get(key, null);
|
||||||
|
*/
|
||||||
|
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||||
|
V get(K key, V defaultValue) {
|
||||||
|
if(containsKey(key))
|
||||||
|
return get(key);
|
||||||
|
else
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CharDisplayMap extends CleverMap<String, String> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
||||||
|
*/
|
||||||
|
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
|
||||||
|
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||||
|
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||||
|
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||||
|
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||||
|
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
|
||||||
|
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
||||||
|
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||||
|
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
||||||
|
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
||||||
|
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
||||||
|
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
|
||||||
|
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
|
||||||
|
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
||||||
|
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
||||||
|
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||||
|
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||||
|
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
||||||
|
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
|
||||||
|
// alternative to classic arrow keys
|
||||||
|
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||||
|
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||||
|
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||||
|
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
|
||||||
|
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
||||||
|
// put("FN", "FN"); // no ISO character exists
|
||||||
|
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
||||||
|
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
||||||
|
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
|
||||||
|
// nicer looking for most cases
|
||||||
|
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiple maps are available to quickly change
|
||||||
|
* the style of the keys.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some classic symbols everybody knows
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
// all other characters are displayed as themselves
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classic symbols and less known symbols
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(lessKnownCharactersDisplay); // NEW
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only arrows
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||||
|
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full Iso
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(lessKnownCharactersDisplay); // NEW
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
putAll(notKnownIsoCharacters); // NEW
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some people might call our keys differently
|
||||||
|
*/
|
||||||
|
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
|
||||||
|
put("ESCAPE", "ESC");
|
||||||
|
put("CONTROL", "CTRL");
|
||||||
|
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
||||||
|
put("FUNCTION", "FN");
|
||||||
|
// no alias for ALT
|
||||||
|
|
||||||
|
// Directions are sometimes written as first and last letter for brevety
|
||||||
|
put("LT", "LEFT");
|
||||||
|
put("RT", "RIGHT");
|
||||||
|
put("DN", "DOWN");
|
||||||
|
// put("UP", "UP"); well, "UP" is already two letters
|
||||||
|
|
||||||
|
put("PAGEUP", "PGUP");
|
||||||
|
put("PAGE_UP", "PGUP");
|
||||||
|
put("PAGE UP", "PGUP");
|
||||||
|
put("PAGE-UP", "PGUP");
|
||||||
|
|
||||||
|
// no alias for HOME
|
||||||
|
// no alias for END
|
||||||
|
|
||||||
|
put("PAGEDOWN", "PGDN");
|
||||||
|
put("PAGE_DOWN", "PGDN");
|
||||||
|
put("PAGE-DOWN", "PGDN");
|
||||||
|
|
||||||
|
put("DELETE", "DEL");
|
||||||
|
put("BACKSPACE", "BKSP");
|
||||||
|
|
||||||
|
// easier for writing in termux.properties
|
||||||
|
put("BACKSLASH", "\\");
|
||||||
|
put("QUOTE", "\"");
|
||||||
|
put("APOSTROPHE", "'");
|
||||||
|
}};
|
||||||
|
|
||||||
|
CharDisplayMap getSelectedCharMap() {
|
||||||
|
switch (style) {
|
||||||
|
case "arrows-only":
|
||||||
|
return arrowsOnlyCharDisplay;
|
||||||
|
case "arrows-all":
|
||||||
|
return lotsOfArrowsCharDisplay;
|
||||||
|
case "all":
|
||||||
|
return fullIsoCharDisplay;
|
||||||
|
case "none":
|
||||||
|
return new CharDisplayMap();
|
||||||
|
default:
|
||||||
|
return defaultCharDisplay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
|
||||||
|
* Modifies the array, doesn't return a new one.
|
||||||
|
*/
|
||||||
|
public static String replaceAlias(String key) {
|
||||||
|
return controlCharsAliases.get(key, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExtraKeyButton {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key that will be sent to the terminal, either a control character
|
||||||
|
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||||
|
* some text.
|
||||||
|
*/
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||||
|
*/
|
||||||
|
private boolean macro;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text that will be shown on the button.
|
||||||
|
*/
|
||||||
|
private String display;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The information of the popup (triggered by swipe up).
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private ExtraKeyButton popup = null;
|
||||||
|
|
||||||
|
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||||
|
this(charDisplayMap, config, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config, ExtraKeyButton popup) throws JSONException {
|
||||||
|
String keyFromConfig = config.optString("key", null);
|
||||||
|
String macroFromConfig = config.optString("macro", null);
|
||||||
|
String[] keys;
|
||||||
|
if (keyFromConfig != null && macroFromConfig != null) {
|
||||||
|
throw new JSONException("Both key and macro can't be set for the same key");
|
||||||
|
} else if (keyFromConfig != null) {
|
||||||
|
keys = new String[]{keyFromConfig};
|
||||||
|
this.macro = false;
|
||||||
|
} else if (macroFromConfig != null) {
|
||||||
|
keys = macroFromConfig.split(" ");
|
||||||
|
this.macro = true;
|
||||||
|
} else {
|
||||||
|
throw new JSONException("All keys have to specify either key or macro");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
keys[i] = ExtraKeysInfos.replaceAlias(keys[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.key = String.join(" ", keys);
|
||||||
|
|
||||||
|
String displayFromConfig = config.optString("display", null);
|
||||||
|
if (displayFromConfig != null) {
|
||||||
|
this.display = displayFromConfig;
|
||||||
|
} else {
|
||||||
|
this.display = Arrays.stream(keys)
|
||||||
|
.map(key -> charDisplayMap.get(key, key))
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.popup = popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMacro() {
|
||||||
|
return macro;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplay() {
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ExtraKeyButton getPopup() {
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,35 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.AttributeSet;
|
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.Gravity;
|
import android.view.Gravity;
|
||||||
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.GridLayout;
|
import android.widget.GridLayout;
|
||||||
|
import android.widget.PopupWindow;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.terminal.TerminalSession;
|
|
||||||
import com.termux.view.TerminalView;
|
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
|
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||||
* keyboard.
|
* keyboard.
|
||||||
@@ -20,153 +37,313 @@ import com.termux.view.TerminalView;
|
|||||||
public final class ExtraKeysView extends GridLayout {
|
public final class ExtraKeysView extends GridLayout {
|
||||||
|
|
||||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||||
|
private static final int BUTTON_COLOR = 0x00000000;
|
||||||
|
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
||||||
|
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
|
||||||
|
|
||||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
|
||||||
reload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void sendKey(View view, String keyName) {
|
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
|
||||||
int keyCode = 0;
|
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||||
String chars = null;
|
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||||
switch (keyName) {
|
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||||
case "ESC":
|
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||||
keyCode = KeyEvent.KEYCODE_ESCAPE;
|
put("END", KeyEvent.KEYCODE_MOVE_END);
|
||||||
break;
|
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
||||||
case "TAB":
|
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
||||||
keyCode = KeyEvent.KEYCODE_TAB;
|
put("INS", KeyEvent.KEYCODE_INSERT);
|
||||||
break;
|
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
||||||
case "▲":
|
put("BKSP", KeyEvent.KEYCODE_DEL);
|
||||||
keyCode = KeyEvent.KEYCODE_DPAD_UP;
|
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
||||||
break;
|
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
||||||
case "◀":
|
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||||
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||||
break;
|
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||||
case "▶":
|
put("F1", KeyEvent.KEYCODE_F1);
|
||||||
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
put("F2", KeyEvent.KEYCODE_F2);
|
||||||
break;
|
put("F3", KeyEvent.KEYCODE_F3);
|
||||||
case "▼":
|
put("F4", KeyEvent.KEYCODE_F4);
|
||||||
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
put("F5", KeyEvent.KEYCODE_F5);
|
||||||
break;
|
put("F6", KeyEvent.KEYCODE_F6);
|
||||||
case "―":
|
put("F7", KeyEvent.KEYCODE_F7);
|
||||||
chars = "-";
|
put("F8", KeyEvent.KEYCODE_F8);
|
||||||
break;
|
put("F9", KeyEvent.KEYCODE_F9);
|
||||||
default:
|
put("F10", KeyEvent.KEYCODE_F10);
|
||||||
chars = keyName;
|
put("F11", KeyEvent.KEYCODE_F11);
|
||||||
}
|
put("F12", KeyEvent.KEYCODE_F12);
|
||||||
|
}};
|
||||||
|
|
||||||
if (keyCode > 0) {
|
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||||
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||||
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
if ("KEYBOARD".equals(keyName)) {
|
||||||
|
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.toggleSoftInput(0, 0);
|
||||||
|
} else if ("DRAWER".equals(keyName)) {
|
||||||
|
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||||
|
drawer.openDrawer(Gravity.LEFT);
|
||||||
|
} else if (keyCodesForString.containsKey(keyName)) {
|
||||||
|
int keyCode = keyCodesForString.get(keyName);
|
||||||
|
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 {
|
} else {
|
||||||
TerminalView terminalView = (TerminalView) view.findViewById(R.id.terminal_view);
|
// not a control char
|
||||||
TerminalSession session = terminalView.getCurrentSession();
|
keyName.codePoints().forEach(codePoint -> {
|
||||||
if (session != null) session.write(chars);
|
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ToggleButton controlButton;
|
private void sendKey(View view, ExtraKeyButton buttonInfo) {
|
||||||
private ToggleButton altButton;
|
if (buttonInfo.isMacro()) {
|
||||||
private ToggleButton fnButton;
|
String[] keys = buttonInfo.getKey().split(" ");
|
||||||
|
boolean ctrlDown = false;
|
||||||
public boolean readControlButton() {
|
boolean altDown = false;
|
||||||
if (controlButton.isPressed()) return true;
|
for (String key : keys) {
|
||||||
boolean result = controlButton.isChecked();
|
if ("CTRL".equals(key)) {
|
||||||
if (result) {
|
ctrlDown = true;
|
||||||
controlButton.setChecked(false);
|
} else if ("ALT".equals(key)) {
|
||||||
controlButton.setTextColor(TEXT_COLOR);
|
altDown = true;
|
||||||
|
} else {
|
||||||
|
sendKey(view, key, ctrlDown, altDown);
|
||||||
|
ctrlDown = false;
|
||||||
|
altDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendKey(view, buttonInfo.getKey(), false, false);
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean readAltButton() {
|
public enum SpecialButton {
|
||||||
if (altButton.isPressed()) return true;
|
CTRL, ALT, FN
|
||||||
boolean result = altButton.isChecked();
|
|
||||||
if (result) {
|
|
||||||
altButton.setChecked(false);
|
|
||||||
altButton.setTextColor(TEXT_COLOR);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean readFnButton() {
|
private static class SpecialButtonState {
|
||||||
if (fnButton.isPressed()) return true;
|
boolean isOn = false;
|
||||||
boolean result = fnButton.isChecked();
|
ToggleButton button = null;
|
||||||
if (result) {
|
|
||||||
fnButton.setChecked(false);
|
|
||||||
fnButton.setTextColor(TEXT_COLOR);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void reload() {
|
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||||
altButton = controlButton = null;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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")
|
||||||
|
void reload(ExtraKeysInfos infos) {
|
||||||
|
if(infos == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for(SpecialButtonState state : specialButtons.values())
|
||||||
|
state.button = null;
|
||||||
|
|
||||||
removeAllViews();
|
removeAllViews();
|
||||||
|
|
||||||
String[][] buttons = {
|
ExtraKeyButton[][] buttons = infos.getMatrix();
|
||||||
{"ESC", "CTRL", "ALT", "TAB", "―", "/", "|"}
|
|
||||||
};
|
|
||||||
|
|
||||||
final int rows = buttons.length;
|
setRowCount(buttons.length);
|
||||||
final int cols = buttons[0].length;
|
setColumnCount(maximumLength(buttons));
|
||||||
|
|
||||||
setRowCount(rows);
|
for (int row = 0; row < buttons.length; row++) {
|
||||||
setColumnCount(cols);
|
for (int col = 0; col < buttons[row].length; col++) {
|
||||||
|
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||||
for (int row = 0; row < rows; row++) {
|
|
||||||
for (int col = 0; col < cols; col++) {
|
|
||||||
final String buttonText = buttons[row][col];
|
|
||||||
|
|
||||||
Button button;
|
Button button;
|
||||||
switch (buttonText) {
|
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
|
||||||
case "CTRL":
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey())); // for valueOf: https://stackoverflow.com/a/604426/1980630
|
||||||
button = controlButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
state.isOn = true;
|
||||||
button.setClickable(true);
|
button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
break;
|
button.setClickable(true);
|
||||||
case "ALT":
|
} else {
|
||||||
button = altButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
button.setClickable(true);
|
|
||||||
break;
|
|
||||||
case "FN":
|
|
||||||
button = fnButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
|
||||||
button.setClickable(true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.setText(buttonText);
|
button.setText(buttonInfo.getDisplay());
|
||||||
button.setTextColor(TEXT_COLOR);
|
button.setTextColor(TEXT_COLOR);
|
||||||
|
button.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
final Button finalButton = button;
|
final Button finalButton = button;
|
||||||
button.setOnClickListener(new OnClickListener() {
|
button.setOnClickListener(v -> {
|
||||||
@Override
|
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||||
public void onClick(View v) {
|
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||||
View root = getRootView();
|
|
||||||
switch (buttonText) {
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
case "CTRL":
|
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
case "ALT":
|
} else {
|
||||||
case "FN":
|
// Perform haptic feedback only if no total silence mode enabled.
|
||||||
ToggleButton self = (ToggleButton) finalButton;
|
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
|
||||||
self.setChecked(self.isChecked());
|
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
self.setTextColor(self.isChecked() ? 0xFF80DEEA : TEXT_COLOR);
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
sendKey(root, buttonText);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
View root = getRootView();
|
||||||
|
if (Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
|
||||||
|
ToggleButton self = (ToggleButton) finalButton;
|
||||||
|
self.setChecked(self.isChecked());
|
||||||
|
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
|
||||||
|
} else {
|
||||||
|
sendKey(root, buttonInfo);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
|
button.setOnTouchListener((v, event) -> {
|
||||||
param.height = param.width = 0;
|
final View root = getRootView();
|
||||||
param.rightMargin = param.topMargin = 0;
|
switch (event.getAction()) {
|
||||||
param.setGravity(Gravity.LEFT);
|
case MotionEvent.ACTION_DOWN:
|
||||||
float weight = "▲▼◀▶".contains(buttonText) ? 0.7f : 1.f;
|
longPressCount = 0;
|
||||||
param.columnSpec = GridLayout.spec(col, weight);
|
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||||
param.rowSpec = GridLayout.spec(row, 1.f);
|
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);
|
||||||
|
String extraButtonDisplayedText = buttonInfo.getPopup().getDisplay();
|
||||||
|
popup(v, extraButtonDisplayedText);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
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);
|
button.setLayoutParams(param);
|
||||||
|
|
||||||
addView(button);
|
addView(button);
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.termux.app;
|
|
||||||
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import com.termux.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to manage full screen immersive mode.
|
|
||||||
* <p/>
|
|
||||||
* See https://code.google.com/p/android/issues/detail?id=5497
|
|
||||||
*/
|
|
||||||
final class FullScreenHelper {
|
|
||||||
|
|
||||||
private boolean mEnabled = false;
|
|
||||||
final TermuxActivity mActivity;
|
|
||||||
|
|
||||||
public FullScreenHelper(TermuxActivity activity) {
|
|
||||||
this.mActivity = activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setImmersive(boolean enabled) {
|
|
||||||
if (enabled == mEnabled) return;
|
|
||||||
mEnabled = enabled;
|
|
||||||
|
|
||||||
View decorView = mActivity.getWindow().getDecorView();
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
decorView.setOnSystemUiVisibilityChangeListener
|
|
||||||
(new View.OnSystemUiVisibilityChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onSystemUiVisibilityChange(int visibility) {
|
|
||||||
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
|
||||||
if (mActivity.mSettings.isShowExtraKeys()) {
|
|
||||||
mActivity.findViewById(R.id.viewpager).setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
setImmersiveMode();
|
|
||||||
} else {
|
|
||||||
mActivity.findViewById(R.id.viewpager).setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setImmersiveMode();
|
|
||||||
} else {
|
|
||||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
|
|
||||||
decorView.setOnSystemUiVisibilityChangeListener(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isColorLight(int color) {
|
|
||||||
double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
|
|
||||||
return darkness < 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setImmersiveMode() {
|
|
||||||
int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
| View.SYSTEM_UI_FLAG_FULLSCREEN;
|
|
||||||
int color = ((ColorDrawable) mActivity.getWindow().getDecorView().getBackground()).getColor();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isColorLight(color))
|
|
||||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
|
||||||
mActivity.getWindow().getDecorView().setSystemUiVisibility(flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
86
app/src/main/java/com/termux/app/RunCommandService.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
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 android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When allow-external-apps property is set to "true", Termux is able to process execute intents
|
||||||
|
* sent by third-party applications.
|
||||||
|
*
|
||||||
|
* Third-party program must declare com.termux.permission.RUN_COMMAND permission and it should be
|
||||||
|
* granted by user.
|
||||||
|
*
|
||||||
|
* Sample code to run command "top":
|
||||||
|
* Intent intent = new Intent();
|
||||||
|
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
|
||||||
|
* intent.setAction("com.termux.RUN_COMMAND");
|
||||||
|
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
|
||||||
|
* startService(intent);
|
||||||
|
*/
|
||||||
|
public class RunCommandService extends Service {
|
||||||
|
|
||||||
|
public static final String RUN_COMMAND_ACTION = "com.termux.RUN_COMMAND";
|
||||||
|
public static final String RUN_COMMAND_PATH = "com.termux.RUN_COMMAND_PATH";
|
||||||
|
public static final String RUN_COMMAND_ARGUMENTS = "com.termux.RUN_COMMAND_ARGUMENTS";
|
||||||
|
public static final String RUN_COMMAND_WORKDIR = "com.termux.RUN_COMMAND_WORKDIR";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
if (allowExternalApps() && RUN_COMMAND_ACTION.equals(intent.getAction())) {
|
||||||
|
Uri programUri = new Uri.Builder().scheme("com.termux.file").path(intent.getStringExtra(RUN_COMMAND_PATH)).build();
|
||||||
|
|
||||||
|
Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri);
|
||||||
|
execIntent.setClass(this, TermuxService.class);
|
||||||
|
execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringExtra(RUN_COMMAND_ARGUMENTS));
|
||||||
|
execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, intent.getStringExtra(RUN_COMMAND_WORKDIR));
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
this.startForegroundService(execIntent);
|
||||||
|
} else {
|
||||||
|
this.startService(execIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean allowExternalApps() {
|
||||||
|
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
||||||
|
if (!propsFile.exists())
|
||||||
|
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||||
|
|
||||||
|
Properties props = new Properties();
|
||||||
|
try {
|
||||||
|
if (propsFile.isFile() && propsFile.canRead()) {
|
||||||
|
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||||
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("termux", "Error loading props", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.getProperty("allow-external-apps", "false").equals("true");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,8 +11,6 @@ import android.content.ClipData;
|
|||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.DialogInterface.OnShowListener;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
@@ -26,12 +24,6 @@ import android.net.Uri;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.Vibrator;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.view.PagerAdapter;
|
|
||||||
import android.support.v4.view.ViewPager;
|
|
||||||
import android.support.v4.widget.DrawerLayout;
|
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@@ -40,19 +32,13 @@ import android.util.Log;
|
|||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.View.OnLongClickListener;
|
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
|
||||||
import android.widget.AdapterView.OnItemLongClickListener;
|
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
@@ -73,10 +59,17 @@ import java.io.InputStream;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A terminal emulator activity.
|
* A terminal emulator activity.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -89,14 +82,16 @@ import java.util.regex.Pattern;
|
|||||||
*/
|
*/
|
||||||
public final class TermuxActivity extends Activity implements ServiceConnection {
|
public final class TermuxActivity extends Activity implements ServiceConnection {
|
||||||
|
|
||||||
|
public static final String TERMUX_FAILSAFE_SESSION_ACTION = "com.termux.app.failsafe_session";
|
||||||
|
|
||||||
private static final int CONTEXTMENU_SELECT_URL_ID = 0;
|
private static final int CONTEXTMENU_SELECT_URL_ID = 0;
|
||||||
private static final int CONTEXTMENU_SHARE_TRANSCRIPT_ID = 1;
|
private static final int CONTEXTMENU_SHARE_TRANSCRIPT_ID = 1;
|
||||||
private static final int CONTEXTMENU_PASTE_ID = 3;
|
private static final int CONTEXTMENU_PASTE_ID = 3;
|
||||||
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
|
private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
|
||||||
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
|
private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
|
||||||
private static final int CONTEXTMENU_STYLING_ID = 6;
|
private static final int CONTEXTMENU_STYLING_ID = 6;
|
||||||
private static final int CONTEXTMENU_TOGGLE_FULLSCREEN_ID = 7;
|
|
||||||
private static final int CONTEXTMENU_HELP_ID = 8;
|
private static final int CONTEXTMENU_HELP_ID = 8;
|
||||||
|
private static final int CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON = 9;
|
||||||
|
|
||||||
private static final int MAX_SESSIONS = 8;
|
private static final int MAX_SESSIONS = 8;
|
||||||
|
|
||||||
@@ -111,8 +106,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
ExtraKeysView mExtraKeysView;
|
ExtraKeysView mExtraKeysView;
|
||||||
|
|
||||||
final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this);
|
|
||||||
|
|
||||||
TermuxPreferences mSettings;
|
TermuxPreferences mSettings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,6 +127,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
*/
|
*/
|
||||||
boolean mIsVisible;
|
boolean mIsVisible;
|
||||||
|
|
||||||
|
boolean mIsUsingBlackUI;
|
||||||
|
|
||||||
final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||||
@@ -151,13 +146,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
checkForFontAndColors();
|
checkForFontAndColors();
|
||||||
mSettings.reloadFromProperties(TermuxActivity.this);
|
mSettings.reloadFromProperties(TermuxActivity.this);
|
||||||
|
|
||||||
|
if (mExtraKeysView != null) {
|
||||||
|
mExtraKeysView.reload(mSettings.mExtraKeys);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void checkForFontAndColors() {
|
void checkForFontAndColors() {
|
||||||
try {
|
try {
|
||||||
// Hard-coded paths since this file is used also in Termux:Float.
|
|
||||||
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
|
@SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf");
|
||||||
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
|
@SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties");
|
||||||
|
|
||||||
@@ -190,37 +188,49 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** For processes to access shared internal storage (/sdcard) we need this permission. */
|
/** For processes to access shared internal storage (/sdcard) we need this permission. */
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
|
||||||
public boolean ensureStoragePermissionGranted() {
|
public boolean ensureStoragePermissionGranted() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Always granted before Android 6.0.
|
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle bundle) {
|
public void onCreate(Bundle bundle) {
|
||||||
|
mSettings = new TermuxPreferences(this);
|
||||||
|
mIsUsingBlackUI = mSettings.isUsingBlackUI();
|
||||||
|
if (mIsUsingBlackUI) {
|
||||||
|
this.setTheme(R.style.Theme_Termux_Black);
|
||||||
|
} else {
|
||||||
|
this.setTheme(R.style.Theme_Termux);
|
||||||
|
}
|
||||||
|
|
||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
|
|
||||||
mSettings = new TermuxPreferences(this);
|
|
||||||
|
|
||||||
setContentView(R.layout.drawer_layout);
|
setContentView(R.layout.drawer_layout);
|
||||||
mTerminalView = (TerminalView) findViewById(R.id.terminal_view);
|
|
||||||
mTerminalView.setOnKeyListener(new TermuxKeyListener(this));
|
if (mIsUsingBlackUI) {
|
||||||
|
findViewById(R.id.left_drawer).setBackgroundColor(
|
||||||
|
getResources().getColor(android.R.color.background_dark)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mTerminalView = findViewById(R.id.terminal_view);
|
||||||
|
mTerminalView.setOnKeyListener(new TermuxViewClient(this));
|
||||||
|
|
||||||
mTerminalView.setTextSize(mSettings.getFontSize());
|
mTerminalView.setTextSize(mSettings.getFontSize());
|
||||||
mFullScreenHelper.setImmersive(mSettings.isFullScreen());
|
mTerminalView.setKeepScreenOn(mSettings.isScreenAlwaysOn());
|
||||||
mTerminalView.requestFocus();
|
mTerminalView.requestFocus();
|
||||||
|
|
||||||
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
|
final ViewPager viewPager = findViewById(R.id.viewpager);
|
||||||
if (mSettings.isShowExtraKeys()) viewPager.setVisibility(View.VISIBLE);
|
if (mSettings.mShowExtraKeys) viewPager.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams();
|
||||||
|
layoutParams.height = layoutParams.height * (mSettings.mExtraKeys == null ? 0 : mSettings.mExtraKeys.getMatrix().length);
|
||||||
|
viewPager.setLayoutParams(layoutParams);
|
||||||
|
|
||||||
viewPager.setAdapter(new PagerAdapter() {
|
viewPager.setAdapter(new PagerAdapter() {
|
||||||
@Override
|
@Override
|
||||||
@@ -229,27 +239,34 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isViewFromObject(View view, Object object) {
|
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||||
return view == object;
|
return view == object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Object instantiateItem(ViewGroup collection, int position) {
|
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
|
||||||
LayoutInflater inflater = LayoutInflater.from(TermuxActivity.this);
|
LayoutInflater inflater = LayoutInflater.from(TermuxActivity.this);
|
||||||
View layout;
|
View layout;
|
||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
|
layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false);
|
||||||
|
mExtraKeysView.reload(mSettings.mExtraKeys);
|
||||||
} else {
|
} else {
|
||||||
layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
|
layout = inflater.inflate(R.layout.extra_keys_right, collection, false);
|
||||||
final EditText editText = (EditText) layout.findViewById(R.id.text_input);
|
final EditText editText = layout.findViewById(R.id.text_input);
|
||||||
editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
editText.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
@Override
|
TerminalSession session = getCurrentTermSession();
|
||||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
if (session != null) {
|
||||||
String s = editText.getText().toString() + "\n";
|
if (session.isRunning()) {
|
||||||
getCurrentTermSession().write(s);
|
String textToSend = editText.getText().toString();
|
||||||
|
if (textToSend.length() == 0) textToSend = "\r";
|
||||||
|
session.write(textToSend);
|
||||||
|
} else {
|
||||||
|
removeFinishedSession(session);
|
||||||
|
}
|
||||||
editText.setText("");
|
editText.setText("");
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
collection.addView(layout);
|
collection.addView(layout);
|
||||||
@@ -257,7 +274,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroyItem(ViewGroup collection, int position, Object view) {
|
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
|
||||||
collection.removeView((View) view);
|
collection.removeView((View) view);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -268,54 +285,30 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
mTerminalView.requestFocus();
|
mTerminalView.requestFocus();
|
||||||
} else {
|
} else {
|
||||||
final EditText editText = (EditText) viewPager.findViewById(R.id.text_input);
|
final EditText editText = viewPager.findViewById(R.id.text_input);
|
||||||
if (editText != null) editText.requestFocus();
|
if (editText != null) editText.requestFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
View newSessionButton = findViewById(R.id.new_session_button);
|
View newSessionButton = findViewById(R.id.new_session_button);
|
||||||
newSessionButton.setOnClickListener(new OnClickListener() {
|
newSessionButton.setOnClickListener(v -> addNewSession(false, null));
|
||||||
@Override
|
newSessionButton.setOnLongClickListener(v -> {
|
||||||
public void onClick(View v) {
|
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button,
|
||||||
addNewSession(false, null);
|
text -> addNewSession(false, text), R.string.new_session_failsafe, text -> addNewSession(true, text)
|
||||||
}
|
, -1, null, null);
|
||||||
});
|
return true;
|
||||||
newSessionButton.setOnLongClickListener(new OnLongClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onLongClick(View v) {
|
|
||||||
DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button,
|
|
||||||
new DialogUtils.TextSetListener() {
|
|
||||||
@Override
|
|
||||||
public void onTextSet(String text) {
|
|
||||||
addNewSession(false, text);
|
|
||||||
}
|
|
||||||
}, R.string.new_session_failsafe, new DialogUtils.TextSetListener() {
|
|
||||||
@Override
|
|
||||||
public void onTextSet(String text) {
|
|
||||||
addNewSession(true, text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, -1, null, null);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
|
findViewById(R.id.toggle_keyboard_button).setOnClickListener(v -> {
|
||||||
@Override
|
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
public void onClick(View v) {
|
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
|
||||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
getDrawer().closeDrawers();
|
||||||
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
|
|
||||||
getDrawer().closeDrawers();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
findViewById(R.id.toggle_keyboard_button).setOnLongClickListener(new OnLongClickListener() {
|
findViewById(R.id.toggle_keyboard_button).setOnLongClickListener(v -> {
|
||||||
@Override
|
toggleShowExtraKeys();
|
||||||
public boolean onLongClick(View v) {
|
return true;
|
||||||
toggleShowExtraKeys();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
registerForContextMenu(mTerminalView);
|
registerForContextMenu(mTerminalView);
|
||||||
@@ -332,7 +325,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
void toggleShowExtraKeys() {
|
void toggleShowExtraKeys() {
|
||||||
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
|
final ViewPager viewPager = findViewById(R.id.viewpager);
|
||||||
final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this);
|
final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this);
|
||||||
viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
|
viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
|
||||||
if (showNow && viewPager.getCurrentItem() == 1) {
|
if (showNow && viewPager.getCurrentItem() == 1) {
|
||||||
@@ -383,33 +376,47 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (indexOfSession >= 0)
|
if (indexOfSession >= 0)
|
||||||
showToast(toToastTitle(finishedSession) + " - exited", true);
|
showToast(toToastTitle(finishedSession) + " - exited", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (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 (mTermService.getSessions().size() > 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mListViewAdapter.notifyDataSetChanged();
|
mListViewAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClipboardText(TerminalSession session, String text) {
|
public void onClipboardText(TerminalSession session, String text) {
|
||||||
if (!mIsVisible) return;
|
if (!mIsVisible) return;
|
||||||
showToast("Clipboard:\n\"" + text + "\"", false);
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBell(TerminalSession session) {
|
public void onBell(TerminalSession session) {
|
||||||
if (mIsVisible) {
|
if (!mIsVisible) return;
|
||||||
switch (mSettings.mBellBehaviour) {
|
|
||||||
case TermuxPreferences.BELL_BEEP:
|
|
||||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
|
||||||
break;
|
|
||||||
case TermuxPreferences.BELL_VIBRATE:
|
|
||||||
((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
|
|
||||||
break;
|
|
||||||
case TermuxPreferences.BELL_IGNORE:
|
|
||||||
// Ignore the bell character.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
switch (mSettings.mBellBehaviour) {
|
||||||
|
case TermuxPreferences.BELL_BEEP:
|
||||||
|
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||||
|
break;
|
||||||
|
case TermuxPreferences.BELL_VIBRATE:
|
||||||
|
BellUtil.getInstance(TermuxActivity.this).doBell();
|
||||||
|
break;
|
||||||
|
case TermuxPreferences.BELL_IGNORE:
|
||||||
|
// Ignore the bell character.
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -418,13 +425,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ListView listView = (ListView) findViewById(R.id.left_drawer_list);
|
ListView listView = findViewById(R.id.left_drawer_list);
|
||||||
mListViewAdapter = new ArrayAdapter<TerminalSession>(getApplicationContext(), R.layout.line_in_drawer, mTermService.getSessions()) {
|
mListViewAdapter = new ArrayAdapter<TerminalSession>(getApplicationContext(), R.layout.line_in_drawer, mTermService.getSessions()) {
|
||||||
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||||
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||||
View row = convertView;
|
View row = convertView;
|
||||||
if (row == null) {
|
if (row == null) {
|
||||||
LayoutInflater inflater = getLayoutInflater();
|
LayoutInflater inflater = getLayoutInflater();
|
||||||
@@ -434,8 +442,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
TerminalSession sessionAtRow = getItem(position);
|
TerminalSession sessionAtRow = getItem(position);
|
||||||
boolean sessionRunning = sessionAtRow.isRunning();
|
boolean sessionRunning = sessionAtRow.isRunning();
|
||||||
|
|
||||||
TextView firstLineView = (TextView) row.findViewById(R.id.row_line);
|
TextView firstLineView = row.findViewById(R.id.row_line);
|
||||||
|
if (mIsUsingBlackUI) {
|
||||||
|
firstLineView.setBackground(
|
||||||
|
getResources().getDrawable(R.drawable.selected_session_background_black)
|
||||||
|
);
|
||||||
|
}
|
||||||
String name = sessionAtRow.mSessionName;
|
String name = sessionAtRow.mSessionName;
|
||||||
String sessionTitle = sessionAtRow.getTitle();
|
String sessionTitle = sessionAtRow.getTitle();
|
||||||
|
|
||||||
@@ -455,51 +467,37 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
} else {
|
} else {
|
||||||
firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||||
}
|
}
|
||||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.BLACK : Color.RED;
|
int defaultColor = mIsUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||||
|
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||||
firstLineView.setTextColor(color);
|
firstLineView.setTextColor(color);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
listView.setAdapter(mListViewAdapter);
|
listView.setAdapter(mListViewAdapter);
|
||||||
listView.setOnItemClickListener(new OnItemClickListener() {
|
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
@Override
|
TerminalSession clickedSession = mListViewAdapter.getItem(position);
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
switchToSession(clickedSession);
|
||||||
TerminalSession clickedSession = mListViewAdapter.getItem(position);
|
getDrawer().closeDrawers();
|
||||||
switchToSession(clickedSession);
|
|
||||||
getDrawer().closeDrawers();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
|
listView.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||||
@Override
|
final TerminalSession selectedSession = mListViewAdapter.getItem(position);
|
||||||
public boolean onItemLongClick(AdapterView<?> parent, View view, final int position, long id) {
|
renameSession(selectedSession);
|
||||||
final TerminalSession selectedSession = mListViewAdapter.getItem(position);
|
return true;
|
||||||
renameSession(selectedSession);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mTermService.getSessions().isEmpty()) {
|
if (mTermService.getSessions().isEmpty()) {
|
||||||
if (mIsVisible) {
|
if (mIsVisible) {
|
||||||
TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
|
TermuxInstaller.setupIfNeeded(TermuxActivity.this, () -> {
|
||||||
@Override
|
if (mTermService == null) return; // Activity might have been destroyed.
|
||||||
public void run() {
|
try {
|
||||||
if (mTermService == null) return; // Activity might have been destroyed.
|
Bundle bundle = getIntent().getExtras();
|
||||||
try {
|
boolean launchFailsafe = false;
|
||||||
if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
|
if (bundle != null) {
|
||||||
new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
|
launchFailsafe = bundle.getBoolean(TERMUX_FAILSAFE_SESSION_ACTION, false);
|
||||||
.setCancelable(false).setPositiveButton(android.R.string.ok, null)
|
|
||||||
.setNegativeButton(R.string.welcome_dialog_dont_show_again_button, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
TermuxPreferences.disableWelcomeDialog(TermuxActivity.this);
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
addNewSession(false, null);
|
|
||||||
} catch (WindowManager.BadTokenException e) {
|
|
||||||
// Activity finished - ignore.
|
|
||||||
}
|
}
|
||||||
|
addNewSession(launchFailsafe, null);
|
||||||
|
} catch (WindowManager.BadTokenException e) {
|
||||||
|
// Activity finished - ignore.
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -507,7 +505,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switchToSession(getStoredCurrentSessionOrLast());
|
Intent i = getIntent();
|
||||||
|
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
|
||||||
|
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
|
||||||
|
boolean failSafe = i.getBooleanExtra(TERMUX_FAILSAFE_SESSION_ACTION, false);
|
||||||
|
addNewSession(failSafe, null);
|
||||||
|
} else {
|
||||||
|
switchToSession(getStoredCurrentSessionOrLast());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,20 +529,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
void renameSession(final TerminalSession sessionToRename) {
|
void renameSession(final TerminalSession sessionToRename) {
|
||||||
DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, new DialogUtils.TextSetListener() {
|
DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, text -> {
|
||||||
@Override
|
sessionToRename.mSessionName = text;
|
||||||
public void onTextSet(String text) {
|
mListViewAdapter.notifyDataSetChanged();
|
||||||
sessionToRename.mSessionName = text;
|
|
||||||
}
|
|
||||||
}, -1, null, -1, null, null);
|
}, -1, null, -1, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
if (mTermService != null) {
|
// Respect being stopped from the TermuxService notification action.
|
||||||
// Respect being stopped from the TermuxService notification action.
|
finish();
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -602,8 +603,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
|
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
|
||||||
.setPositiveButton(android.R.string.ok, null).show();
|
.setPositiveButton(android.R.string.ok, null).show();
|
||||||
} else {
|
} else {
|
||||||
String executablePath = (failSafe ? "/system/bin/sh" : null);
|
TerminalSession currentSession = getCurrentTermSession();
|
||||||
TerminalSession newSession = mTermService.createTermSession(executablePath, null, null, failSafe);
|
String workingDirectory = (currentSession == null) ? null : currentSession.getCwd();
|
||||||
|
TerminalSession newSession = mTermService.createTermSession(null, null, workingDirectory, failSafe);
|
||||||
if (sessionName != null) {
|
if (sessionName != null) {
|
||||||
newSession.mSessionName = sessionName;
|
newSession.mSessionName = sessionName;
|
||||||
}
|
}
|
||||||
@@ -641,7 +643,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
final int indexOfSession = mTermService.getSessions().indexOf(session);
|
final int indexOfSession = mTermService.getSessions().indexOf(session);
|
||||||
showToast(toToastTitle(session), false);
|
showToast(toToastTitle(session), false);
|
||||||
mListViewAdapter.notifyDataSetChanged();
|
mListViewAdapter.notifyDataSetChanged();
|
||||||
final ListView lv = ((ListView) findViewById(R.id.left_drawer_list));
|
final ListView lv = findViewById(R.id.left_drawer_list);
|
||||||
lv.setItemChecked(indexOfSession, true);
|
lv.setItemChecked(indexOfSession, true);
|
||||||
lv.smoothScrollToPosition(indexOfSession);
|
lv.smoothScrollToPosition(indexOfSession);
|
||||||
}
|
}
|
||||||
@@ -655,8 +657,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
|
menu.add(Menu.NONE, CONTEXTMENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share);
|
||||||
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
|
menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
|
||||||
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.kill_process, getCurrentTermSession().getPid())).setEnabled(currentSession.isRunning());
|
menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.kill_process, getCurrentTermSession().getPid())).setEnabled(currentSession.isRunning());
|
||||||
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
|
|
||||||
menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal);
|
menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal);
|
||||||
|
menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.toggle_keep_screen_on).setCheckable(true).setChecked(mSettings.isScreenAlwaysOn());
|
||||||
menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help);
|
menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,65 +670,126 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
static LinkedHashSet<CharSequence> extractUrls(String text) {
|
static LinkedHashSet<CharSequence> extractUrls(String text) {
|
||||||
// Pattern for recognizing a URL, based off RFC 3986
|
|
||||||
// http://stackoverflow.com/questions/5713558/detect-and-extract-url-from-a-string
|
StringBuilder regex_sb = new StringBuilder();
|
||||||
|
|
||||||
|
regex_sb.append("("); // Begin first matching group.
|
||||||
|
regex_sb.append("(?:"); // Begin scheme group.
|
||||||
|
regex_sb.append("dav|"); // The DAV proto.
|
||||||
|
regex_sb.append("dict|"); // The DICT proto.
|
||||||
|
regex_sb.append("dns|"); // The DNS proto.
|
||||||
|
regex_sb.append("file|"); // File path.
|
||||||
|
regex_sb.append("finger|"); // The Finger proto.
|
||||||
|
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
|
||||||
|
regex_sb.append("git|"); // The Git proto.
|
||||||
|
regex_sb.append("gopher|"); // The Gopher proto.
|
||||||
|
regex_sb.append("http(?:s?)|"); // The HTTP proto.
|
||||||
|
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
|
||||||
|
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
|
||||||
|
regex_sb.append("ip[fn]s|"); // The IPFS proto.
|
||||||
|
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
|
||||||
|
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
|
||||||
|
regex_sb.append("redis(?:s?)|"); // The Redis proto.
|
||||||
|
regex_sb.append("rsync|"); // The Rsync proto.
|
||||||
|
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
|
||||||
|
regex_sb.append("sftp|"); // The SFTP proto.
|
||||||
|
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
|
||||||
|
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
|
||||||
|
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
|
||||||
|
regex_sb.append("tcp|"); // The TCP proto.
|
||||||
|
regex_sb.append("telnet|"); // The Telnet proto.
|
||||||
|
regex_sb.append("tftp|"); // The TFTP proto.
|
||||||
|
regex_sb.append("udp|"); // The UDP proto.
|
||||||
|
regex_sb.append("vnc|"); // The VNC proto.
|
||||||
|
regex_sb.append("ws(?:s?)"); // The Websocket proto.
|
||||||
|
regex_sb.append(")://"); // End scheme group.
|
||||||
|
regex_sb.append(")"); // End first matching group.
|
||||||
|
|
||||||
|
|
||||||
|
// Begin second matching group.
|
||||||
|
regex_sb.append("(");
|
||||||
|
|
||||||
|
// User name and/or password in format 'user:pass@'.
|
||||||
|
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
|
||||||
|
|
||||||
|
// Begin host group.
|
||||||
|
regex_sb.append("(?:");
|
||||||
|
|
||||||
|
// IP address (from http://www.regular-expressions.info/examples.html).
|
||||||
|
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
|
||||||
|
|
||||||
|
// Host name or domain.
|
||||||
|
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
|
||||||
|
|
||||||
|
// Just path. Used in case of 'file://' scheme.
|
||||||
|
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
|
||||||
|
|
||||||
|
// End host group.
|
||||||
|
regex_sb.append(")");
|
||||||
|
|
||||||
|
// Port number.
|
||||||
|
regex_sb.append("(?::\\d{1,5})?");
|
||||||
|
|
||||||
|
// Resource path with optional query string.
|
||||||
|
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||||
|
|
||||||
|
// Fragment.
|
||||||
|
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||||
|
|
||||||
|
// End second matching group.
|
||||||
|
regex_sb.append(")");
|
||||||
|
|
||||||
final Pattern urlPattern = Pattern.compile(
|
final Pattern urlPattern = Pattern.compile(
|
||||||
"(?:^|[\\W])((ht|f)tp(s?)://|www\\.)" + "(([\\w\\-]+\\.)+?([\\w\\-.~]+/?)*" + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
|
regex_sb.toString(),
|
||||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
||||||
|
|
||||||
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
||||||
Matcher matcher = urlPattern.matcher(text);
|
Matcher matcher = urlPattern.matcher(text);
|
||||||
|
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
int matchStart = matcher.start(1);
|
int matchStart = matcher.start(1);
|
||||||
int matchEnd = matcher.end();
|
int matchEnd = matcher.end();
|
||||||
String url = text.substring(matchStart, matchEnd);
|
String url = text.substring(matchStart, matchEnd);
|
||||||
urlSet.add(url);
|
urlSet.add(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlSet;
|
return urlSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
void showUrlSelection() {
|
void showUrlSelection() {
|
||||||
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptText();
|
String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptTextWithFullLinesJoined();
|
||||||
LinkedHashSet<CharSequence> urlSet = extractUrls(text);
|
LinkedHashSet<CharSequence> urlSet = extractUrls(text);
|
||||||
if (urlSet.isEmpty()) {
|
if (urlSet.isEmpty()) {
|
||||||
new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
|
new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CharSequence[] urls = urlSet.toArray(new CharSequence[urlSet.size()]);
|
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
|
||||||
Collections.reverse(Arrays.asList(urls)); // Latest first.
|
Collections.reverse(Arrays.asList(urls)); // Latest first.
|
||||||
|
|
||||||
// Click to copy url to clipboard:
|
// Click to copy url to clipboard:
|
||||||
final AlertDialog dialog = new AlertDialog.Builder(TermuxActivity.this).setItems(urls, new DialogInterface.OnClickListener() {
|
final AlertDialog dialog = new AlertDialog.Builder(TermuxActivity.this).setItems(urls, (di, which) -> {
|
||||||
@Override
|
String url = (String) urls[which];
|
||||||
public void onClick(DialogInterface di, int which) {
|
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
String url = (String) urls[which];
|
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
|
||||||
Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}).setTitle(R.string.select_url_dialog_title).create();
|
}).setTitle(R.string.select_url_dialog_title).create();
|
||||||
|
|
||||||
// Long press to open URL:
|
// Long press to open URL:
|
||||||
dialog.setOnShowListener(new OnShowListener() {
|
dialog.setOnShowListener(di -> {
|
||||||
@Override
|
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
|
||||||
public void onShow(DialogInterface di) {
|
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||||
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
|
dialog.dismiss();
|
||||||
lv.setOnItemLongClickListener(new OnItemLongClickListener() {
|
String url = (String) urls[position];
|
||||||
@Override
|
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
try {
|
||||||
dialog.dismiss();
|
startActivity(i, null);
|
||||||
String url = (String) urls[position];
|
} catch (ActivityNotFoundException e) {
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
// If no applications match, Android displays a system message.
|
||||||
try {
|
startActivity(Intent.createChooser(i, null));
|
||||||
startActivity(i, null);
|
}
|
||||||
} catch (ActivityNotFoundException e) {
|
return true;
|
||||||
// If no applications match, Android displays a system message.
|
});
|
||||||
startActivity(Intent.createChooser(i, null));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show();
|
||||||
@@ -744,7 +807,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (session != null) {
|
if (session != null) {
|
||||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
intent.setType("text/plain");
|
intent.setType("text/plain");
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
|
||||||
|
// See https://github.com/termux/termux-app/issues/1166.
|
||||||
|
final int MAX_LENGTH = 100_000;
|
||||||
|
if (transcriptText.length() > MAX_LENGTH) {
|
||||||
|
int cutOffIndex = transcriptText.length() - MAX_LENGTH;
|
||||||
|
int nextNewlineIndex = transcriptText.indexOf('\n', cutOffIndex);
|
||||||
|
if (nextNewlineIndex != -1 && nextNewlineIndex != transcriptText.length() - 1) {
|
||||||
|
cutOffIndex = nextNewlineIndex + 1;
|
||||||
|
}
|
||||||
|
transcriptText = transcriptText.substring(cutOffIndex).trim();
|
||||||
|
}
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
||||||
}
|
}
|
||||||
@@ -756,12 +830,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
final AlertDialog.Builder b = new AlertDialog.Builder(this);
|
||||||
b.setIcon(android.R.drawable.ic_dialog_alert);
|
b.setIcon(android.R.drawable.ic_dialog_alert);
|
||||||
b.setMessage(R.string.confirm_kill_process);
|
b.setMessage(R.string.confirm_kill_process);
|
||||||
b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
b.setPositiveButton(android.R.string.yes, (dialog, id) -> {
|
||||||
@Override
|
dialog.dismiss();
|
||||||
public void onClick(DialogInterface dialog, int id) {
|
getCurrentTermSession().finishIfRunning();
|
||||||
dialog.dismiss();
|
|
||||||
getCurrentTermSession().finishIfRunning();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
b.setNegativeButton(android.R.string.no, null);
|
b.setNegativeButton(android.R.string.no, null);
|
||||||
b.show();
|
b.show();
|
||||||
@@ -782,21 +853,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
// The startActivity() call is not documented to throw IllegalArgumentException.
|
// The startActivity() call is not documented to throw IllegalArgumentException.
|
||||||
// However, crash reporting shows that it sometimes does, so catch it here.
|
// However, crash reporting shows that it sometimes does, so catch it here.
|
||||||
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
|
||||||
.setPositiveButton(R.string.styling_install, new DialogInterface.OnClickListener() {
|
.setPositiveButton(R.string.styling_install, (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")))).setNegativeButton(android.R.string.cancel, null).show();
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
|
|
||||||
}
|
|
||||||
}).setNegativeButton(android.R.string.cancel, null).show();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
|
||||||
case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
|
|
||||||
toggleImmersive();
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
case CONTEXTMENU_HELP_ID:
|
case CONTEXTMENU_HELP_ID:
|
||||||
startActivity(new Intent(this, TermuxHelpActivity.class));
|
startActivity(new Intent(this, TermuxHelpActivity.class));
|
||||||
return true;
|
return true;
|
||||||
|
case CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON: {
|
||||||
|
if(mTerminalView.getKeepScreenOn()) {
|
||||||
|
mTerminalView.setKeepScreenOn(false);
|
||||||
|
mSettings.setScreenAlwaysOn(this, false);
|
||||||
|
} else {
|
||||||
|
mTerminalView.setKeepScreenOn(true);
|
||||||
|
mSettings.setScreenAlwaysOn(this, true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -809,12 +882,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleImmersive() {
|
|
||||||
boolean newValue = !mSettings.isFullScreen();
|
|
||||||
mSettings.setFullScreen(this, newValue);
|
|
||||||
mFullScreenHelper.setImmersive(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
void changeFontSize(boolean increase) {
|
void changeFontSize(boolean increase) {
|
||||||
mSettings.changeFontSize(this, increase);
|
mSettings.changeFontSize(this, increase);
|
||||||
mTerminalView.setTextSize(mSettings.getFontSize());
|
mTerminalView.setTextSize(mSettings.getFontSize());
|
||||||
@@ -833,9 +900,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
public TerminalSession getStoredCurrentSessionOrLast() {
|
public TerminalSession getStoredCurrentSessionOrLast() {
|
||||||
TerminalSession stored = TermuxPreferences.getCurrentSession(this);
|
TerminalSession stored = TermuxPreferences.getCurrentSession(this);
|
||||||
if (stored != null) return stored;
|
if (stored != null) return stored;
|
||||||
int numberOfSessions = mTermService.getSessions().size();
|
List<TerminalSession> sessions = mTermService.getSessions();
|
||||||
if (numberOfSessions == 0) return null;
|
return sessions.isEmpty() ? null : sessions.get(sessions.size() - 1);
|
||||||
return mTermService.getSessions().get(numberOfSessions - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Show a toast and dismiss the last one if still visible. */
|
/** Show a toast and dismiss the last one if still visible. */
|
||||||
@@ -846,4 +912,21 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
mLastToast.show();
|
mLastToast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeFinishedSession(TerminalSession finishedSession) {
|
||||||
|
// Return pressed with finished session - remove it.
|
||||||
|
TermuxService service = mTermService;
|
||||||
|
|
||||||
|
int index = service.removeTermSession(finishedSession);
|
||||||
|
mListViewAdapter.notifyDataSetChanged();
|
||||||
|
if (mTermService.getSessions().isEmpty()) {
|
||||||
|
// There are no sessions to show, so finish the activity.
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
if (index >= service.getSessions().size()) {
|
||||||
|
index = service.getSessions().size() - 1;
|
||||||
|
}
|
||||||
|
switchToSession(service.getSessions().get(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,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://termux.com")) {
|
if (url.startsWith("https://wiki.termux.com")) {
|
||||||
// Inline help.
|
// Inline help.
|
||||||
setContentView(progressLayout);
|
setContentView(progressLayout);
|
||||||
return false;
|
return false;
|
||||||
@@ -60,7 +60,7 @@ public final class TermuxHelpActivity extends Activity {
|
|||||||
setContentView(mWebView);
|
setContentView(mWebView);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mWebView.loadUrl("https://termux.com/help.html");
|
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ 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.content.DialogInterface;
|
|
||||||
import android.content.DialogInterface.OnClickListener;
|
|
||||||
import android.content.DialogInterface.OnDismissListener;
|
|
||||||
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;
|
||||||
@@ -19,13 +15,12 @@ import com.termux.R;
|
|||||||
import com.termux.terminal.EmulatorDebug;
|
import com.termux.terminal.EmulatorDebug;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
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;
|
||||||
@@ -40,10 +35,10 @@ import java.util.zip.ZipInputStream;
|
|||||||
* <p/>
|
* <p/>
|
||||||
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
|
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
|
||||||
* <p/>
|
* <p/>
|
||||||
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
|
* (4) The zip file is loaded from a shared library.
|
||||||
* <p/>
|
* <p/>
|
||||||
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
|
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
|
||||||
* continously encountering zip file entries:
|
* continuously encountering zip file entries:
|
||||||
* <p/>
|
* <p/>
|
||||||
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
|
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -59,12 +54,7 @@ final class TermuxInstaller {
|
|||||||
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)
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
|
||||||
.setOnDismissListener(new OnDismissListener() {
|
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
|
||||||
@Override
|
|
||||||
public void onDismiss(DialogInterface dialog) {
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
}).setPositiveButton(android.R.string.ok, null).show();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +79,8 @@ final class TermuxInstaller {
|
|||||||
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);
|
||||||
|
|
||||||
final URL zipUrl = determineZipUrl();
|
final byte[] zipBytes = loadZipBytes();
|
||||||
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
|
try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||||
ZipEntry zipEntry;
|
ZipEntry zipEntry;
|
||||||
while ((zipEntry = zipInput.getNextEntry()) != null) {
|
while ((zipEntry = zipInput.getNextEntry()) != null) {
|
||||||
if (zipEntry.getName().equals("SYMLINKS.txt")) {
|
if (zipEntry.getName().equals("SYMLINKS.txt")) {
|
||||||
@@ -103,14 +93,17 @@ final class TermuxInstaller {
|
|||||||
String oldPath = parts[0];
|
String oldPath = parts[0];
|
||||||
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());
|
||||||
}
|
}
|
||||||
} 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);
|
||||||
if (zipEntry.isDirectory()) {
|
boolean isDirectory = zipEntry.isDirectory();
|
||||||
if (!targetFile.mkdirs())
|
|
||||||
throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
|
ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||||
} else {
|
|
||||||
|
if (!isDirectory) {
|
||||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||||
int readBytes;
|
int readBytes;
|
||||||
while ((readBytes = zipInput.read(buffer)) != -1)
|
while ((readBytes = zipInput.read(buffer)) != -1)
|
||||||
@@ -135,46 +128,29 @@ final class TermuxInstaller {
|
|||||||
throw new RuntimeException("Unable to rename staging folder");
|
throw new RuntimeException("Unable to rename staging folder");
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
activity.runOnUiThread(whenDone);
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
whenDone.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
|
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
|
||||||
activity.runOnUiThread(new Runnable() {
|
activity.runOnUiThread(() -> {
|
||||||
@Override
|
try {
|
||||||
public void run() {
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||||
try {
|
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
dialog.dismiss();
|
||||||
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
|
activity.finish();
|
||||||
@Override
|
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
dialog.dismiss();
|
||||||
dialog.dismiss();
|
TermuxInstaller.setupIfNeeded(activity, whenDone);
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
}).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
TermuxInstaller.setupIfNeeded(activity, whenDone);
|
|
||||||
}
|
|
||||||
}).show();
|
}).show();
|
||||||
} catch (WindowManager.BadTokenException e) {
|
} catch (WindowManager.BadTokenException e1) {
|
||||||
// Activity already dismissed - ignore.
|
// Activity already dismissed - ignore.
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
activity.runOnUiThread(new Runnable() {
|
activity.runOnUiThread(() -> {
|
||||||
@Override
|
try {
|
||||||
public void run() {
|
progress.dismiss();
|
||||||
try {
|
} catch (RuntimeException e) {
|
||||||
progress.dismiss();
|
// Activity already dismissed - ignore.
|
||||||
} catch (RuntimeException e) {
|
|
||||||
// Activity already dismissed - ignore.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -182,52 +158,51 @@ final class TermuxInstaller {
|
|||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get bootstrap zip url for this systems cpu architecture. */
|
private static void ensureDirectoryExists(File directory) {
|
||||||
static URL determineZipUrl() throws MalformedURLException {
|
if (!directory.isDirectory() && !directory.mkdirs()) {
|
||||||
String termuxArch = null;
|
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
|
||||||
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
|
|
||||||
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
|
|
||||||
// Instead we search through the supported abi:s on the device, see:
|
|
||||||
// http://developer.android.com/ndk/guides/abis.html
|
|
||||||
// Note that we search for abi:s in preferred order, and want to avoid installing arm on
|
|
||||||
// an x86 system where arm emulation is available.
|
|
||||||
final String[] androidArchNames = {"arm64-v8a", "x86_64", "x86", "armeabi-v7a"};
|
|
||||||
final String[] termuxArchNames = {"aarch64", "x86_64", "i686", "arm"};
|
|
||||||
|
|
||||||
final List<String> supportedArches = Arrays.asList(Build.SUPPORTED_ABIS);
|
|
||||||
for (int i = 0; i < termuxArchNames.length; i++) {
|
|
||||||
if (supportedArches.contains(androidArchNames[i])) {
|
|
||||||
termuxArch = termuxArchNames[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new URL("https://termux.net/bootstrap/bootstrap-" + termuxArch + ".zip");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Delete a folder and all its content or throw. */
|
public static byte[] loadZipBytes() {
|
||||||
static void deleteFolder(File fileOrDirectory) {
|
// Only load the shared library when necessary to save memory usage.
|
||||||
File[] children = fileOrDirectory.listFiles();
|
System.loadLibrary("termux-bootstrap");
|
||||||
if (children != null) {
|
return getZip();
|
||||||
for (File child : children) {
|
}
|
||||||
deleteFolder(child);
|
|
||||||
|
public static native byte[] getZip();
|
||||||
|
|
||||||
|
/** Delete a folder and all its content or throw. Don't follow symlinks. */
|
||||||
|
static void deleteFolder(File fileOrDirectory) throws IOException {
|
||||||
|
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
|
||||||
|
File[] children = fileOrDirectory.listFiles();
|
||||||
|
|
||||||
|
if (children != null) {
|
||||||
|
for (File child : children) {
|
||||||
|
deleteFolder(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fileOrDirectory.delete()) {
|
if (!fileOrDirectory.delete()) {
|
||||||
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
|
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setupStorageSymlinks(final Context context) {
|
static void setupStorageSymlinks(final Context context) {
|
||||||
final String LOG_TAG = "termux-storage";
|
final String LOG_TAG = "termux-storage";
|
||||||
new Thread() {
|
new Thread() {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
||||||
|
|
||||||
if (storageDir.exists() && !storageDir.delete()) {
|
if (storageDir.exists()) {
|
||||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage");
|
try {
|
||||||
return;
|
deleteFolder(storageDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!storageDir.mkdirs()) {
|
if (!storageDir.mkdirs()) {
|
||||||
@@ -254,9 +229,13 @@ final class TermuxInstaller {
|
|||||||
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
||||||
|
|
||||||
final File[] dirs = context.getExternalFilesDirs(null);
|
final File[] dirs = context.getExternalFilesDirs(null);
|
||||||
if (dirs != null && dirs.length >= 2) {
|
if (dirs != null && dirs.length > 1) {
|
||||||
final File externalDir = dirs[1];
|
for (int i = 1; i < dirs.length; i++) {
|
||||||
Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath());
|
File dir = dirs[i];
|
||||||
|
if (dir == null) continue;
|
||||||
|
String symlinkName = "external-" + i;
|
||||||
|
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(LOG_TAG, "Error setting up link", e);
|
Log.e(LOG_TAG, "Error setting up link", e);
|
||||||
|
|||||||
191
app/src/main/java/com/termux/app/TermuxOpenReceiver.java
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import com.termux.terminal.EmulatorDebug;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
final Uri data = intent.getData();
|
||||||
|
if (data == null) {
|
||||||
|
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Called without intent data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String filePath = data.getPath();
|
||||||
|
final String contentTypeExtra = intent.getStringExtra("content-type");
|
||||||
|
final boolean useChooser = intent.getBooleanExtra("chooser", false);
|
||||||
|
final String intentAction = intent.getAction() == null ? Intent.ACTION_VIEW : intent.getAction();
|
||||||
|
switch (intentAction) {
|
||||||
|
case Intent.ACTION_SEND:
|
||||||
|
case Intent.ACTION_VIEW:
|
||||||
|
// Ok.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.e(EmulatorDebug.LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
|
||||||
|
if (isExternalUrl) {
|
||||||
|
Intent urlIntent = new Intent(intentAction, data);
|
||||||
|
if (intentAction.equals(Intent.ACTION_SEND)) {
|
||||||
|
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
|
||||||
|
urlIntent.setData(null);
|
||||||
|
} else if (contentTypeExtra != null) {
|
||||||
|
urlIntent.setDataAndType(data, contentTypeExtra);
|
||||||
|
}
|
||||||
|
urlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
try {
|
||||||
|
context.startActivity(urlIntent);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final File fileToShare = new File(filePath);
|
||||||
|
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
||||||
|
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent sendIntent = new Intent();
|
||||||
|
sendIntent.setAction(intentAction);
|
||||||
|
sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
|
String contentTypeToUse;
|
||||||
|
if (contentTypeExtra == null) {
|
||||||
|
String fileName = fileToShare.getName();
|
||||||
|
int lastDotIndex = fileName.lastIndexOf('.');
|
||||||
|
String fileExtension = fileName.substring(lastDotIndex + 1);
|
||||||
|
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
|
||||||
|
// Lower casing makes it work with e.g. "JPG":
|
||||||
|
contentTypeToUse = mimeTypes.getMimeTypeFromExtension(fileExtension.toLowerCase());
|
||||||
|
if (contentTypeToUse == null) contentTypeToUse = "application/octet-stream";
|
||||||
|
} else {
|
||||||
|
contentTypeToUse = contentTypeExtra;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uriToShare = Uri.parse("content://com.termux.files" + fileToShare.getAbsolutePath());
|
||||||
|
|
||||||
|
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||||
|
sendIntent.setType(contentTypeToUse);
|
||||||
|
} else {
|
||||||
|
sendIntent.setDataAndType(uriToShare, contentTypeToUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useChooser) {
|
||||||
|
sendIntent = Intent.createChooser(sendIntent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.startActivity(sendIntent);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ContentProvider extends android.content.ContentProvider {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||||
|
File file = new File(uri.getPath());
|
||||||
|
|
||||||
|
if (projection == null) {
|
||||||
|
projection = new String[]{
|
||||||
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
|
MediaStore.MediaColumns.SIZE,
|
||||||
|
MediaStore.MediaColumns._ID
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[] row = new Object[projection.length];
|
||||||
|
for (int i = 0; i < projection.length; i++) {
|
||||||
|
String column = projection[i];
|
||||||
|
Object value;
|
||||||
|
switch (column) {
|
||||||
|
case MediaStore.MediaColumns.DISPLAY_NAME:
|
||||||
|
value = file.getName();
|
||||||
|
break;
|
||||||
|
case MediaStore.MediaColumns.SIZE:
|
||||||
|
value = (int) file.length();
|
||||||
|
break;
|
||||||
|
case MediaStore.MediaColumns._ID:
|
||||||
|
value = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
row[i] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
MatrixCursor cursor = new MatrixCursor(projection);
|
||||||
|
cursor.addRow(row);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType(@NonNull Uri uri) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||||
|
File file = new File(uri.getPath());
|
||||||
|
try {
|
||||||
|
String path = file.getCanonicalPath();
|
||||||
|
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||||
|
// See https://support.google.com/faqs/answer/7496913:
|
||||||
|
if (!(path.startsWith(TermuxService.FILES_PATH) || path.startsWith(storagePath))) {
|
||||||
|
throw new IllegalArgumentException("Invalid path: " + path);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,29 +2,55 @@ package com.termux.app;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.IntDef;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
|
||||||
final class TermuxPreferences {
|
final class TermuxPreferences {
|
||||||
|
|
||||||
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
|
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
public @interface AsciiBellBehaviour {
|
@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_VIBRATE = 1;
|
||||||
static final int BELL_BEEP = 2;
|
static final int BELL_BEEP = 2;
|
||||||
static final int BELL_IGNORE = 3;
|
static final int BELL_IGNORE = 3;
|
||||||
@@ -32,21 +58,33 @@ final class TermuxPreferences {
|
|||||||
private final int MIN_FONTSIZE;
|
private final int MIN_FONTSIZE;
|
||||||
private static final int MAX_FONTSIZE = 256;
|
private static final int MAX_FONTSIZE = 256;
|
||||||
|
|
||||||
private static final String FULLSCREEN_KEY = "fullscreen";
|
|
||||||
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
|
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
|
||||||
private static final String FONTSIZE_KEY = "fontsize";
|
private static final String FONTSIZE_KEY = "fontsize";
|
||||||
private static final String CURRENT_SESSION_KEY = "current_session";
|
private static final String CURRENT_SESSION_KEY = "current_session";
|
||||||
private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog";
|
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
|
||||||
|
|
||||||
private boolean mFullScreen;
|
private boolean mUseDarkUI;
|
||||||
|
private boolean mScreenAlwaysOn;
|
||||||
private int mFontSize;
|
private int mFontSize;
|
||||||
|
|
||||||
@AsciiBellBehaviour
|
@AsciiBellBehaviour
|
||||||
int mBellBehaviour = BELL_VIBRATE;
|
int mBellBehaviour = BELL_VIBRATE;
|
||||||
|
|
||||||
boolean mBackIsEscape;
|
boolean mBackIsEscape;
|
||||||
|
boolean mDisableVolumeVirtualKeys;
|
||||||
boolean mShowExtraKeys;
|
boolean mShowExtraKeys;
|
||||||
|
|
||||||
|
ExtraKeysInfos mExtraKeys;
|
||||||
|
|
||||||
|
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If value is not in the range [min, max], set it to either min or max.
|
||||||
|
*/
|
||||||
|
static int clamp(int value, int min, int max) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
TermuxPreferences(Context context) {
|
TermuxPreferences(Context context) {
|
||||||
reloadFromProperties(context);
|
reloadFromProperties(context);
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
@@ -57,8 +95,8 @@ final class TermuxPreferences {
|
|||||||
// to prevent invisible text due to zoom be mistake:
|
// to prevent invisible text due to zoom be mistake:
|
||||||
MIN_FONTSIZE = (int) (4f * dipInPixels);
|
MIN_FONTSIZE = (int) (4f * dipInPixels);
|
||||||
|
|
||||||
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false);
|
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, true);
|
||||||
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, false);
|
mScreenAlwaysOn = prefs.getBoolean(SCREEN_ALWAYS_ON_KEY, false);
|
||||||
|
|
||||||
// http://www.google.com/design/spec/style/typography.html#typography-line-height
|
// http://www.google.com/design/spec/style/typography.html#typography-line-height
|
||||||
int defaultFontSize = Math.round(12 * dipInPixels);
|
int defaultFontSize = Math.round(12 * dipInPixels);
|
||||||
@@ -70,20 +108,7 @@ final class TermuxPreferences {
|
|||||||
} catch (NumberFormatException | ClassCastException e) {
|
} catch (NumberFormatException | ClassCastException e) {
|
||||||
mFontSize = defaultFontSize;
|
mFontSize = defaultFontSize;
|
||||||
}
|
}
|
||||||
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
|
mFontSize = clamp(mFontSize, MIN_FONTSIZE, MAX_FONTSIZE);
|
||||||
}
|
|
||||||
|
|
||||||
boolean isFullScreen() {
|
|
||||||
return mFullScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setFullScreen(Context context, boolean newValue) {
|
|
||||||
mFullScreen = newValue;
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isShowExtraKeys() {
|
|
||||||
return mShowExtraKeys;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean toggleShowExtraKeys(Context context) {
|
boolean toggleShowExtraKeys(Context context) {
|
||||||
@@ -104,8 +129,21 @@ final class TermuxPreferences {
|
|||||||
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
|
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isScreenAlwaysOn() {
|
||||||
|
return mScreenAlwaysOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isUsingBlackUI() {
|
||||||
|
return mUseDarkUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setScreenAlwaysOn(Context context, boolean newValue) {
|
||||||
|
mScreenAlwaysOn = newValue;
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply();
|
||||||
|
}
|
||||||
|
|
||||||
static void storeCurrentSession(Context context, TerminalSession session) {
|
static void storeCurrentSession(Context context, TerminalSession session) {
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit();
|
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
static TerminalSession getCurrentSession(TermuxActivity context) {
|
static TerminalSession getCurrentSession(TermuxActivity context) {
|
||||||
@@ -117,76 +155,82 @@ final class TermuxPreferences {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isShowWelcomeDialog(Context context) {
|
void reloadFromProperties(Context context) {
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true);
|
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
||||||
}
|
if (!propsFile.exists())
|
||||||
|
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||||
|
|
||||||
public static void disableWelcomeDialog(Context context) {
|
Properties props = new Properties();
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reloadFromProperties(Context context) {
|
|
||||||
try {
|
try {
|
||||||
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();
|
|
||||||
if (propsFile.isFile() && propsFile.canRead()) {
|
if (propsFile.isFile() && propsFile.canRead()) {
|
||||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||||
props.load(in);
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (props.getProperty("bell-character", "vibrate")) {
|
|
||||||
case "beep":
|
|
||||||
mBellBehaviour = BELL_BEEP;
|
|
||||||
break;
|
|
||||||
case "ignore":
|
|
||||||
mBellBehaviour = BELL_IGNORE;
|
|
||||||
break;
|
|
||||||
default: // "vibrate".
|
|
||||||
mBellBehaviour = BELL_VIBRATE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
Toast.makeText(context, "Could not open properties file termux.properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||||
Log.e("termux", "Error loading props", e);
|
Log.e("termux", "Error loading props", e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static final int SHORTCUT_ACTION_CREATE_SESSION = 1;
|
switch (props.getProperty("bell-character", "vibrate")) {
|
||||||
public static final int SHORTCUT_ACTION_NEXT_SESSION = 2;
|
case "beep":
|
||||||
public static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3;
|
mBellBehaviour = BELL_BEEP;
|
||||||
public static final int SHORTCUT_ACTION_RENAME_SESSION = 4;
|
break;
|
||||||
|
case "ignore":
|
||||||
public final static class KeyboardShortcut {
|
mBellBehaviour = BELL_IGNORE;
|
||||||
|
break;
|
||||||
public KeyboardShortcut(int codePoint, int shortcutAction) {
|
default: // "vibrate".
|
||||||
this.codePoint = codePoint;
|
mBellBehaviour = BELL_VIBRATE;
|
||||||
this.shortcutAction = shortcutAction;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int codePoint;
|
switch (props.getProperty("use-black-ui", "").toLowerCase()) {
|
||||||
final int shortcutAction;
|
case "true":
|
||||||
}
|
mUseDarkUI = true;
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
mUseDarkUI = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
|
mUseDarkUI = nightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||||
|
}
|
||||||
|
|
||||||
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
|
String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]";
|
||||||
|
|
||||||
|
try {
|
||||||
|
String extrakeyProp = props.getProperty("extra-keys", defaultExtraKeys);
|
||||||
|
String extraKeysStyle = props.getProperty("extra-keys-style", "default");
|
||||||
|
mExtraKeys = new ExtraKeysInfos(extrakeyProp, extraKeysStyle);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show();
|
||||||
|
Log.e("termux", "Error loading props", e);
|
||||||
|
|
||||||
|
try {
|
||||||
|
mExtraKeys = new ExtraKeysInfos(defaultExtraKeys, "default");
|
||||||
|
} catch (JSONException e2) {
|
||||||
|
e2.printStackTrace();
|
||||||
|
Toast.makeText(context, "Can't create default extra keys", Toast.LENGTH_LONG).show();
|
||||||
|
mExtraKeys = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
||||||
|
mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual"));
|
||||||
|
|
||||||
|
shortcuts.clear();
|
||||||
|
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
|
||||||
|
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
|
||||||
|
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
|
||||||
|
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
|
||||||
|
}
|
||||||
|
|
||||||
private void parseAction(String name, int shortcutAction, Properties props) {
|
private void parseAction(String name, int shortcutAction, Properties props) {
|
||||||
String value = props.getProperty(name);
|
String value = props.getProperty(name);
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
String[] parts = value.trim().split("\\+");
|
String[] parts = value.toLowerCase().trim().split("\\+");
|
||||||
String input = parts.length == 2 ? parts[1].trim() : null;
|
String input = parts.length == 2 ? parts[1].trim() : null;
|
||||||
if (!(parts.length == 2 && parts[0].trim().equalsIgnoreCase("ctrl")) || input.isEmpty() || input.length() > 2) {
|
if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) {
|
||||||
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
|
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,22 @@ package com.termux.app;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
|
|
||||||
@@ -22,7 +27,6 @@ import com.termux.terminal.TerminalSession;
|
|||||||
import com.termux.terminal.TerminalSession.SessionChangedCallback;
|
import com.termux.terminal.TerminalSession.SessionChangedCallback;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -40,6 +44,8 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public final class TermuxService extends Service implements SessionChangedCallback {
|
public final class TermuxService extends Service implements SessionChangedCallback {
|
||||||
|
|
||||||
|
private static final String NOTIFICATION_CHANNEL_ID = "termux_notification_channel";
|
||||||
|
|
||||||
/** Note that this is a symlink on the Android M preview. */
|
/** Note that this is a symlink on the Android M preview. */
|
||||||
@SuppressLint("SdCardPath")
|
@SuppressLint("SdCardPath")
|
||||||
public static final String FILES_PATH = "/data/data/com.termux/files";
|
public static final String FILES_PATH = "/data/data/com.termux/files";
|
||||||
@@ -48,18 +54,16 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
|
|
||||||
private static final int NOTIFICATION_ID = 1337;
|
private static final int NOTIFICATION_ID = 1337;
|
||||||
|
|
||||||
/** Intent action to stop the service. */
|
|
||||||
private static final String ACTION_STOP_SERVICE = "com.termux.service_stop";
|
private static final String ACTION_STOP_SERVICE = "com.termux.service_stop";
|
||||||
/** Intent action to toggle the wake lock, {@link #mWakeLock}, which this service may hold. */
|
private static final String ACTION_LOCK_WAKE = "com.termux.service_wake_lock";
|
||||||
private static final String ACTION_LOCK_WAKE = "com.termux.service_toggle_wake_lock";
|
private static final String ACTION_UNLOCK_WAKE = "com.termux.service_wake_unlock";
|
||||||
/** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */
|
|
||||||
private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
|
|
||||||
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
|
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
|
||||||
public static final String ACTION_EXECUTE = "com.termux.service_execute";
|
public static final String ACTION_EXECUTE = "com.termux.service_execute";
|
||||||
|
|
||||||
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
|
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
|
||||||
|
|
||||||
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
|
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
|
||||||
|
private static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background";
|
||||||
|
|
||||||
/** This service is only bound from inside the same process and never uses IPC. */
|
/** This service is only bound from inside the same process and never uses IPC. */
|
||||||
class LocalBinder extends Binder {
|
class LocalBinder extends Binder {
|
||||||
@@ -68,6 +72,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
|
|
||||||
private final IBinder mBinder = new LocalBinder();
|
private final IBinder mBinder = new LocalBinder();
|
||||||
|
|
||||||
|
private final Handler mHandler = new Handler();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The terminal sessions which this service manages.
|
* The terminal sessions which this service manages.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -76,9 +82,12 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
*/
|
*/
|
||||||
final List<TerminalSession> mTerminalSessions = new ArrayList<>();
|
final List<TerminalSession> mTerminalSessions = new ArrayList<>();
|
||||||
|
|
||||||
|
final List<BackgroundJob> mBackgroundTasks = new ArrayList<>();
|
||||||
|
|
||||||
/** Note that the service may often outlive the activity, so need to clear this reference. */
|
/** Note that the service may often outlive the activity, so need to clear this reference. */
|
||||||
SessionChangedCallback mSessionChangeCallback;
|
SessionChangedCallback mSessionChangeCallback;
|
||||||
|
|
||||||
|
/** The wake lock and wifi lock are always acquired and released together. */
|
||||||
private PowerManager.WakeLock mWakeLock;
|
private PowerManager.WakeLock mWakeLock;
|
||||||
private WifiManager.WifiLock mWifiLock;
|
private WifiManager.WifiLock mWifiLock;
|
||||||
|
|
||||||
@@ -99,41 +108,67 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||||
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
|
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
|
||||||
mWakeLock.acquire();
|
mWakeLock.acquire();
|
||||||
} else {
|
|
||||||
mWakeLock.release();
|
// http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak
|
||||||
mWakeLock = null;
|
WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||||
}
|
|
||||||
updateNotification();
|
|
||||||
} else if (ACTION_LOCK_WIFI.equals(action)) {
|
|
||||||
if (mWifiLock == null) {
|
|
||||||
WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
|
||||||
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
|
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
|
||||||
mWifiLock.acquire();
|
mWifiLock.acquire();
|
||||||
} else {
|
|
||||||
|
String packageName = getPackageName();
|
||||||
|
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||||
|
Intent whitelist = new Intent();
|
||||||
|
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||||
|
whitelist.setData(Uri.parse("package:" + packageName));
|
||||||
|
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(whitelist);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.e(EmulatorDebug.LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNotification();
|
||||||
|
}
|
||||||
|
} else if (ACTION_UNLOCK_WAKE.equals(action)) {
|
||||||
|
if (mWakeLock != null) {
|
||||||
|
mWakeLock.release();
|
||||||
|
mWakeLock = null;
|
||||||
|
|
||||||
mWifiLock.release();
|
mWifiLock.release();
|
||||||
mWifiLock = null;
|
mWifiLock = null;
|
||||||
|
|
||||||
|
updateNotification();
|
||||||
}
|
}
|
||||||
updateNotification();
|
|
||||||
} else if (ACTION_EXECUTE.equals(action)) {
|
} else if (ACTION_EXECUTE.equals(action)) {
|
||||||
Uri executableUri = intent.getData();
|
Uri executableUri = intent.getData();
|
||||||
String executablePath = (executableUri == null ? null : executableUri.getPath());
|
String executablePath = (executableUri == null ? null : executableUri.getPath());
|
||||||
|
|
||||||
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
|
String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS));
|
||||||
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
|
String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY);
|
||||||
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
|
|
||||||
|
|
||||||
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
if (intent.getBooleanExtra(EXTRA_EXECUTE_IN_BACKGROUND, false)) {
|
||||||
if (executablePath != null) {
|
BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this, intent.getParcelableExtra("pendingIntent"));
|
||||||
int lastSlash = executablePath.lastIndexOf('/');
|
mBackgroundTasks.add(task);
|
||||||
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
|
updateNotification();
|
||||||
name = name.replace('-', ' ');
|
} else {
|
||||||
newSession.mSessionName = name;
|
boolean failsafe = intent.getBooleanExtra(TermuxActivity.TERMUX_FAILSAFE_SESSION_ACTION, false);
|
||||||
|
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, failsafe);
|
||||||
|
|
||||||
|
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
||||||
|
if (executablePath != null) {
|
||||||
|
int lastSlash = executablePath.lastIndexOf('/');
|
||||||
|
String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1);
|
||||||
|
name = name.replace('-', ' ');
|
||||||
|
newSession.mSessionName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the newly created session the current one to be displayed:
|
||||||
|
TermuxPreferences.storeCurrentSession(this, newSession);
|
||||||
|
|
||||||
|
// Launch the main Termux app, which will now show the current session:
|
||||||
|
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the newly created session the current one to be displayed:
|
|
||||||
TermuxPreferences.storeCurrentSession(this, newSession);
|
|
||||||
|
|
||||||
// Launch the main Termux app, which will now show to current session:
|
|
||||||
startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
|
||||||
} else if (action != null) {
|
} else if (action != null) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
|
Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
|
||||||
}
|
}
|
||||||
@@ -150,13 +185,14 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
setupNotificationChannel();
|
||||||
startForeground(NOTIFICATION_ID, buildNotification());
|
startForeground(NOTIFICATION_ID, buildNotification());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update the shown foreground service notification after making any changes that affect it. */
|
/** Update the shown foreground service notification after making any changes that affect it. */
|
||||||
private void updateNotification() {
|
void updateNotification() {
|
||||||
if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) {
|
if (mWakeLock == null && mTerminalSessions.isEmpty() && mBackgroundTasks.isEmpty()) {
|
||||||
// Exit if we are updating after the user disabled all locks with no sessions.
|
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
|
||||||
stopSelf();
|
stopSelf();
|
||||||
} else {
|
} else {
|
||||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
|
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
|
||||||
@@ -171,18 +207,15 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
|
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
|
||||||
|
|
||||||
int sessionCount = mTerminalSessions.size();
|
int sessionCount = mTerminalSessions.size();
|
||||||
String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s");
|
int taskCount = mBackgroundTasks.size();
|
||||||
|
String contentText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
|
||||||
boolean wakeLockHeld = mWakeLock != null;
|
if (taskCount > 0) {
|
||||||
boolean wifiLockHeld = mWifiLock != null;
|
contentText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
|
||||||
if (wakeLockHeld && wifiLockHeld) {
|
|
||||||
contentText += " (wake&wifi lock held)";
|
|
||||||
} else if (wakeLockHeld) {
|
|
||||||
contentText += " (wake lock held)";
|
|
||||||
} else if (wifiLockHeld) {
|
|
||||||
contentText += " (wifi lock held)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final boolean wakeLockHeld = mWakeLock != null;
|
||||||
|
if (wakeLockHeld) contentText += " (wake lock held)";
|
||||||
|
|
||||||
Notification.Builder builder = new Notification.Builder(this);
|
Notification.Builder builder = new Notification.Builder(this);
|
||||||
builder.setContentTitle(getText(R.string.application_name));
|
builder.setContentTitle(getText(R.string.application_name));
|
||||||
builder.setContentText(contentText);
|
builder.setContentText(contentText);
|
||||||
@@ -191,32 +224,48 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
builder.setOngoing(true);
|
builder.setOngoing(true);
|
||||||
|
|
||||||
// If holding a wake or wifi lock consider the notification of high priority since it's using power,
|
// If holding a wake or wifi lock consider the notification of high priority since it's using power,
|
||||||
// otherwise use a minimal priority since this is just a background service notification:
|
// otherwise use a low priority
|
||||||
builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN);
|
builder.setPriority((wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_LOW);
|
||||||
|
|
||||||
// No need to show a timestamp:
|
// No need to show a timestamp:
|
||||||
builder.setShowWhen(false);
|
builder.setShowWhen(false);
|
||||||
|
|
||||||
// Background color for small notification icon:
|
// Background color for small notification icon:
|
||||||
builder.setColor(0xFF000000);
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
builder.setChannelId(NOTIFICATION_CHANNEL_ID);
|
||||||
|
}
|
||||||
|
|
||||||
Resources res = getResources();
|
Resources res = getResources();
|
||||||
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
|
Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
|
||||||
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
|
builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
|
||||||
|
|
||||||
Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WAKE);
|
String newWakeAction = wakeLockHeld ? ACTION_UNLOCK_WAKE : ACTION_LOCK_WAKE;
|
||||||
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wakelock),
|
Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(newWakeAction);
|
||||||
PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
|
String actionTitle = res.getString(wakeLockHeld ?
|
||||||
|
R.string.notification_action_wake_unlock :
|
||||||
Intent toggleWifiLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WIFI);
|
R.string.notification_action_wake_lock);
|
||||||
builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wifilock),
|
int actionIcon = wakeLockHeld ? android.R.drawable.ic_lock_idle_lock : android.R.drawable.ic_lock_lock;
|
||||||
PendingIntent.getService(this, 0, toggleWifiLockIntent, 0));
|
builder.addAction(actionIcon, actionTitle, PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
|
File termuxTmpDir = new File(TermuxService.PREFIX_PATH + "/tmp");
|
||||||
|
|
||||||
|
if (termuxTmpDir.exists()) {
|
||||||
|
try {
|
||||||
|
TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(EmulatorDebug.LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
termuxTmpDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
if (mWakeLock != null) mWakeLock.release();
|
if (mWakeLock != null) mWakeLock.release();
|
||||||
if (mWifiLock != null) mWifiLock.release();
|
if (mWifiLock != null) mWifiLock.release();
|
||||||
|
|
||||||
@@ -224,7 +273,6 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
|
|
||||||
for (int i = 0; i < mTerminalSessions.size(); i++)
|
for (int i = 0; i < mTerminalSessions.size(); i++)
|
||||||
mTerminalSessions.get(i).finishIfRunning();
|
mTerminalSessions.get(i).finishIfRunning();
|
||||||
mTerminalSessions.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TerminalSession> getSessions() {
|
public List<TerminalSession> getSessions() {
|
||||||
@@ -236,48 +284,12 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
|
|
||||||
if (cwd == null) cwd = HOME_PATH;
|
if (cwd == null) cwd = HOME_PATH;
|
||||||
|
|
||||||
final String termEnv = "TERM=xterm-256color";
|
String[] env = BackgroundJob.buildEnvironment(failSafe, cwd);
|
||||||
final String homeEnv = "HOME=" + HOME_PATH;
|
boolean isLoginShell = false;
|
||||||
final String prefixEnv = "PREFIX=" + PREFIX_PATH;
|
|
||||||
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
|
|
||||||
final String androidDataEnv = "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.
|
|
||||||
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
|
|
||||||
String[] env;
|
|
||||||
if (failSafe) {
|
|
||||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
|
||||||
final String pathEnv = "PATH=" + System.getenv("PATH");
|
|
||||||
env = new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
|
|
||||||
} else {
|
|
||||||
final String ps1Env = "PS1=$ ";
|
|
||||||
final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
|
|
||||||
final String langEnv = "LANG=en_US.UTF-8";
|
|
||||||
final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets";
|
|
||||||
final String pwdEnv = "PWD=" + cwd;
|
|
||||||
|
|
||||||
env = new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv};
|
|
||||||
}
|
|
||||||
|
|
||||||
String shellName;
|
|
||||||
if (executablePath == null) {
|
if (executablePath == null) {
|
||||||
File shell = new File(HOME_PATH, ".termux/shell");
|
if (!failSafe) {
|
||||||
if (shell.exists()) {
|
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
||||||
try {
|
|
||||||
File canonicalFile = shell.getCanonicalFile();
|
|
||||||
if (canonicalFile.isFile() && canonicalFile.canExecute()) {
|
|
||||||
executablePath = canonicalFile.getName().equals("busybox") ? (PREFIX_PATH + "/bin/ash") : canonicalFile.getAbsolutePath();
|
|
||||||
} else {
|
|
||||||
Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath());
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (executablePath == null) {
|
|
||||||
// Try bash, zsh and ash in that order:
|
|
||||||
for (String shellBinary : new String[]{"bash", "zsh", "ash"}) {
|
|
||||||
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
|
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
|
||||||
if (shellFile.canExecute()) {
|
if (shellFile.canExecute()) {
|
||||||
executablePath = shellFile.getAbsolutePath();
|
executablePath = shellFile.getAbsolutePath();
|
||||||
@@ -290,27 +302,28 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
// Fall back to system shell as last resort:
|
// Fall back to system shell as last resort:
|
||||||
executablePath = "/system/bin/sh";
|
executablePath = "/system/bin/sh";
|
||||||
}
|
}
|
||||||
|
isLoginShell = true;
|
||||||
String[] parts = executablePath.split("/");
|
|
||||||
shellName = "-" + parts[parts.length - 1];
|
|
||||||
} else {
|
|
||||||
int lastSlashIndex = executablePath.lastIndexOf('/');
|
|
||||||
shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] args;
|
String[] processArgs = BackgroundJob.setupProcessArgs(executablePath, arguments);
|
||||||
if (arguments == null) {
|
executablePath = processArgs[0];
|
||||||
args = new String[]{shellName};
|
int lastSlashIndex = executablePath.lastIndexOf('/');
|
||||||
} else {
|
String processName = (isLoginShell ? "-" : "") +
|
||||||
args = new String[arguments.length + 1];
|
(lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1));
|
||||||
args[0] = shellName;
|
|
||||||
|
|
||||||
System.arraycopy(arguments, 0, args, 1, arguments.length);
|
String[] args = new String[processArgs.length];
|
||||||
}
|
args[0] = processName;
|
||||||
|
if (processArgs.length > 1) System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);
|
||||||
|
|
||||||
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
|
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
|
||||||
mTerminalSessions.add(session);
|
mTerminalSessions.add(session);
|
||||||
updateNotification();
|
updateNotification();
|
||||||
|
|
||||||
|
// Make sure that terminal styling is always applied.
|
||||||
|
Intent stylingIntent = new Intent("com.termux.app.reload_style");
|
||||||
|
stylingIntent.putExtra("com.termux.app.reload_style", "styling");
|
||||||
|
sendBroadcast(stylingIntent);
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,4 +371,23 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
if (mSessionChangeCallback != null) mSessionChangeCallback.onColorsChanged(session);
|
if (mSessionChangeCallback != null) mSessionChangeCallback.onColorsChanged(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onBackgroundJobExited(final BackgroundJob task) {
|
||||||
|
mHandler.post(() -> {
|
||||||
|
mBackgroundTasks.remove(task);
|
||||||
|
updateNotification();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||||
|
|
||||||
|
String channelName = "Termux";
|
||||||
|
String channelDescription = "Notifications from Termux";
|
||||||
|
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||||
|
|
||||||
|
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName,importance);
|
||||||
|
channel.setDescription(channelDescription);
|
||||||
|
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
manager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package com.termux.app;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.support.v4.widget.DrawerLayout;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@@ -13,18 +11,20 @@ import android.view.inputmethod.InputMethodManager;
|
|||||||
import com.termux.terminal.KeyHandler;
|
import com.termux.terminal.KeyHandler;
|
||||||
import com.termux.terminal.TerminalEmulator;
|
import com.termux.terminal.TerminalEmulator;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
import com.termux.view.TerminalKeyListener;
|
import com.termux.view.TerminalViewClient;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public final class TermuxKeyListener implements TerminalKeyListener {
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
|
||||||
|
public final class TermuxViewClient implements TerminalViewClient {
|
||||||
|
|
||||||
final TermuxActivity mActivity;
|
final TermuxActivity mActivity;
|
||||||
|
|
||||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||||
|
|
||||||
public TermuxKeyListener(TermuxActivity activity) {
|
public TermuxViewClient(TermuxActivity activity) {
|
||||||
this.mActivity = activity;
|
this.mActivity = activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,33 +55,14 @@ public final class TermuxKeyListener implements TerminalKeyListener {
|
|||||||
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void returnOnFinishedSession(TerminalSession currentSession) {
|
|
||||||
// Return pressed with finished session - remove it.
|
|
||||||
currentSession.finishIfRunning();
|
|
||||||
|
|
||||||
TermuxService service = mActivity.mTermService;
|
|
||||||
|
|
||||||
int index = service.removeTermSession(currentSession);
|
|
||||||
mActivity.mListViewAdapter.notifyDataSetChanged();
|
|
||||||
if (mActivity.mTermService.getSessions().isEmpty()) {
|
|
||||||
// There are no sessions to show, so finish the activity.
|
|
||||||
mActivity.finish();
|
|
||||||
} else {
|
|
||||||
if (index >= service.getSessions().size()) {
|
|
||||||
index = service.getSessions().size() - 1;
|
|
||||||
}
|
|
||||||
mActivity.switchToSession(service.getSessions().get(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
||||||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||||
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||||
returnOnFinishedSession(currentSession);
|
mActivity.removeFinishedSession(currentSession);
|
||||||
return true;
|
return true;
|
||||||
} else if (e.isCtrlPressed() && e.isShiftPressed()) {
|
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
||||||
// Get the unmodified code point:
|
// Get the unmodified code point:
|
||||||
int unicodeChar = e.getUnicodeChar(0);
|
int unicodeChar = e.getUnicodeChar(0);
|
||||||
|
|
||||||
@@ -93,8 +74,6 @@ public final class TermuxKeyListener implements TerminalKeyListener {
|
|||||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||||
mActivity.getDrawer().closeDrawers();
|
mActivity.getDrawer().closeDrawers();
|
||||||
} else if (unicodeChar == 'f'/* full screen */) {
|
|
||||||
mActivity.toggleImmersive();
|
|
||||||
} else if (unicodeChar == 'k'/* keyboard */) {
|
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||||
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||||
@@ -134,12 +113,12 @@ public final class TermuxKeyListener implements TerminalKeyListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean readControlKey() {
|
public boolean readControlKey() {
|
||||||
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readControlButton()) || mVirtualControlKeyDown;
|
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean readAltKey() {
|
public boolean readAltKey() {
|
||||||
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readAltButton());
|
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -180,7 +159,7 @@ public final class TermuxKeyListener implements TerminalKeyListener {
|
|||||||
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
||||||
break;
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME;
|
resultingCodePoint = '~';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Special characters to input.
|
// Special characters to input.
|
||||||
@@ -231,6 +210,7 @@ public final class TermuxKeyListener implements TerminalKeyListener {
|
|||||||
|
|
||||||
// Writing mode:
|
// Writing mode:
|
||||||
case 'q':
|
case 'q':
|
||||||
|
case 'k':
|
||||||
mActivity.toggleShowExtraKeys();
|
mActivity.toggleShowExtraKeys();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -244,15 +224,16 @@ public final class TermuxKeyListener implements TerminalKeyListener {
|
|||||||
return true;
|
return true;
|
||||||
} else if (ctrlDown) {
|
} else if (ctrlDown) {
|
||||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||||
returnOnFinishedSession(session);
|
mActivity.removeFinishedSession(session);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts;
|
List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts;
|
||||||
if (!shortcuts.isEmpty()) {
|
if (!shortcuts.isEmpty()) {
|
||||||
|
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||||
TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i);
|
TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i);
|
||||||
if (codePoint == shortcut.codePoint) {
|
if (codePointLowerCase == shortcut.codePoint) {
|
||||||
switch (shortcut.shortcutAction) {
|
switch (shortcut.shortcutAction) {
|
||||||
case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION:
|
case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION:
|
||||||
mActivity.addNewSession(false, null);
|
mActivity.addNewSession(false, null);
|
||||||
@@ -275,10 +256,17 @@ public final class TermuxKeyListener implements TerminalKeyListener {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onLongPress(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||||
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||||
InputDevice inputDevice = event.getDevice();
|
InputDevice inputDevice = event.getDevice();
|
||||||
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
if (mActivity.mSettings.mDisableVolumeVirtualKeys) {
|
||||||
|
return false;
|
||||||
|
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||||
// Do not steal dedicated buttons from a full external keyboard.
|
// Do not steal dedicated buttons from a full external keyboard.
|
||||||
return false;
|
return false;
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||||
@@ -71,7 +71,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
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());
|
||||||
@@ -117,6 +117,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);
|
||||||
@@ -169,6 +192,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.
|
||||||
@@ -195,7 +223,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
final String name = file.getName();
|
final String name = file.getName();
|
||||||
final int lastDot = name.lastIndexOf('.');
|
final int lastDot = name.lastIndexOf('.');
|
||||||
if (lastDot >= 0) {
|
if (lastDot >= 0) {
|
||||||
final String extension = name.substring(lastDot + 1);
|
final String extension = name.substring(lastDot + 1).toLowerCase();
|
||||||
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
if (mime != null) return mime;
|
if (mime != null) return mime;
|
||||||
}
|
}
|
||||||
@@ -220,10 +248,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);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.termux.filepicker;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@@ -22,6 +21,7 @@ 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 {
|
||||||
|
|
||||||
@@ -37,6 +37,11 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
*/
|
*/
|
||||||
boolean mFinishOnDismissNameDialog = true;
|
boolean mFinishOnDismissNameDialog = true;
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -51,7 +56,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);
|
||||||
@@ -83,17 +88,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
|
|
||||||
void showErrorDialogAndQuit(String message) {
|
void showErrorDialogAndQuit(String message) {
|
||||||
mFinishOnDismissNameDialog = false;
|
mFinishOnDismissNameDialog = false;
|
||||||
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(new DialogInterface.OnDismissListener() {
|
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(dialog -> finish()).setPositiveButton(android.R.string.ok, (dialog, which) -> finish()).show();
|
||||||
@Override
|
|
||||||
public void onDismiss(DialogInterface dialog) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleContentUri(final Uri uri, String subjectFromIntent) {
|
void handleContentUri(final Uri uri, String subjectFromIntent) {
|
||||||
@@ -119,54 +114,40 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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, new DialogUtils.TextSetListener() {
|
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, text -> {
|
||||||
@Override
|
File outFile = saveStreamWithName(in, text);
|
||||||
public void onTextSet(String text) {
|
if (outFile == null) return;
|
||||||
File outFile = saveStreamWithName(in, text);
|
|
||||||
if (outFile == null) return;
|
|
||||||
|
|
||||||
final File editorProgramFile = new File(EDITOR_PROGRAM);
|
final File editorProgramFile = new File(EDITOR_PROGRAM);
|
||||||
if (!editorProgramFile.isFile()) {
|
if (!editorProgramFile.isFile()) {
|
||||||
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
|
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-file-editor\n\n"
|
||||||
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
|
+ "Create this file as a script or a symlink - it will be called with the received file as only argument.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do this for the user if necessary:
|
// Do this for the user if necessary:
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
editorProgramFile.setExecutable(true);
|
editorProgramFile.setExecutable(true);
|
||||||
|
|
||||||
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(TermuxService.ACTION_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(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||||
startService(executeIntent);
|
startService(executeIntent);
|
||||||
finish();
|
finish();
|
||||||
}
|
},
|
||||||
|
R.string.file_received_open_folder_button, text -> {
|
||||||
|
if (saveStreamWithName(in, text) == null) return;
|
||||||
|
|
||||||
|
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
|
||||||
|
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
|
||||||
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
|
startService(executeIntent);
|
||||||
|
finish();
|
||||||
},
|
},
|
||||||
R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
|
android.R.string.cancel, text -> finish(), dialog -> {
|
||||||
@Override
|
if (mFinishOnDismissNameDialog) finish();
|
||||||
public void onTextSet(String text) {
|
|
||||||
if (saveStreamWithName(in, text) == null) return;
|
|
||||||
|
|
||||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
|
|
||||||
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
|
|
||||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
|
||||||
startService(executeIntent);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
android.R.string.cancel, new DialogUtils.TextSetListener() {
|
|
||||||
@Override
|
|
||||||
public void onTextSet(final String text) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}, new DialogInterface.OnDismissListener() {
|
|
||||||
@Override
|
|
||||||
public void onDismiss(DialogInterface dialog) {
|
|
||||||
if (mFinishOnDismissNameDialog) finish();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.termux.terminal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes effects, foreground and background colors into a 32 bit integer, which are stored for each cell in a terminal
|
|
||||||
* row in {@link TerminalRow#mStyle}.
|
|
||||||
* <p/>
|
|
||||||
* The foreground and background colors take 9 bits each, leaving (32-9-9)=14 bits for effect flags. Using 9 for now
|
|
||||||
* (the different CHARACTER_ATTRIBUTE_* bits).
|
|
||||||
*/
|
|
||||||
public final class TextStyle {
|
|
||||||
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_BOLD = 1;
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1;
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2;
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3;
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4;
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5;
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
|
|
||||||
/**
|
|
||||||
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
|
|
||||||
* <p/>
|
|
||||||
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
|
|
||||||
* come after it as erasable from the screen.
|
|
||||||
*/
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
|
|
||||||
/** Dim colors. Also known as faint or half intensity. */
|
|
||||||
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
|
|
||||||
|
|
||||||
public final static int COLOR_INDEX_FOREGROUND = 256;
|
|
||||||
public final static int COLOR_INDEX_BACKGROUND = 257;
|
|
||||||
public final static int COLOR_INDEX_CURSOR = 258;
|
|
||||||
|
|
||||||
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
|
|
||||||
public final static int NUM_INDEXED_COLORS = 259;
|
|
||||||
|
|
||||||
/** Normal foreground and background colors and no effects. */
|
|
||||||
final static int NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
|
|
||||||
|
|
||||||
static int encode(int foreColor, int backColor, int effect) {
|
|
||||||
return ((effect & 0b111111111) << 18) | ((foreColor & 0b111111111) << 9) | (backColor & 0b111111111);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int decodeForeColor(int encodedColor) {
|
|
||||||
return (encodedColor >> 9) & 0b111111111;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int decodeBackColor(int encodedColor) {
|
|
||||||
return encodedColor & 0b111111111;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int decodeEffect(int encodedColor) {
|
|
||||||
return (encodedColor >> 18) & 0b111111111;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package com.termux.terminal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* wcwidth() implementation from http://git.musl-libc.org/cgit/musl/tree/src/ctype
|
|
||||||
* <p/>
|
|
||||||
* Modified to return 0 instead of -1.
|
|
||||||
*/
|
|
||||||
public final class WcWidth {
|
|
||||||
|
|
||||||
private static final short table[] = {16, 16, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 16, 16, 32, 16, 16, 16, 33, 34, 35, 36, 37, 38,
|
|
||||||
39, 16, 16, 40, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 41, 42, 16, 16, 43, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 44, 16, 45, 46, 47, 48, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
49, 16, 16, 50, 51, 16, 52, 16, 16, 16, 16, 16, 16, 16, 16, 53, 16, 16, 16, 16, 16, 54, 55, 16, 16, 16, 16, 56, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 57, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 58, 59, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
248, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 191, 182, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 31, 0, 255, 7, 0, 0, 0, 0, 0, 248, 255, 255, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 191, 159, 61, 0, 0, 0, 128, 2, 0, 0, 0,
|
|
||||||
255, 255, 255, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 255, 1, 0, 0, 0, 0, 0, 0, 248, 15, 0, 0, 0, 192, 251, 239, 62, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 255, 255, 127, 7, 0, 0, 0, 0, 0, 0, 20, 254, 33, 254, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 16, 30, 32, 0,
|
|
||||||
0, 12, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 16, 134, 57, 2, 0, 0, 0, 35, 0, 6, 0, 0, 0, 0, 0, 0, 16, 190, 33, 0, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 144,
|
|
||||||
30, 32, 64, 0, 12, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 193, 61, 96, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 144, 64, 48, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 32, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 92, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 242, 7, 128, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 242, 27, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 160, 2, 0, 0, 0, 0, 0, 0, 254,
|
|
||||||
127, 223, 224, 255, 254, 255, 255, 255, 31, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 253, 102, 0, 0, 0, 195, 1, 0, 30, 0, 100, 32, 0, 32, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0,
|
|
||||||
28, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 176, 63, 64, 254, 15, 32, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 1, 4, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
128, 1, 0, 0, 0, 0, 0, 0, 64, 127, 229, 31, 248, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 208, 23, 4, 0, 0, 0, 0,
|
|
||||||
248, 15, 0, 3, 0, 0, 0, 60, 11, 0, 0, 0, 0, 0, 0, 64, 163, 3, 0, 0, 0, 0, 0, 0, 240, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
247, 255, 253, 33, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 127, 0, 0, 240, 0, 248, 0, 0,
|
|
||||||
0, 124, 0, 0, 0, 0, 0, 0, 31, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255,
|
|
||||||
255, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128,
|
|
||||||
247, 63, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 68, 8, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0,
|
|
||||||
255, 255, 3, 0, 0, 0, 0, 0, 192, 63, 0, 0, 128, 255, 3, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 200, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 102,
|
|
||||||
0, 8, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 193, 2, 0, 0, 0, 0, 48, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 32, 33, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0,
|
|
||||||
127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 110, 240, 0,
|
|
||||||
0, 0, 0, 0, 135, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 255, 127, 0, 0, 0, 0, 0, 0, 0, 3, 0,
|
|
||||||
0, 0, 0, 0, 120, 38, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 128, 239, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 192, 127, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 128, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 248, 255, 231, 15, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,};
|
|
||||||
|
|
||||||
private static final short wtable[] = {16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 18, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 19, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 21, 22, 23, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
|
||||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
|
||||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
|
||||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 26, 16, 16, 16, 16, 27, 16, 16, 17, 17, 17, 17, 17,
|
|
||||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
|
||||||
17, 28, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17,
|
|
||||||
16, 16, 16, 29, 30, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 31, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 32, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
|
||||||
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 251, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 63, 0, 0, 0, 255, 15, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 224, 255, 255, 255, 255, 63, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255,
|
|
||||||
255, 255, 255, 7, 255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
|
||||||
255, 31, 255, 255, 255, 255, 255, 255, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15, 0, 255, 255, 127, 248,
|
|
||||||
255, 255, 255, 255, 255, 15, 0, 0, 255, 3, 0, 0, 255, 255, 255, 255, 247, 255, 127, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254,
|
|
||||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 255, 255, 255, 255, 255, 7, 255, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
||||||
|
|
||||||
/** Return the terminal display width of a code point: 0, 1 or 2. */
|
|
||||||
public static int width(int wc) {
|
|
||||||
if (wc < 0xff) return (wc + 1 & 0x7f) >= 0x21 ? 1 : (wc != 0) ? 0 : 0;
|
|
||||||
if ((wc & 0xfffeffff) < 0xfffe) {
|
|
||||||
if (((table[table[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 0;
|
|
||||||
if (((wtable[wtable[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 2;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if ((wc & 0xfffe) == 0xfffe) return 0;
|
|
||||||
if (wc - 0x20000 < 0x20000) return 2;
|
|
||||||
if (wc == 0xe0001 || wc - 0xe0020 < 0x5f || wc - 0xe0100 < 0xef) return 0;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The width at an index position in a java char array. */
|
|
||||||
public static int width(char[] chars, int index) {
|
|
||||||
char c = chars[index];
|
|
||||||
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
5
app/src/main/res/drawable-anydpi-v26/ic_launcher.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>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 695 B |
|
Before Width: | Height: | Size: 786 B |
|
Before Width: | Height: | Size: 597 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 779 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 983 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||||
<solid android:color="#E0E0E0" />
|
<solid android:color="#E0E0E0" />
|
||||||
</shape>
|
</shape>
|
||||||
|
|||||||
4
app/src/main/res/drawable/current_session_black.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||||
|
<solid android:color="#212325" />
|
||||||
|
</shape>
|
||||||
28
app/src/main/res/drawable/ic_foreground.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
|
||||||
|
<!-- Keep in sync with non-adaptive ic_launcher.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>
|
||||||
34
app/src/main/res/drawable/ic_launcher.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<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>
|
||||||
17
app/src/main/res/drawable/ic_new_session.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:pathData="M 12, 12
|
||||||
|
m -10.5, 0
|
||||||
|
a 10.5,10.5 0 1,0 21,0
|
||||||
|
a 10.5,10.5 0 1,0 -21,0"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
|
||||||
|
</vector>
|
||||||
40
app/src/main/res/drawable/ic_service_notification.xml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<!--
|
||||||
|
https://material.google.com/style/icons.html
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- A circle as the icon border. -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#FFF"
|
||||||
|
android:strokeWidth="3"
|
||||||
|
android:pathData="M18,54
|
||||||
|
A36,36 0 1,1 90,54
|
||||||
|
A36,36 0 1,1 18,54 Z" />
|
||||||
|
|
||||||
|
<!-- Keep in sync with 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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_activated="true" android:drawable="@drawable/current_session_black"/>
|
||||||
|
<item android:state_activated="false" android:drawable="@drawable/session_ripple_black"/>
|
||||||
|
</selector>
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
<item>
|
<item>
|
||||||
<color android:color="@android:color/white" />
|
<color android:color="@android:color/white" />
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
||||||
|
|||||||
7
app/src/main/res/drawable/session_ripple_black.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@android:color/darker_gray" >
|
||||||
|
<item>
|
||||||
|
<color android:color="@android:color/background_dark" />
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:src="@drawable/text_select_handle_left_mtrl_alpha"
|
|
||||||
android:tint="#2196F3" />
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:src="@drawable/text_select_handle_right_mtrl_alpha"
|
|
||||||
android:tint="#2196F3" />
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<android.support.v4.widget.DrawerLayout
|
<androidx.drawerlayout.widget.DrawerLayout
|
||||||
android:id="@+id/drawer_layout"
|
android:id="@+id/drawer_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
android:id="@+id/terminal_view"
|
android:id="@+id/terminal_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="3dp"
|
||||||
|
android:layout_marginLeft="3dp"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||||
android:scrollbars="vertical" />
|
android:scrollbars="vertical" />
|
||||||
@@ -63,13 +66,13 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</android.support.v4.widget.DrawerLayout>
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
|
|
||||||
<android.support.v4.view.ViewPager
|
<androidx.viewpager.widget.ViewPager
|
||||||
android:id="@+id/viewpager"
|
android:id="@+id/viewpager"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="40dp"
|
android:layout_height="37.5dp"
|
||||||
android:background="@android:drawable/screen_background_dark_transparent"
|
android:background="@android:drawable/screen_background_dark_transparent"
|
||||||
android:layout_alignParentBottom="true" />
|
android:layout_alignParentBottom="true" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
android:imeOptions="actionSend|flagNoFullscreen"
|
android:imeOptions="actionSend|flagNoFullscreen"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:singleLine="true"
|
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
|
android:textColorHighlight="@android:color/darker_gray"
|
||||||
android:paddingTop="0dp"
|
android:paddingTop="0dp"
|
||||||
android:textCursorDrawable="@null"
|
android:textCursorDrawable="@null"
|
||||||
android:paddingBottom="0dp"
|
android:paddingBottom="0dp"
|
||||||
|
|||||||
@@ -1,60 +1,53 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="application_name">Termux</string>
|
<string name="application_name">Termux</string>
|
||||||
<string name="shared_user_label">Termux user</string>
|
<string name="shared_user_label">Termux user</string>
|
||||||
<string name="new_session">New session</string>
|
<string name="run_command_permission_label">Run commands in Termux environment</string>
|
||||||
<string name="new_session_failsafe">Failsafe</string>
|
<string name="run_command_permission_description">Allow third-party applications to execute arbitrary commands within Termux environment.</string>
|
||||||
<string name="toggle_soft_keyboard">Keyboard</string>
|
<string name="new_session">New session</string>
|
||||||
<string name="reset_terminal">Reset</string>
|
<string name="new_session_failsafe">Failsafe</string>
|
||||||
<string name="style_terminal">Style</string>
|
<string name="toggle_soft_keyboard">Keyboard</string>
|
||||||
<string name="toggle_fullscreen">Fullscreen</string>
|
<string name="reset_terminal">Reset</string>
|
||||||
<string name="share_transcript_title">Terminal transcript</string>
|
<string name="style_terminal">Style</string>
|
||||||
<string name="help">Help</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="welcome_dialog_title">Welcome to Termux</string>
|
<string name="bootstrap_installer_body">Installing…</string>
|
||||||
<string name="welcome_dialog_body">Long press and select <i>More…</i> to show a menu where <i>Help</i> is available.\n\nExecute <b>apt update</b> to update the packages list before installing packages.</string>
|
<string name="bootstrap_error_title">Unable to install</string>
|
||||||
<string name="welcome_dialog_dont_show_again_button">Do not show again</string>
|
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.</string>
|
||||||
|
<string name="bootstrap_error_abort">Abort</string>
|
||||||
|
<string name="bootstrap_error_try_again">Try again</string>
|
||||||
|
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
|
||||||
|
|
||||||
<string name="bootstrap_installer_body">Installing…</string>
|
<string name="max_terminals_reached_title">Max terminals reached</string>
|
||||||
<string name="bootstrap_error_title">Unable to install</string>
|
<string name="max_terminals_reached_message">Close down existing ones before creating new.</string>
|
||||||
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.\n\nCheck your network connection and try again.</string>
|
|
||||||
<string name="bootstrap_error_abort">Abort</string>
|
|
||||||
<string name="bootstrap_error_try_again">Try again</string>
|
|
||||||
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
|
|
||||||
|
|
||||||
<string name="max_terminals_reached_title">Max terminals reached</string>
|
<string name="reset_toast_notification">Terminal reset.</string>
|
||||||
<string name="max_terminals_reached_message">Close down existing ones before creating new.</string>
|
|
||||||
|
|
||||||
<string name="reset_toast_notification">Terminal reset.</string>
|
<string name="select_url">Select URL</string>
|
||||||
|
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
||||||
|
<string name="select_all_and_share">Share transcript</string>
|
||||||
|
<string name="select_url_no_found">No URL found in the terminal.</string>
|
||||||
|
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
||||||
|
<string name="share_transcript_chooser_title">Send text to:</string>
|
||||||
|
|
||||||
<string name="select_url">Select URL</string>
|
<string name="kill_process">Kill process (%d)</string>
|
||||||
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
<string name="confirm_kill_process">Really kill this session?</string>
|
||||||
<string name="select_all_and_share">Share transcript</string>
|
|
||||||
<string name="select_url_no_found">No URL found in the terminal.</string>
|
|
||||||
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
|
||||||
<string name="share_transcript_chooser_title">Send text to:</string>
|
|
||||||
|
|
||||||
<string name="paste_text">Paste</string>
|
<string name="session_rename_title">Set session name</string>
|
||||||
<string name="copy_text">Copy</string>
|
<string name="session_rename_positive_button">Set</string>
|
||||||
<string name="text_selection_more">More…</string>
|
<string name="session_new_named_title">New named session</string>
|
||||||
|
<string name="session_new_named_positive_button">Create</string>
|
||||||
|
|
||||||
<string name="kill_process">Kill process (%d)</string>
|
<string name="styling_not_installed">The Termux:Style add-on is not installed.</string>
|
||||||
<string name="confirm_kill_process">Really kill this session?</string>
|
<string name="styling_install">Install</string>
|
||||||
|
|
||||||
<string name="session_rename_title">Set session name</string>
|
<string name="notification_action_exit">Exit</string>
|
||||||
<string name="session_rename_positive_button">Set</string>
|
<string name="notification_action_wake_lock">Acquire wakelock</string>
|
||||||
<string name="session_new_named_title">New named session</string>
|
<string name="notification_action_wake_unlock">Release wakelock</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="styling_install">Install</string>
|
|
||||||
|
|
||||||
<string name="notification_action_exit">Exit</string>
|
|
||||||
<string name="notification_action_wakelock">Wake</string>
|
|
||||||
<string name="notification_action_wifilock">Wifi</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>
|
|
||||||
|
|
||||||
|
<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>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<!-- 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. -->
|
|
||||||
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
|
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
|
||||||
<item name="android:statusBarColor">#000000</item>
|
<item name="android:statusBarColor">#000000</item>
|
||||||
|
<item name="android:colorPrimary">#FF000000</item>
|
||||||
<item name="android:windowBackground">@android:color/black</item>
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
|
|
||||||
<!-- Seen in buttons on left drawer: -->
|
<!-- Seen in buttons on left drawer: -->
|
||||||
@@ -13,11 +12,38 @@
|
|||||||
<!-- Avoid action mode toolbar pushing down terminal content when
|
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||||
selecting text on pre-6.0 (non-floating toolbar). -->
|
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||||
<item name="android:windowActionModeOverlay">true</item>
|
<item name="android:windowActionModeOverlay">true</item>
|
||||||
|
|
||||||
|
<item name="android:windowTranslucentStatus">true</item>
|
||||||
|
<item name="android:windowTranslucentNavigation">true</item>
|
||||||
|
|
||||||
|
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
|
||||||
|
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||||
|
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||||
<!-- Seen in buttons on alert dialog: -->
|
<!-- Seen in buttons on alert dialog: -->
|
||||||
<item name="android:colorAccent">#212121</item>
|
<item name="android:colorAccent">#212121</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
||||||
|
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||||
|
<style name="Theme.Termux.Black" parent="@android:style/Theme.Material.NoActionBar">
|
||||||
|
<item name="android:statusBarColor">#000000</item>
|
||||||
|
<item name="android:colorPrimary">#FF000000</item>
|
||||||
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
|
|
||||||
|
<!-- Seen in buttons on left drawer: -->
|
||||||
|
<item name="android:colorAccent">#FDFDFD</item>
|
||||||
|
<!-- Avoid action mode toolbar pushing down terminal content when
|
||||||
|
selecting text on pre-6.0 (non-floating toolbar). -->
|
||||||
|
<item name="android:windowActionModeOverlay">true</item>
|
||||||
|
|
||||||
|
<item name="android:windowTranslucentStatus">true</item>
|
||||||
|
<item name="android:windowTranslucentNavigation">true</item>
|
||||||
|
|
||||||
|
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
|
||||||
|
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||||
|
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<full-backup-content>
|
|
||||||
<!-- See https://developer.android.com/training/backup/autosyncapi.html -->
|
|
||||||
<include domain="file" path="home/backup" />
|
|
||||||
</full-backup-content>
|
|
||||||
30
app/src/main/res/xml/shortcuts.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<shortcuts xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<shortcut
|
||||||
|
android:shortcutId="new_session"
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_new_session"
|
||||||
|
android:shortcutShortLabel="@string/new_session"
|
||||||
|
tools:targetApi="n_mr1">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.RUN"
|
||||||
|
android:targetPackage="com.termux"
|
||||||
|
android:targetClass="com.termux.app.TermuxActivity"/>
|
||||||
|
</shortcut>
|
||||||
|
|
||||||
|
<shortcut
|
||||||
|
android:shortcutId="new_failsafe_session"
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_new_session"
|
||||||
|
android:shortcutShortLabel="@string/new_session_failsafe"
|
||||||
|
tools:targetApi="n_mr1">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.RUN"
|
||||||
|
android:targetPackage="com.termux"
|
||||||
|
android:targetClass="com.termux.app.TermuxActivity">
|
||||||
|
<extra android:name="com.termux.app.failsafe_session" android:value="true" />
|
||||||
|
</intent>
|
||||||
|
</shortcut>
|
||||||
|
|
||||||
|
</shortcuts>
|
||||||
@@ -1,25 +1,30 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
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, TermuxActivity.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.termux.terminal;
|
|
||||||
|
|
||||||
/** "\033[" is the Control Sequence Introducer char sequence (CSI). */
|
|
||||||
public class ControlSequenceIntroducerTest extends TerminalTestCase {
|
|
||||||
|
|
||||||
/** CSI Ps P Scroll down Ps lines (default = 1) (SD). */
|
|
||||||
public void testCsiT() {
|
|
||||||
withTerminalSized(4, 6).enterString("1\r\n2\r\n3\r\nhi\033[2Tyo\r\nA\r\nB").assertLinesAre(" ", " ", "1 ", "2 yo", "A ",
|
|
||||||
"Bi ");
|
|
||||||
// Default value (1):
|
|
||||||
withTerminalSized(4, 6).enterString("1\r\n2\r\n3\r\nhi\033[Tyo\r\nA\r\nB").assertLinesAre(" ", "1 ", "2 ", "3 yo", "Ai ",
|
|
||||||
"B ");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** CSI Ps S Scroll up Ps lines (default = 1) (SU). */
|
|
||||||
public void testCsiS() {
|
|
||||||
// The behaviour here is a bit inconsistent between terminals - this is how the OS X Terminal.app does it:
|
|
||||||
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[2Sy").assertLinesAre("3 ", "hi ", " ", " y");
|
|
||||||
// Default value (1):
|
|
||||||
withTerminalSized(3, 4).enterString("1\r\n2\r\n3\r\nhi\033[Sy").assertLinesAre("2 ", "3 ", "hi ", " y");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** CSI Ps X Erase Ps Character(s) (default = 1) (ECH). */
|
|
||||||
public void testCsiX() {
|
|
||||||
// See https://code.google.com/p/chromium/issues/detail?id=212712 where test was extraced from.
|
|
||||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[X").assertLinesAre("abcdefg ijkl ", " ");
|
|
||||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[1X").assertLinesAre("abcdefg ijkl ", " ");
|
|
||||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[2X").assertLinesAre("abcdefg jkl ", " ");
|
|
||||||
withTerminalSized(13, 2).enterString("abcdefghijkl\b\b\b\b\b\033[20X").assertLinesAre("abcdefg ", " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
13
art/copy-to-other-apps.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e -u
|
||||||
|
|
||||||
|
for APP in api boot styling tasker widget; do
|
||||||
|
APPDIR=../../termux-$APP
|
||||||
|
for file in ic_foreground ic_launcher; do
|
||||||
|
cp ../app/src/main/res/drawable/$file.xml \
|
||||||
|
$APPDIR/app/src/main/res/drawable/$file.xml
|
||||||
|
done
|
||||||
|
|
||||||
|
cp ../app/src/main/res/drawable-anydpi-v26/ic_launcher.xml \
|
||||||
|
$APPDIR/app/src/main/res/drawable-anydpi-v26/$file.xml
|
||||||
|
done
|
||||||
30
art/feature-graphic.svg
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||||
|
<!--
|
||||||
|
This is a feature graphic:
|
||||||
|
https://support.google.com/googleplay/android-developer/answer/1078870
|
||||||
|
- 1024px by 500px, no alpha
|
||||||
|
- Don't include any copy or important visual information near the borders of the asset,
|
||||||
|
specifically near the bottom third of the frame.
|
||||||
|
- Try to center align any logo/copy information in the vertical and horizontal center of the frame.
|
||||||
|
- If adding text, use large font sizes.
|
||||||
|
- Your graphic may be displayed alone without the app icon.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 1024 500">
|
||||||
|
|
||||||
|
<rect fill="#0" width="100%" height="100%" />
|
||||||
|
|
||||||
|
<text id="shell_prompt"
|
||||||
|
x="130"
|
||||||
|
y="330"
|
||||||
|
style="fill: #ffffff; font-size: 124px; font-family: Menlo;">
|
||||||
|
<!--
|
||||||
|
<tspan>$</tspan>
|
||||||
|
<tspan x="290">Termux</tspan>
|
||||||
|
<tspan x="734">█</tspan>
|
||||||
|
-->
|
||||||
|
<tspan>$ Termux █</tspan>
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 962 B |
23
art/generate-big-icon.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e -u
|
||||||
|
|
||||||
|
echo "Generating ~/termux-icons/ic_launcher.png..."
|
||||||
|
mkdir -p ~/termux-icons/
|
||||||
|
|
||||||
|
vector2svg ../app/src/main/res/drawable/ic_launcher.xml ~/termux-icons/ic_launcher.svg
|
||||||
|
|
||||||
|
sed -i "" 's/viewBox="0 0 108 108"/viewBox="18 18 72 72"/' ~/termux-icons/ic_launcher.svg
|
||||||
|
|
||||||
|
SIZE=512
|
||||||
|
rsvg-convert \
|
||||||
|
-w $SIZE \
|
||||||
|
-h $SIZE \
|
||||||
|
-o ~/termux-icons/ic_launcher_$SIZE.png \
|
||||||
|
~/termux-icons/ic_launcher.svg
|
||||||
|
|
||||||
|
rsvg-convert \
|
||||||
|
-b black \
|
||||||
|
-w $SIZE \
|
||||||
|
-h $SIZE \
|
||||||
|
-o ~/termux-icons/ic_launcher_square_$SIZE.png \
|
||||||
|
~/termux-icons/ic_launcher.svg
|
||||||
5
art/generate-feature-graphic.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Generating feature graphics to ~/termux-icons/termux-feature-graphic.png..."
|
||||||
|
mkdir -p ~/termux-icons/
|
||||||
|
rsvg-convert feature-graphic.svg > ~/termux-icons/feature-graphic.png
|
||||||
20
art/generate-launcher-images.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
for DENSITY in mdpi hdpi xhdpi xxhdpi xxxhdpi; do
|
||||||
|
case $DENSITY in
|
||||||
|
mdpi) SIZE=48;;
|
||||||
|
hdpi) SIZE=72;;
|
||||||
|
xhdpi) SIZE=96;;
|
||||||
|
xxhdpi) SIZE=144;;
|
||||||
|
xxxhdpi) SIZE=192;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
FOLDER=../app/src/main/res/mipmap-$DENSITY
|
||||||
|
mkdir -p $FOLDER
|
||||||
|
|
||||||
|
for FILE in ic_launcher ic_launcher_round; do
|
||||||
|
PNG=$FOLDER/$FILE.png
|
||||||
|
rsvg-convert -w $SIZE -h $SIZE $FILE.svg > $PNG
|
||||||
|
zopflipng -y $PNG $PNG
|
||||||
|
done
|
||||||
|
done
|
||||||
9
art/generate-tv-banner.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Generating feature graphics to ~/termux-icons/termux-feature-graphic.png..."
|
||||||
|
mkdir -p ~/termux-icons/
|
||||||
|
|
||||||
|
# The Android TV banner on google play (1280x720) has same aspect ratio
|
||||||
|
# as the banner in the app (320x180).
|
||||||
|
rsvg-convert -w 1280 -h 720 tv-banner.svg > ~/termux-icons/tv-banner.png
|
||||||
|
rsvg-convert -w 320 -h 180 tv-banner.svg > ../app/src/main/res/drawable/banner.png
|
||||||
26
art/ic_launcher.svg
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||||
|
|
||||||
|
<!-- Screen and border. -->
|
||||||
|
<path fill="#000"
|
||||||
|
stroke="#BFCBCD"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9,6
|
||||||
|
l30,0
|
||||||
|
q3 0,3 3
|
||||||
|
l0,30
|
||||||
|
q0 3, -3 3
|
||||||
|
l-30,0
|
||||||
|
q-3 0, -3-3
|
||||||
|
l0 -30
|
||||||
|
q0 -3, 3 -3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Block cursor. -->
|
||||||
|
<path fill="#FFF"
|
||||||
|
d="M14,14
|
||||||
|
l5,0
|
||||||
|
l0,10
|
||||||
|
l-5,0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 512 B |
23
art/tv-banner.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||||
|
<!--
|
||||||
|
This is a tv banner graphic:
|
||||||
|
https://developer.android.com/design/tv/patterns.html#banner
|
||||||
|
- Size: 320 x 180 px, xhdpi resource in app
|
||||||
|
- Size: 1280 x 720 in google play.
|
||||||
|
- Text must be included in the image. If your app is available in more
|
||||||
|
than one language, you must provide versions of the banner image for each supported language.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 1280 720">
|
||||||
|
|
||||||
|
<rect fill="#0" width="100%" height="100%" />
|
||||||
|
|
||||||
|
<text id="shell_prompt"
|
||||||
|
x="200"
|
||||||
|
y="410"
|
||||||
|
style="fill: #ffffff; font-size: 210px; font-family: Menlo, Monospace;">
|
||||||
|
<tspan>Termux ▌</tspan>
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 736 B |
@@ -1,19 +1,16 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:2.1.2'
|
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
|
||||||
// in the individual module build.gradle files
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,5 @@
|
|||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# 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
|
||||||
#Fri May 13 01:11:09 CEST 2016
|
|
||||||
org.gradle.jvmargs=-Xmx2048M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
android.useDeprecatedNdk=true
|
android.useAndroidX=true
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
|||||||
#Sat Jul 23 17:08:29 CEST 2016
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip
|
|
||||||
|
|||||||
65
gradlew
vendored
@@ -1,4 +1,20 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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.
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
##
|
||||||
@@ -28,16 +44,16 @@ 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=""
|
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"
|
||||||
|
|
||||||
warn ( ) {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
die ( ) {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
@@ -109,8 +125,8 @@ 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"`
|
||||||
@@ -138,27 +154,30 @@ 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
|
||||||
|
|
||||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
# Escape application args
|
||||||
function splitJvmOpts() {
|
save () {
|
||||||
JVM_OPTS=("$@")
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
}
|
}
|
||||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
APP_ARGS=`save "$@"`
|
||||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
|
||||||
|
|
||||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
# 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"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
|
|||||||
103
gradlew.bat
vendored
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
77
scripts/bintray-publish.gradle
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
|
||||||
|
group = publishedGroupId // Maven Group ID for the artifact
|
||||||
|
install {
|
||||||
|
repositories.mavenInstaller {
|
||||||
|
pom {
|
||||||
|
project {
|
||||||
|
packaging 'aar'
|
||||||
|
groupId publishedGroupId
|
||||||
|
artifactId artifact
|
||||||
|
|
||||||
|
name libraryName
|
||||||
|
description libraryDescription
|
||||||
|
url siteUrl
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name 'GNU General Public License version 3'
|
||||||
|
url 'https://opensource.org/licenses/gpl-3.0.html'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id 'fornwall'
|
||||||
|
name 'Fredrik Fornwall'
|
||||||
|
email 'fredrik@fornwall.net'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
connection gitUrl
|
||||||
|
developerConnection gitUrl
|
||||||
|
url siteUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// End https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
|
||||||
|
|
||||||
|
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle
|
||||||
|
apply plugin: 'com.jfrog.bintray'
|
||||||
|
|
||||||
|
version = libraryVersion
|
||||||
|
|
||||||
|
task sourcesJar(type: Jar) {
|
||||||
|
classifier = 'sources'
|
||||||
|
from android.sourceSets.main.java.srcDirs
|
||||||
|
}
|
||||||
|
|
||||||
|
artifacts {
|
||||||
|
archives sourcesJar
|
||||||
|
}
|
||||||
|
|
||||||
|
bintray {
|
||||||
|
user = System.getenv('BINTRAY_USER')
|
||||||
|
key = System.getenv('BINTRAY_API_KEY')
|
||||||
|
|
||||||
|
configurations = ['archives']
|
||||||
|
pkg {
|
||||||
|
repo = 'maven'
|
||||||
|
name = bintrayName
|
||||||
|
userOrg = 'termux'
|
||||||
|
desc = libraryDescription
|
||||||
|
websiteUrl = siteUrl
|
||||||
|
vcsUrl = gitUrl
|
||||||
|
licenses = ['GPL-3.0']
|
||||||
|
publish = true
|
||||||
|
publicDownloadNumbers = true
|
||||||
|
version {
|
||||||
|
desc = libraryDescription
|
||||||
|
gpg {
|
||||||
|
sign = false //Determines whether to GPG sign the files. The default is false
|
||||||
|
// passphrase = properties.getProperty("bintray.gpg.password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
include ':app'
|
include ':app', ':terminal-emulator', ':terminal-view'
|
||||||
|
|||||||
67
terminal-emulator/build.gradle
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.jfrog.bintray" version "1.7.3"
|
||||||
|
id "com.github.dcendents.android-maven" version "2.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
bintrayName = 'terminal-emulator'
|
||||||
|
publishedGroupId = 'com.termux'
|
||||||
|
libraryName = 'TerminalEmulator'
|
||||||
|
artifact = 'terminal-emulator'
|
||||||
|
libraryDescription = 'The terminal emulator used in Termux'
|
||||||
|
siteUrl = 'https://github.com/termux/termux'
|
||||||
|
gitUrl = 'https://github.com/termux/termux.git'
|
||||||
|
libraryVersion = '0.52'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 28
|
||||||
|
ndkVersion '21.3.6528147'
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 28
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
path "src/main/jni/Android.mk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(Test) {
|
||||||
|
testLogging {
|
||||||
|
events "started", "passed", "skipped", "failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation 'junit:junit:4.13'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: '../scripts/bintray-publish.gradle'
|
||||||
25
terminal-emulator/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
2
terminal-emulator/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<manifest package="com.termux.terminal">
|
||||||
|
</manifest>
|
||||||
@@ -26,9 +26,9 @@ import static android.view.KeyEvent.KEYCODE_F7;
|
|||||||
import static android.view.KeyEvent.KEYCODE_F8;
|
import static android.view.KeyEvent.KEYCODE_F8;
|
||||||
import static android.view.KeyEvent.KEYCODE_F9;
|
import static android.view.KeyEvent.KEYCODE_F9;
|
||||||
import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
|
import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
|
||||||
import static android.view.KeyEvent.KEYCODE_HOME;
|
|
||||||
import static android.view.KeyEvent.KEYCODE_INSERT;
|
import static android.view.KeyEvent.KEYCODE_INSERT;
|
||||||
import static android.view.KeyEvent.KEYCODE_MOVE_END;
|
import static android.view.KeyEvent.KEYCODE_MOVE_END;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_MOVE_HOME;
|
||||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
|
||||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_1;
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_1;
|
||||||
import static android.view.KeyEvent.KEYCODE_NUMPAD_2;
|
import static android.view.KeyEvent.KEYCODE_NUMPAD_2;
|
||||||
@@ -66,7 +66,7 @@ public final class KeyHandler {
|
|||||||
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
|
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
|
||||||
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
|
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
|
||||||
TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
|
TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
|
||||||
TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_HOME); // Shifted home
|
TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_MOVE_HOME); // Shifted home
|
||||||
TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
|
TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
|
||||||
TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
|
TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ public final class KeyHandler {
|
|||||||
TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
|
TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
|
||||||
|
|
||||||
TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
|
TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
|
||||||
TERMCAP_TO_KEYCODE.put("kh", KEYCODE_HOME);
|
TERMCAP_TO_KEYCODE.put("kh", KEYCODE_MOVE_HOME);
|
||||||
TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
|
TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
|
||||||
TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
|
TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ public final class KeyHandler {
|
|||||||
// t_K3 <kPageUp> keypad page-up key
|
// t_K3 <kPageUp> keypad page-up key
|
||||||
// t_K4 <kEnd> keypad end key
|
// t_K4 <kEnd> keypad end key
|
||||||
// t_K5 <kPageDown> keypad page-down key
|
// t_K5 <kPageDown> keypad page-down key
|
||||||
TERMCAP_TO_KEYCODE.put("K1", KEYCODE_HOME);
|
TERMCAP_TO_KEYCODE.put("K1", KEYCODE_MOVE_HOME);
|
||||||
TERMCAP_TO_KEYCODE.put("K3", KEYCODE_PAGE_UP);
|
TERMCAP_TO_KEYCODE.put("K3", KEYCODE_PAGE_UP);
|
||||||
TERMCAP_TO_KEYCODE.put("K4", KEYCODE_MOVE_END);
|
TERMCAP_TO_KEYCODE.put("K4", KEYCODE_MOVE_END);
|
||||||
TERMCAP_TO_KEYCODE.put("K5", KEYCODE_PAGE_DOWN);
|
TERMCAP_TO_KEYCODE.put("K5", KEYCODE_PAGE_DOWN);
|
||||||
@@ -162,7 +162,9 @@ public final class KeyHandler {
|
|||||||
case KEYCODE_DPAD_LEFT:
|
case KEYCODE_DPAD_LEFT:
|
||||||
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
|
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
|
||||||
|
|
||||||
case KEYCODE_HOME:
|
case KEYCODE_MOVE_HOME:
|
||||||
|
// Note that KEYCODE_HOME is handled by the system and never delivered to applications.
|
||||||
|
// On a Logitech k810 keyboard KEYCODE_MOVE_HOME is sent by FN+LeftArrow.
|
||||||
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
|
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
|
||||||
case KEYCODE_MOVE_END:
|
case KEYCODE_MOVE_END:
|
||||||
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
|
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
|
||||||
@@ -217,9 +219,6 @@ public final class KeyHandler {
|
|||||||
case KEYCODE_FORWARD_DEL:
|
case KEYCODE_FORWARD_DEL:
|
||||||
return transformForModifiers("\033[3", keyMode, '~');
|
return transformForModifiers("\033[3", keyMode, '~');
|
||||||
|
|
||||||
case KEYCODE_NUMPAD_DOT:
|
|
||||||
return keypadApplication ? "\033On" : "\033[3~";
|
|
||||||
|
|
||||||
case KEYCODE_PAGE_UP:
|
case KEYCODE_PAGE_UP:
|
||||||
return "\033[5~";
|
return "\033[5~";
|
||||||
case KEYCODE_PAGE_DOWN:
|
case KEYCODE_PAGE_DOWN:
|
||||||
@@ -249,12 +248,14 @@ public final class KeyHandler {
|
|||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
|
||||||
case KEYCODE_NUMPAD_COMMA:
|
case KEYCODE_NUMPAD_COMMA:
|
||||||
return ",";
|
return ",";
|
||||||
|
case KEYCODE_NUMPAD_DOT:
|
||||||
|
return keypadApplication ? "\033On" : ".";
|
||||||
case KEYCODE_NUMPAD_SUBTRACT:
|
case KEYCODE_NUMPAD_SUBTRACT:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
|
||||||
case KEYCODE_NUMPAD_DIVIDE:
|
case KEYCODE_NUMPAD_DIVIDE:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
|
||||||
case KEYCODE_NUMPAD_0:
|
case KEYCODE_NUMPAD_0:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "1";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
|
||||||
case KEYCODE_NUMPAD_1:
|
case KEYCODE_NUMPAD_1:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
|
||||||
case KEYCODE_NUMPAD_2:
|
case KEYCODE_NUMPAD_2:
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.termux.terminal;
|
package com.termux.terminal;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
|
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
|
||||||
* history.
|
* history.
|
||||||
* <p/>
|
* <p>
|
||||||
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
|
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
|
||||||
*/
|
*/
|
||||||
public final class TerminalBuffer {
|
public final class TerminalBuffer {
|
||||||
@@ -39,7 +41,23 @@ public final class TerminalBuffer {
|
|||||||
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
|
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTranscriptTextWithoutJoinedLines() {
|
||||||
|
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, false).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTranscriptTextWithFullLinesJoined() {
|
||||||
|
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, true, true).trim();
|
||||||
|
}
|
||||||
|
|
||||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
||||||
|
return getSelectedText(selX1, selY1, selX2, selY2, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
|
||||||
|
return getSelectedText(selX1, selY1, selX2, selY2, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) {
|
||||||
final StringBuilder builder = new StringBuilder();
|
final StringBuilder builder = new StringBuilder();
|
||||||
final int columns = mColumns;
|
final int columns = mColumns;
|
||||||
|
|
||||||
@@ -77,7 +95,9 @@ public final class TerminalBuffer {
|
|||||||
}
|
}
|
||||||
if (lastPrintingCharIndex != -1)
|
if (lastPrintingCharIndex != -1)
|
||||||
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||||
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1;
|
||||||
|
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||||
|
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
@@ -92,22 +112,20 @@ public final class TerminalBuffer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a row value from the public external coordinate system to our internal private coordinate system.
|
* Convert a row value from the public external coordinate system to our internal private coordinate system.
|
||||||
* <p/>
|
*
|
||||||
* <ul>
|
|
||||||
* <li>External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
|
|
||||||
* <li>Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
|
|
||||||
* mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
|
|
||||||
* </ul>
|
|
||||||
* <p/>
|
|
||||||
* External <---> Internal:
|
|
||||||
* <p/>
|
|
||||||
* <pre>
|
* <pre>
|
||||||
* [ ... ] [ ... ]
|
* - External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
|
||||||
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
|
* - Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
|
||||||
* [ ... ] [ ... ]
|
* mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
|
||||||
* [ 0 (visible screen starts here) ] <-----> [ mScreenFirstRow ]
|
*
|
||||||
* [ ... ] [ ... ]
|
* External ↔ Internal:
|
||||||
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
|
*
|
||||||
|
* [ ... ] [ ... ]
|
||||||
|
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
|
||||||
|
* [ ... ] [ ... ]
|
||||||
|
* [ 0 (visible screen starts here) ] ↔ [ mScreenFirstRow ]
|
||||||
|
* [ ... ] [ ... ]
|
||||||
|
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* @param externalRow a row in the external coordinate system.
|
* @param externalRow a row in the external coordinate system.
|
||||||
@@ -140,7 +158,7 @@ public final class TerminalBuffer {
|
|||||||
* @param newRows The number of rows the screen should have.
|
* @param newRows The number of rows the screen should have.
|
||||||
* @param cursor An int[2] containing the (column, row) cursor location.
|
* @param cursor An int[2] containing the (column, row) cursor location.
|
||||||
*/
|
*/
|
||||||
public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, int currentStyle, boolean altScreen) {
|
public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean altScreen) {
|
||||||
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
|
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
|
||||||
if (newColumns == mColumns && newRows <= mTotalRows) {
|
if (newColumns == mColumns && newRows <= mTotalRows) {
|
||||||
// Fast resize where just the rows changed.
|
// Fast resize where just the rows changed.
|
||||||
@@ -237,7 +255,7 @@ public final class TerminalBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int currentOldCol = 0;
|
int currentOldCol = 0;
|
||||||
int styleAtCol = 0;
|
long styleAtCol = 0;
|
||||||
for (int i = 0; i < lastNonSpaceIndex; i++) {
|
for (int i = 0; i < lastNonSpaceIndex; i++) {
|
||||||
// Note that looping over java character, not cells.
|
// Note that looping over java character, not cells.
|
||||||
char c = oldLine.mText[i];
|
char c = oldLine.mText[i];
|
||||||
@@ -321,7 +339,7 @@ public final class TerminalBuffer {
|
|||||||
* @param bottomMargin One line after the last line that is scrolled.
|
* @param bottomMargin One line after the last line that is scrolled.
|
||||||
* @param style the style for the newly exposed line.
|
* @param style the style for the newly exposed line.
|
||||||
*/
|
*/
|
||||||
public void scrollDownOneLine(int topMargin, int bottomMargin, int style) {
|
public void scrollDownOneLine(int topMargin, int bottomMargin, long style) {
|
||||||
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
|
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
|
||||||
throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
|
throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
|
||||||
|
|
||||||
@@ -374,7 +392,7 @@ public final class TerminalBuffer {
|
|||||||
* InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
|
* InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
|
||||||
* of characters.
|
* of characters.
|
||||||
*/
|
*/
|
||||||
public void blockSet(int sx, int sy, int w, int h, int val, int style) {
|
public void blockSet(int sx, int sy, int w, int h, int val, long style) {
|
||||||
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
|
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
|
"Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
|
||||||
@@ -388,14 +406,14 @@ public final class TerminalBuffer {
|
|||||||
return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
|
return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setChar(int column, int row, int codePoint, int style) {
|
public void setChar(int column, int row, int codePoint, long style) {
|
||||||
if (row >= mScreenRows || column >= mColumns)
|
if (row >= mScreenRows || column >= mColumns)
|
||||||
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
||||||
row = externalToInternalRow(row);
|
row = externalToInternalRow(row);
|
||||||
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
|
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getStyleAt(int externalRow, int column) {
|
public long getStyleAt(int externalRow, int column) {
|
||||||
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
|
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +425,7 @@ public final class TerminalBuffer {
|
|||||||
int startOfLine = (rectangular || y == top) ? left : leftMargin;
|
int startOfLine = (rectangular || y == top) ? left : leftMargin;
|
||||||
int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
|
int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
|
||||||
for (int x = startOfLine; x < endOfLine; x++) {
|
for (int x = startOfLine; x < endOfLine; x++) {
|
||||||
int currentStyle = line.getStyle(x);
|
long currentStyle = line.getStyle(x);
|
||||||
int foreColor = TextStyle.decodeForeColor(currentStyle);
|
int foreColor = TextStyle.decodeForeColor(currentStyle);
|
||||||
int backColor = TextStyle.decodeBackColor(currentStyle);
|
int backColor = TextStyle.decodeBackColor(currentStyle);
|
||||||
int effect = TextStyle.decodeEffect(currentStyle);
|
int effect = TextStyle.decodeEffect(currentStyle);
|
||||||
@@ -424,4 +442,14 @@ public final class TerminalBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clearTranscript() {
|
||||||
|
if (mScreenFirstRow < mActiveTranscriptRows) {
|
||||||
|
Arrays.fill(mLines, mTotalRows + mScreenFirstRow - mActiveTranscriptRows, mTotalRows, null);
|
||||||
|
Arrays.fill(mLines, 0, mScreenFirstRow, null);
|
||||||
|
} else {
|
||||||
|
Arrays.fill(mLines, mScreenFirstRow - mActiveTranscriptRows, mScreenFirstRow, null);
|
||||||
|
}
|
||||||
|
mActiveTranscriptRows = 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||