Compare commits
462 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adc32e25e1 | |||
| e2e94e938e | |||
| a0c2d12eeb | |||
| 11a5cc020e | |||
|
|
2f40df91e5 | ||
|
|
755b752a95 | ||
|
|
67f4891580 | ||
|
|
7b19cd2f5a | ||
|
|
882da34fcd | ||
|
|
8e3a8980a8 | ||
|
|
e4385832b7 | ||
|
|
3b5018b4c7 | ||
|
|
c84d4804c8 | ||
|
|
6727bbecc4 | ||
|
|
e27f9fa979 | ||
|
|
e2f0edf4d2 | ||
|
|
c5b69975e1 | ||
|
|
cce78cc274 | ||
|
|
32cd8a9384 | ||
|
|
eef5ac43a7 | ||
|
|
55cdef01e7 | ||
|
|
7c262b8d99 | ||
|
|
06230f95df | ||
|
|
9d308c2331 | ||
|
|
11d8e4ff8f | ||
|
|
66a9495d91 | ||
|
|
33295decbb | ||
|
|
ba1fb850bf | ||
|
|
1240c5ca47 | ||
|
|
c1dca29076 | ||
|
|
93eafffb90 | ||
|
|
9b274f9a0d | ||
|
|
b800f1cc81 | ||
|
|
2ac7fd1e56 | ||
|
|
c6dce12510 | ||
|
|
2f5a6f7de6 | ||
|
|
82f83a2970 | ||
|
|
b1c043d540 | ||
|
|
cff6cff609 | ||
|
|
29cf9820e1 | ||
|
|
c8a74dc588 | ||
|
|
20dee0e940 | ||
|
|
3516f1979f | ||
|
|
5bc3d2db8d | ||
|
|
3f7a939313 | ||
|
|
0c14c291b2 | ||
|
|
63d035ce39 | ||
|
|
8c1749ef96 | ||
|
|
6c56073958 | ||
|
|
061dc776bd | ||
|
|
211340781b | ||
|
|
605dd6c192 | ||
|
|
4646aca597 | ||
|
|
f1d411a5ab | ||
|
|
5fc2b4cd4a | ||
|
|
a2df7d791a | ||
|
|
82b1580312 | ||
|
|
e92a6db06b | ||
|
|
4c47f4f732 | ||
|
|
26ff978b0f | ||
|
|
b80126fd61 | ||
|
|
162469f7ce | ||
|
|
e75680a884 | ||
|
|
af6ac30bb1 | ||
|
|
79d799a99d | ||
|
|
841c41bf37 | ||
|
|
c2ddc23ae5 | ||
|
|
b69630355a | ||
|
|
42eee49d30 | ||
|
|
03e1d14e1e | ||
|
|
f76c20d036 | ||
|
|
150b1ff99c | ||
|
|
ebdab0e59c | ||
|
|
afc06cfd0a | ||
|
|
9749360caa | ||
|
|
29d05cc72c | ||
|
|
2998558e9f | ||
|
|
13d93ccac7 | ||
|
|
f102ea20b2 | ||
|
|
0328d15ea7 | ||
|
|
f9e9193c4e | ||
|
|
790481b802 | ||
|
|
1788013c80 | ||
|
|
5759411109 | ||
|
|
0fd354a469 | ||
|
|
042487c2b4 | ||
|
|
b96fcb78fd | ||
|
|
9547869a52 | ||
|
|
d29e20b0d0 | ||
|
|
0c22067b5e | ||
|
|
d287734aba | ||
|
|
46cfea09ec | ||
|
|
980bf8f0ae | ||
|
|
231ecff5f0 | ||
|
|
c1c46dfcfc | ||
|
|
37f08c4fcc | ||
|
|
a50387b553 | ||
|
|
30cb848639 | ||
|
|
b04f209f17 | ||
|
|
7b222ba392 | ||
|
|
899ef71e17 | ||
|
|
4d084c02e7 | ||
|
|
18a1a33e83 | ||
|
|
7677633e8f | ||
|
|
007bef8132 | ||
|
|
5290ce1f77 | ||
|
|
ab9b620c88 | ||
|
|
4e08f76fd2 | ||
|
|
c549988434 | ||
|
|
55dcd09a09 | ||
|
|
677a580042 | ||
|
|
fa829623a8 | ||
|
|
14e9a8b6fc | ||
|
|
a1719d91b3 | ||
|
|
9143ebdc22 | ||
|
|
623aaebb4a | ||
|
|
6213b7f782 | ||
|
|
0b4f456132 | ||
|
|
b950efec27 | ||
|
|
4b3b1a5b6a | ||
|
|
7f7d889dd0 | ||
|
|
53f26c9659 | ||
|
|
2aa7f43d1c | ||
|
|
5f8a922201 | ||
|
|
9a71074c7d | ||
|
|
69cc65c3ac | ||
|
|
bcd8f4c419 | ||
|
|
6bda7c4fc4 | ||
|
|
89a08ff01a | ||
|
|
c81d9c3346 | ||
|
|
58c3d427e8 | ||
|
|
1b9ca91da5 | ||
|
|
9c7ec0cebd | ||
|
|
6b60adc079 | ||
|
|
5116d886c3 | ||
|
|
02ab8324e9 | ||
|
|
c095a6184b | ||
|
|
cc981d8a03 | ||
|
|
007f9cd7f1 | ||
|
|
b025872029 | ||
|
|
2851175d8b | ||
|
|
3dee2eb486 | ||
|
|
33b88b5d4b | ||
|
|
f366db0cb3 | ||
|
|
4aca16326c | ||
|
|
e597ece75f | ||
|
|
9e06bfce1f | ||
|
|
5794ab9a56 | ||
|
|
ee32ef0c7e | ||
|
|
8746db0d22 | ||
|
|
ce12b8ad2d | ||
|
|
87a79a9b24 | ||
|
|
caa13b7047 | ||
|
|
5e820ad249 | ||
|
|
25d21e9d2e | ||
|
|
dd378738e3 | ||
|
|
93d57f053b | ||
|
|
26e0fa2b9e | ||
|
|
d25f7afd97 | ||
|
|
e0074f280f | ||
|
|
cc58ddde31 | ||
|
|
477b36acd1 | ||
|
|
5f00531381 | ||
|
|
4dbfc1fac8 | ||
|
|
4b07e4f4c0 | ||
|
|
621545dd0a | ||
|
|
9a65aa4589 | ||
|
|
021cb60e23 | ||
|
|
14c5fc7b1e | ||
|
|
792c33c9a5 | ||
|
|
760ae78aff | ||
|
|
c3ac30e2fb | ||
|
|
b94dc7eea9 | ||
|
|
05283bd774 | ||
|
|
6d944b5f7f | ||
|
|
bd004514df | ||
|
|
270e41fae5 | ||
|
|
68cdbd6ff4 | ||
|
|
280e284488 | ||
|
|
a01ff018b3 | ||
|
|
f8e7ada143 | ||
|
|
f33758c7c0 | ||
|
|
c567cc3b92 | ||
|
|
43858dfbb1 | ||
|
|
b8c3db0b6e | ||
|
|
622ff4fad4 | ||
|
|
a56ed5771d | ||
|
|
2a1c5a70da | ||
|
|
9b5aad9416 | ||
|
|
95d7a154a4 | ||
|
|
172a75e578 | ||
|
|
4fd48a5aed | ||
|
|
81dd113157 | ||
|
|
2452399a13 | ||
|
|
6631599fb6 | ||
|
|
f3f434af92 | ||
|
|
6fea1fbddc | ||
|
|
a3cd058fb4 | ||
|
|
b435d94888 | ||
|
|
3898ebdc74 | ||
|
|
1f3d3616a4 | ||
|
|
b45ff8a407 | ||
|
|
bf10c72661 | ||
|
|
1fb4fe2510 | ||
|
|
8e7e355fcb | ||
|
|
0fa0738cf6 | ||
|
|
998499d991 | ||
|
|
20a70b1a22 | ||
|
|
5d202d082f | ||
|
|
bb1584decb | ||
|
|
c1a0d6deff | ||
|
|
3f84b5345f | ||
|
|
006bfeac8d | ||
|
|
d222102635 | ||
|
|
7a386a7f2a | ||
|
|
b79ed509f1 | ||
|
|
1b794b3518 | ||
|
|
0a3efc537d | ||
|
|
36e49707ec | ||
|
|
f857bf2968 | ||
|
|
b69d14119e | ||
|
|
8c43b7f0a1 | ||
|
|
6ff5572999 | ||
|
|
8e506859a6 | ||
|
|
361bfb3961 | ||
|
|
549a772d45 | ||
|
|
37b9bcf5af | ||
|
|
7bbc12c7c9 | ||
|
|
74b23cb209 | ||
|
|
b559d5a0bd | ||
|
|
3e518a6a75 | ||
|
|
d96883c4d6 | ||
|
|
28ecb64992 | ||
|
|
5d64f1225c | ||
|
|
aed4b96a31 | ||
|
|
5b2aca9cf7 | ||
|
|
93d738ae63 | ||
|
|
f7ebcae7b3 | ||
|
|
63c106c746 | ||
|
|
2c0e9c6c5c | ||
|
|
9eeb2babd7 | ||
|
|
32dd7eab03 | ||
|
|
50a97b1977 | ||
|
|
f4a997b7dd | ||
|
|
3c202928b4 | ||
|
|
aca0000ee6 | ||
|
|
5252fbbe11 | ||
|
|
304aed3063 | ||
|
|
cbac7c8fbd | ||
|
|
65252dc640 | ||
|
|
2c6d009657 | ||
|
|
a987246bd8 | ||
|
|
059feaacf1 | ||
|
|
f62997a60e | ||
|
|
79980a07a8 | ||
|
|
4faf2b9d28 | ||
|
|
701b5ccd5c | ||
|
|
9798b30c76 | ||
|
|
fa91205bca | ||
|
|
64adc521de | ||
|
|
9814438ae5 | ||
|
|
ae7f141aca | ||
|
|
fd4159f1ba | ||
|
|
1327cef7b4 | ||
|
|
0da1984b59 | ||
|
|
09412da9d7 | ||
|
|
009c128052 | ||
|
|
9259ef0be1 | ||
|
|
480f92880c | ||
|
|
b01a738791 | ||
|
|
0eaaa1372a | ||
|
|
903b1c75a2 | ||
|
|
085b17e496 | ||
|
|
897d911a52 | ||
|
|
cd5962c696 | ||
|
|
6e6da752bd | ||
|
|
6d60bc669b | ||
|
|
6c24e6ac3b | ||
|
|
edf3b622e4 | ||
|
|
af16e79bf8 | ||
|
|
da6174e4c4 | ||
|
|
dcedf39434 | ||
|
|
e302a14cd0 | ||
|
|
f3ffc36bfd | ||
|
|
1f0f80b0c9 | ||
|
|
5e2bec0f4c | ||
|
|
075a080f00 | ||
|
|
0bf4b1eca4 | ||
|
|
4f66786b98 | ||
|
|
fefbf2ec03 | ||
|
|
54bb83de41 | ||
|
|
1a5a66d0ee | ||
|
|
865f29d49a | ||
|
|
22811167ac | ||
|
|
819571a03a | ||
|
|
c3280a94f0 | ||
|
|
5f3b1ccf90 | ||
|
|
0b47b20a9c | ||
|
|
783a840e3a | ||
|
|
c19e01fc1b | ||
|
|
9ffcd21ce1 | ||
|
|
94e01d68d6 | ||
|
|
0cf3cef7de | ||
|
|
7b10a35f24 | ||
|
|
e36c5294db | ||
|
|
dd952a90ad | ||
|
|
da07826a0c | ||
|
|
1259a212aa | ||
|
|
ac32fbc53d | ||
|
|
f00738fe3a | ||
|
|
5c72c3ca1b | ||
|
|
b62645cd03 | ||
|
|
23b707a819 | ||
|
|
4953b1269c | ||
|
|
d5ffb116b8 | ||
|
|
e5c0548942 | ||
|
|
4e5f2c7e01 | ||
|
|
3373a1f41c | ||
|
|
52c1ee520f | ||
|
|
197979fdcc | ||
|
|
bc779d2ffb | ||
|
|
9f1203f049 | ||
|
|
d55c1001c8 | ||
|
|
36557b2166 | ||
|
|
1cf1e612e5 | ||
|
|
e7d06aebb5 | ||
|
|
582e56938a | ||
|
|
5a8c4f10ee | ||
|
|
8387b70f64 | ||
|
|
994df1c4af | ||
|
|
63504f0adc | ||
|
|
829cc39868 | ||
|
|
16c56a968e | ||
|
|
b68a398fa8 | ||
|
|
f97f07df3f | ||
|
|
c59835ed93 | ||
|
|
d1478fb6c3 | ||
|
|
9117240961 | ||
|
|
fbb91149b5 | ||
|
|
2a74d43ca5 | ||
|
|
f65f384acf | ||
|
|
f055305790 | ||
|
|
296ee60dc8 | ||
|
|
1c01f4df08 | ||
|
|
e889d84dc4 | ||
|
|
956e20e53d | ||
|
|
10704b1dad | ||
|
|
19f4084099 | ||
|
|
486faf7fad | ||
|
|
6409019a40 | ||
|
|
7047bbefbb | ||
|
|
24ea83d6c0 | ||
|
|
351934a619 | ||
|
|
e7fc60af72 | ||
|
|
baacabdfbf | ||
|
|
35ea19dd75 | ||
|
|
7de0613617 | ||
|
|
5e09a501c9 | ||
|
|
60f37bde8d | ||
|
|
fabcc4fa35 | ||
|
|
98edf1fbc7 | ||
|
|
8ee0c5a6ec | ||
|
|
4a74618f17 | ||
|
|
19c6134c71 | ||
|
|
501d13a0cb | ||
|
|
e13773fd83 | ||
|
|
23d2c1f0e9 | ||
|
|
cac9a769c0 | ||
|
|
e30812af22 | ||
|
|
1578ab5547 | ||
|
|
d5d87639ce | ||
|
|
8ba5458221 | ||
|
|
a7596e7d03 | ||
|
|
2b7aa5e803 | ||
|
|
2b386efc3c | ||
|
|
9a306ca1c5 | ||
|
|
9febca9567 | ||
|
|
7d76e8b185 | ||
|
|
00d80b9e02 | ||
|
|
f10de462d2 | ||
|
|
f837ddef23 | ||
|
|
f4e70678b1 | ||
|
|
a189f63604 | ||
|
|
0308d6a6ca | ||
|
|
1b62f7c9a9 | ||
|
|
6fa4b9b7cd | ||
|
|
b2a071aad9 | ||
|
|
9272a757af | ||
|
|
d49fd6b00c | ||
|
|
e0ad9ff573 | ||
|
|
7aefd94369 | ||
|
|
dc8bdfe675 | ||
|
|
c6b4114f86 | ||
|
|
cce6dfed22 | ||
|
|
56c3826680 | ||
|
|
2cf21c8409 | ||
|
|
4361c5e0c5 | ||
|
|
a53cc88688 | ||
|
|
48161816e0 | ||
|
|
eabbda8efd | ||
|
|
b90d59479a | ||
|
|
dccd155ba6 | ||
|
|
78be0e793e | ||
|
|
e547c15481 | ||
|
|
c621c35827 | ||
|
|
886e52dcff | ||
|
|
8e4da6cbcd | ||
|
|
bde9d01f76 | ||
|
|
5a511a2ba3 | ||
|
|
5c50964b1f | ||
|
|
dea8c9879e | ||
|
|
2034121798 | ||
|
|
23a900c433 | ||
|
|
93a7525d9b | ||
|
|
5670128236 | ||
|
|
dfd32435af | ||
|
|
49265160f8 | ||
|
|
70e1accafe | ||
|
|
1c7f9166f2 | ||
|
|
553913cde1 | ||
|
|
6bca378cec | ||
|
|
12f910c32d | ||
|
|
94c5f3674a | ||
|
|
28b9f93d13 | ||
|
|
69bebb5916 | ||
|
|
321350256e | ||
|
|
e5a9b99afe | ||
|
|
00f805f7ec | ||
|
|
d3c34ad1f5 | ||
|
|
59877a08d1 | ||
|
|
9c92251595 | ||
|
|
e408fdcc08 | ||
|
|
53c1a49b5b | ||
|
|
2aafcf8435 | ||
|
|
1c1af34374 | ||
|
|
52f18a73fb | ||
|
|
28f81f2cc7 | ||
|
|
4494bc66e4 | ||
|
|
679e0de044 | ||
|
|
80b495e50b | ||
|
|
69e5deedc7 | ||
|
|
7f36d7bbd0 | ||
|
|
b7b12ebe84 | ||
|
|
f77c88633e | ||
|
|
5f2ccca423 | ||
|
|
f0f6927273 | ||
|
|
0fb18c0c8b | ||
|
|
4dfed3320e | ||
|
|
7ac62c9840 | ||
|
|
fd80cdaf23 | ||
|
|
19c690d02b | ||
|
|
e119d34bca | ||
|
|
f545ebf0bd | ||
|
|
0b4bbaf23d | ||
|
|
e7dd0eeebe | ||
|
|
7ef9255437 | ||
|
|
7225e2b379 | ||
|
|
1ad038ece5 | ||
|
|
cb8b0225ca | ||
|
|
7620800cd5 | ||
|
|
6837db0015 | ||
|
|
e08e3b536e |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
||||
patreon: termux
|
||||
custom: https://paypal.me/fornwall
|
||||
custom: https://termux.dev/donate
|
||||
|
||||
44
.github/ISSUE_TEMPLATE/01-bug-report.yml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/01-bug-report.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: "Bug report"
|
||||
description: "Create a report to help us improve"
|
||||
title: "[Bug]: "
|
||||
labels: ["bug report"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This is a bug tracker of the Termux app. If you have issues with a package inside the app, then please open an issue at [termux-packages](https://github.com/termux/termux-packages) instead.
|
||||
|
||||
Use search before you open an issue to check whether your issue has been already reported and perhaps solved.
|
||||
|
||||
Android versions 5.x and 6.x are not supported anymore.
|
||||
|
||||
If you have issues installing packages then please see https://github.com/termux/termux-packages/issues/6726.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem description
|
||||
description: |
|
||||
A clear and concise description of what the problem is. You may attach the logs, screenshots, screen video recording and whatever else that will help to understand the issue.
|
||||
|
||||
Issues without proper description will be closed without solution.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce the behavior.
|
||||
description: |
|
||||
Please post all necessary commands that are needed to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What is the expected behavior?
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: System information
|
||||
description: Please provide info about your device
|
||||
value: |
|
||||
* Termux application version:
|
||||
* Android OS version:
|
||||
* Device model:
|
||||
validations:
|
||||
required: true
|
||||
19
.github/ISSUE_TEMPLATE/02-feature-request.yml
vendored
Normal file
19
.github/ISSUE_TEMPLATE/02-feature-request.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: "Feature request"
|
||||
description: "Suggest a new feature for Termux application"
|
||||
title: "[Feature]: "
|
||||
labels: ["feature request"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: Describe the feature and why you want it.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |
|
||||
Does another app/terminal emulator have this feature?
|
||||
Provide links to more background information.
|
||||
validations:
|
||||
required: true
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
||||
---
|
||||
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:
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Want ask questions about the project?
|
||||
url: https://github.com/termux/termux-app/discussions
|
||||
about: Join GitHub Discussions
|
||||
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
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.
|
||||
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
commit-message:
|
||||
# Prefix all commit messages with "Changed: "
|
||||
prefix: "Changed"
|
||||
84
.github/workflows/attach_debug_apks_to_release.yml
vendored
Normal file
84
.github/workflows/attach_debug_apks_to_release.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: Attach Debug APKs To Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
attach-apks:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package_variant: [ apt-android-7, apt-android-5 ]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.GITHUB_REF }}
|
||||
|
||||
- name: Build and attach APKs to release
|
||||
shell: bash {0}
|
||||
env:
|
||||
PACKAGE_VARIANT: ${{ matrix.package_variant }}
|
||||
run: |
|
||||
exit_on_error() {
|
||||
echo "$1"
|
||||
echo "Deleting '$RELEASE_VERSION_NAME' release and '$GITHUB_REF' tag"
|
||||
hub release delete "$RELEASE_VERSION_NAME"
|
||||
git push --delete origin "$GITHUB_REF"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "Setting vars"
|
||||
RELEASE_VERSION_NAME="${GITHUB_REF/refs\/tags\//}"
|
||||
if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then
|
||||
exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html."
|
||||
fi
|
||||
|
||||
APK_DIR_PATH="./app/build/outputs/apk/debug"
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME+${{ env.PACKAGE_VARIANT }}-github-debug"
|
||||
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
|
||||
|
||||
echo "Building APKs for 'APK_VERSION_TAG' release"
|
||||
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
|
||||
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle
|
||||
if ! ./gradlew assembleDebug; then
|
||||
exit_on_error "Build failed for '$APK_VERSION_TAG' release."
|
||||
fi
|
||||
|
||||
echo "Validating APKs"
|
||||
for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do
|
||||
if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then
|
||||
files_found="$(ls "$APK_DIR_PATH")"
|
||||
exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Generating sha25sums file"
|
||||
if ! (cd "$APK_DIR_PATH"; sha256sum \
|
||||
"${APK_BASENAME_PREFIX}_universal.apk" \
|
||||
"${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
|
||||
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
> "${APK_BASENAME_PREFIX}_sha256sums"); then
|
||||
exit_on_error "Generate sha25sums failed for '$APK_VERSION_TAG' release."
|
||||
fi
|
||||
|
||||
echo "Attaching APKs to github release"
|
||||
if ! hub release edit \
|
||||
-m "" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_sha256sums" \
|
||||
"$RELEASE_VERSION_NAME"; then
|
||||
exit_on_error "Attach APKs to release failed for '$APK_VERSION_TAG' release."
|
||||
fi
|
||||
124
.github/workflows/debug_build.yml
vendored
124
.github/workflows/debug_build.yml
vendored
@@ -4,23 +4,123 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- android-10
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- android-10
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package_variant: [ apt-android-7, apt-android-5 ]
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: |
|
||||
./gradlew assembleDebug
|
||||
- name: Store generated APK file
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: termux-app
|
||||
path: ./app/build/outputs/apk/debug
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build APKs
|
||||
shell: bash {0}
|
||||
env:
|
||||
PACKAGE_VARIANT: ${{ matrix.package_variant }}
|
||||
run: |
|
||||
exit_on_error() { echo "$1"; exit 1; }
|
||||
|
||||
echo "Setting vars"
|
||||
|
||||
if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
|
||||
GITHUB_SHA="${{ github.event.pull_request.head.sha }}" # Do not use last merge commit set in GITHUB_SHA
|
||||
fi
|
||||
|
||||
# Set RELEASE_VERSION_NAME to "<CURRENT_VERSION_NAME>+<last_commit_hash>"
|
||||
CURRENT_VERSION_NAME_REGEX='\s+versionName "([^"]+)"$'
|
||||
CURRENT_VERSION_NAME="$(grep -m 1 -E "$CURRENT_VERSION_NAME_REGEX" ./app/build.gradle | sed -r "s/$CURRENT_VERSION_NAME_REGEX/\1/")"
|
||||
RELEASE_VERSION_NAME="v$CURRENT_VERSION_NAME+${GITHUB_SHA:0:7}" # The "+" is necessary so that versioning precedence is not affected
|
||||
if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then
|
||||
exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html."
|
||||
fi
|
||||
|
||||
APK_DIR_PATH="./app/build/outputs/apk/debug"
|
||||
APK_VERSION_TAG="$RELEASE_VERSION_NAME-${{ env.PACKAGE_VARIANT }}-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
|
||||
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
|
||||
|
||||
# Used by attachment steps later
|
||||
echo "APK_DIR_PATH=$APK_DIR_PATH" >> $GITHUB_ENV
|
||||
echo "APK_VERSION_TAG=$APK_VERSION_TAG" >> $GITHUB_ENV
|
||||
echo "APK_BASENAME_PREFIX=$APK_BASENAME_PREFIX" >> $GITHUB_ENV
|
||||
|
||||
echo "Building APKs for 'APK_VERSION_TAG' build"
|
||||
export TERMUX_APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle
|
||||
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
|
||||
export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle
|
||||
if ! ./gradlew assembleDebug; then
|
||||
exit_on_error "Build failed for '$APK_VERSION_TAG' build."
|
||||
fi
|
||||
|
||||
echo "Validating APKs"
|
||||
for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do
|
||||
if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then
|
||||
files_found="$(ls "$APK_DIR_PATH")"
|
||||
exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Generating sha25sums file"
|
||||
if ! (cd "$APK_DIR_PATH"; sha256sum \
|
||||
"${APK_BASENAME_PREFIX}_universal.apk" \
|
||||
"${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
|
||||
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||
"${APK_BASENAME_PREFIX}_x86.apk" \
|
||||
> "${APK_BASENAME_PREFIX}_sha256sums"); then
|
||||
exit_on_error "Generate sha25sums failed for '$APK_VERSION_TAG' release."
|
||||
fi
|
||||
|
||||
- name: Attach universal APK file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_universal
|
||||
path: |
|
||||
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_universal.apk
|
||||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach arm64-v8a APK file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a
|
||||
path: |
|
||||
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_arm64-v8a.apk
|
||||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach armeabi-v7a APK file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a
|
||||
path: |
|
||||
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a.apk
|
||||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach x86_64 APK file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_x86_64
|
||||
path: |
|
||||
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86_64.apk
|
||||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach x86 APK file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_x86
|
||||
path: |
|
||||
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86.apk
|
||||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
- name: Attach sha256sums file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.APK_BASENAME_PREFIX }}_sha256sums
|
||||
path: |
|
||||
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_sha256sums
|
||||
${{ env.APK_DIR_PATH }}/output-metadata.json
|
||||
|
||||
@@ -15,5 +15,5 @@ jobs:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/wrapper-validation-action@v3
|
||||
|
||||
26
.github/workflows/publish_libraries.yml
vendored
26
.github/workflows/publish_libraries.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Publish library packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'terminal-emulator/build.gradle'
|
||||
- 'terminal-view/build.gradle'
|
||||
- 'termux-shared/build.gradle'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Perform release build
|
||||
run: |
|
||||
./gradlew assembleRelease
|
||||
- name: Publish libraries on Github Packages
|
||||
env:
|
||||
GH_USERNAME: xeffyr
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
./gradlew publish
|
||||
2
.github/workflows/run_tests.yml
vendored
2
.github/workflows/run_tests.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Execute tests
|
||||
run: |
|
||||
./gradlew test
|
||||
|
||||
21
.github/workflows/trigger_library_builds_on_jitpack.yml
vendored
Normal file
21
.github/workflows/trigger_library_builds_on_jitpack.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Trigger Termux Library Builds on Jitpack
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
trigger-termux-library-builds:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set vars
|
||||
run: echo "TERMUX_LIB_VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV # Do not include "v" prefix
|
||||
- name: Echo release
|
||||
run: echo "Triggering termux library builds on jitpack for '$TERMUX_LIB_VERSION' release after waiting for 3 mins"
|
||||
- name: Trigger termux library builds on jitpack
|
||||
run: |
|
||||
sleep 180 # It will take some time for the new tag to be detected by Jitpack
|
||||
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/terminal-emulator/$TERMUX_LIB_VERSION/terminal-emulator-$TERMUX_LIB_VERSION.pom"
|
||||
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/terminal-view/$TERMUX_LIB_VERSION/terminal-view-$TERMUX_LIB_VERSION.pom"
|
||||
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/termux-shared/$TERMUX_LIB_VERSION/termux-shared-$TERMUX_LIB_VERSION.pom"
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -4,15 +4,13 @@
|
||||
|
||||
# Built application files
|
||||
build/
|
||||
release/
|
||||
*.apk
|
||||
*.so
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
*.zip
|
||||
|
||||
# Crashlytics configuations
|
||||
com_crashlytics_export_strings.xml
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
@@ -26,6 +24,10 @@ local.properties
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Vim
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
@@ -35,3 +37,9 @@ local.properties
|
||||
.swp
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
## WinBoLL Studio Configs
|
||||
winboll.properties
|
||||
appkey.jks
|
||||
appkey.keystore
|
||||
|
||||
|
||||
@@ -2,6 +2,5 @@ The `termux/termux-app` repository is released under [GPLv3 only](https://www.gn
|
||||
|
||||
### Exceptions
|
||||
|
||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [terminal-view](terminal-view) and [terminal-emulator](terminal-emulator) modules.
|
||||
- [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/) code is used which is released under [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html). Check `com.termux.shared.file` package under [termux-shared](termux-shared) module.
|
||||
- [libsuperuser ](https://github.com/Chainfire/libsuperuser) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check `com.termux.shared.shell.StreamGobbler` class under [termux-shared](termux-shared) module.
|
||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
||||
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
|
||||
|
||||
225
README.md
225
README.md
@@ -3,6 +3,9 @@
|
||||
[](https://github.com/termux/termux-app/actions)
|
||||
[](https://github.com/termux/termux-app/actions)
|
||||
[](https://gitter.im/termux/termux)
|
||||
[](https://discord.gg/HXpF69X)
|
||||
[](https://jitpack.io/#termux/termux-app)
|
||||
|
||||
|
||||
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
||||
|
||||
@@ -10,20 +13,22 @@ Note that this repository is for the app itself (the user interface and the term
|
||||
|
||||
Quick how-to about Termux package management is available at [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management). It also has info on how to fix **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands.
|
||||
|
||||
***
|
||||
|
||||
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.**
|
||||
|
||||
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
|
||||
**We are looking for Termux Android application maintainers.**
|
||||
|
||||
***
|
||||
|
||||
### Contents
|
||||
- [Termux App and Plugins](#Termux-App-and-Plugins)
|
||||
- [Installation](#Installation)
|
||||
- [Uninstallation](#Uninstallation)
|
||||
- [Important Links](#Important-Links)
|
||||
- [For Devs and Contributors](#For-Devs-and-Contributors)
|
||||
**NOTICE: Termux may be unstable on Android 12+.** Android OS will kill any (phantom) processes greater than 32 (limit is for all apps combined) and also kill any processes using excessive CPU. You may get `[Process completed (signal 9) - press Enter]` message in the terminal without actually exiting the shell process yourself. Check the related issue [#2366](https://github.com/termux/termux-app/issues/2366), [issue tracker](https://issuetracker.google.com/u/1/issues/205156966), [phantom cached and empty processes docs](https://github.com/agnostic-apollo/Android-Docs/blob/master/en/docs/apps/processes/phantom-cached-and-empty-processes.md) and [this TLDR comment](https://github.com/termux/termux-app/issues/2366#issuecomment-1237468220) on how to disable trimming of phantom and excessive cpu usage processes. A proper docs page will be added later. An option to disable the killing should be available in Android 12L or 13, so upgrade at your own risk if you are on Android 11, specially if you are not rooted.
|
||||
|
||||
***
|
||||
|
||||
## Contents
|
||||
- [Termux App and Plugins](#termux-app-and-plugins)
|
||||
- [Installation](#installation)
|
||||
- [Uninstallation](#uninstallation)
|
||||
- [Important Links](#important-links)
|
||||
- [Debugging](#debugging)
|
||||
- [For Maintainers and Contributors](#for-maintainers-and-contributors)
|
||||
- [Forking](#forking)
|
||||
##
|
||||
|
||||
|
||||
@@ -44,38 +49,120 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
|
||||
|
||||
## Installation
|
||||
|
||||
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).
|
||||
Latest version is `v0.118.0`.
|
||||
|
||||
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [sharedUserId](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from F-Droid and another one from a different source. Android Package Manager will also normally not allow installation of APKs with a different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
|
||||
**NOTICE: It is highly recommended that you update to `v0.118.0` or higher ASAP for various bug fixes, including a critical world-readable vulnerability reported [here](https://termux.github.io/general/2022/02/15/termux-apps-vulnerability-disclosures.html). Also reminding [again](https://www.reddit.com/r/termux/comments/pkujfa/important_deprecation_notice_for_google_play) to users who have installed termux apps from google playstore that playstore builds are [deprecated](#google-play-store-deprecated) and no longer supported. It is recommended that you shift to F-Droid or GitHub releases.**
|
||||
|
||||
If you wish to install from a different source, then you must uninstall **any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#Uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation.
|
||||
Termux can be obtained through various sources listed below for **only** Android `>= 7` with full support for apps and packages.
|
||||
|
||||
Support for both app and packages was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, however it was re-added just for the app *without any support for package updates* on [2022-05-24](https://github.com/termux/termux-app/pull/2740) via the [GitHub](#github) sources. Check [here](https://github.com/termux/termux-app/wiki/Termux-on-android-5-or-6) for the details.
|
||||
|
||||
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [`sharedUserId`](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from `F-Droid` and another one from a different source like `GitHub`. Android Package Manager will also normally not allow installation of APKs with different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
|
||||
|
||||
If you wish to install from a different source, then you must **uninstall any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation so that you can restore it after re-installing from Termux different source.
|
||||
|
||||
In the following paragraphs, *"bootstrap"* refers to the minimal packages that are shipped with the `termux-app` itself to start a working shell environment. Its zips are built and released [here](https://github.com/termux/termux-packages/releases).
|
||||
|
||||
### F-Droid
|
||||
|
||||
Termux application can be obtained from F-Droid [here](https://f-droid.org/en/packages/com.termux/). It usually takes a few days (or even a week or more) for updates to be available on F-Droid once an update has been released on Github. F-Droid releases are built and published by F-Droid once they detect a new Github release. The Termux maintainers **do not** have any control over building and publishing of Termux app on F-Droid. Moreover, the Termux maintainers also do not have access to the APK signing keys of F-Droid releases, so we cannot release an APK ourselves on Github that would be compatible with F-Droid releases.
|
||||
Termux application can be obtained from `F-Droid` from [here](https://f-droid.org/en/packages/com.termux/).
|
||||
|
||||
### Debug Builds
|
||||
You **do not** need to download the `F-Droid` app (via the `Download F-Droid` link) to install Termux. You can download the Termux APK directly from the site by clicking the `Download APK` link at the bottom of each version section.
|
||||
|
||||
For users who don't want to wait for F-Droid releases and want to try out the latest features immediately or want to test their pull requests can get the APKs from [Github Actions](https://github.com/termux/termux-app/actions) page from the workflow runs labeled `Build`. The APK will be listed under `Artifacts` section. These are published for each commit done to the repository. These APKs are [debuggable](https://developer.android.com/studio/debug) and are also not compatible with other sources.
|
||||
It usually takes a few days (or even a week or more) for updates to be available on `F-Droid` once an update has been released on `GitHub`. The `F-Droid` releases are built and published by `F-Droid` once they [detect](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.yml) a new `GitHub` release. The Termux maintainers **do not** have any control over the building and publishing of the Termux apps on `F-Droid`. Moreover, the Termux maintainers also do not have access to the APK signing keys of `F-Droid` releases, so we cannot release an APK ourselves on `GitHub` that would be compatible with `F-Droid` releases.
|
||||
|
||||
### Google Playstore **(Deprecated)**
|
||||
The `F-Droid` app often may not notify you of updates and you will manually have to do a pull down swipe action in the `Updates` tab of the app for it to check updates. Make sure battery optimizations are disabled for the app, check https://dontkillmyapp.com/ for details on how to do that.
|
||||
|
||||
**Termux and its plugins are no longer updated on [Google playstore](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10).** The last version released for Android `>= 7` was `v0.101`. There are currently no immediate plans to resume updates on Google playstore. **It is highly recommended to not install Termux from playstore for now.** Any current users **should switch** to a different source like F-Droid.
|
||||
Only a universal APK is released, which will work on all supported architectures. The APK and bootstrap installation size will be `~180MB`. `F-Droid` does [not support](https://github.com/termux/termux-app/pull/1904) architecture specific APKs.
|
||||
|
||||
### GitHub
|
||||
|
||||
Termux application can be obtained on `GitHub` either from [`GitHub Releases`](https://github.com/termux/termux-app/releases) for version `>= 0.118.0` or from [`GitHub Build Action`](https://github.com/termux/termux-app/actions/workflows/debug_build.yml?query=branch%3Amaster+event%3Apush) workflows. **For android `>= 7`, only install `apt-android-7` variants. For android `5` and `6`, only install `apt-android-5` variants.**
|
||||
|
||||
The APKs for `GitHub Releases` will be listed under `Assets` drop-down of a release. These are automatically attached when a new version is released.
|
||||
|
||||
The APKs for `GitHub Build` action workflows will be listed under `Artifacts` section of a workflow run. These are created for each commit/push done to the repository and can be used by users who don't want to wait for releases and want to try out the latest features immediately or want to test their pull requests. Note that for action workflows, you need to be [**logged into a `GitHub` account**](https://github.com/login) for the `Artifacts` links to be enabled/clickable. If you are using the [`GitHub` app](https://github.com/mobile), then make sure to open workflow link in a browser like Chrome or Firefox that has your GitHub account logged in since the in-app browser may not be logged in.
|
||||
|
||||
The APKs for both of these are [`debuggable`](https://developer.android.com/studio/debug) and are compatible with each other but they are not compatible with other sources.
|
||||
|
||||
Both universal and architecture specific APKs are released. The APK and bootstrap installation size will be `~180MB` if using universal and `~120MB` if using architecture specific. Check [here](https://github.com/termux/termux-app/issues/2153) for details.
|
||||
|
||||
**Security warning**: APK files on GitHub are signed with a test key that has been [shared with community](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks). This IS NOT an official developer key and everyone can use it to generate releases for own testing. Be very careful when using Termux GitHub builds obtained elsewhere except https://github.com/termux/termux-app. Everyone is able to use it to forge a malicious Termux update installable over the GitHub build. Think twice about installing Termux builds distributed via Telegram or other social media. If your device get caught by malware, we will not be able to help you.
|
||||
|
||||
The [test key](https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks) shall not be used to impersonate @termux and can't be used for this anyway. This key is not trusted by us and it is quite easy to detect its use in user generated content.
|
||||
|
||||
<details>
|
||||
<summary>Keystore information</summary>
|
||||
|
||||
```
|
||||
Alias name: alias
|
||||
Creation date: Oct 4, 2019
|
||||
Entry type: PrivateKeyEntry
|
||||
Certificate chain length: 1
|
||||
Certificate[1]:
|
||||
Owner: CN=APK Signer, OU=Earth, O=Earth
|
||||
Issuer: CN=APK Signer, OU=Earth, O=Earth
|
||||
Serial number: 29be297b
|
||||
Valid from: Wed Sep 04 02:03:24 EEST 2019 until: Tue Oct 26 02:03:24 EEST 2049
|
||||
Certificate fingerprints:
|
||||
SHA1: 51:79:55:EA:BF:69:FC:05:7C:41:C7:D3:79:DB:BC:EF:20:AD:85:F2
|
||||
SHA256: B6:DA:01:48:0E:EF:D5:FB:F2:CD:37:71:B8:D1:02:1E:C7:91:30:4B:DD:6C:4B:F4:1D:3F:AA:BA:D4:8E:E5:E1
|
||||
Signature algorithm name: SHA1withRSA (disabled)
|
||||
Subject Public Key Algorithm: 2048-bit RSA key
|
||||
Version: 3
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Google Play Store **(Deprecated)**
|
||||
|
||||
**Termux and its plugins are no longer updated on [Google Play Store](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10) and have been deprecated.** The last version released for Android `>= 7` was `v0.101`. **It is highly recommended to not install Termux apps from Play Store any more.**
|
||||
|
||||
**Termux developers do not have access to Play Store Console account where Termux is published and therefore can't remove the app.** You are encouraged to move to `F-Droid` or `GitHub` builds as soon as possible and suggest doing so for other users via social media.
|
||||
|
||||
You **will not need to buy plugins again** if you bought them on Play Store. All plugins are free on `F-Droid` and `GitHub`.
|
||||
|
||||
You can backup all your data under `$HOME/` and `$PREFIX/` before changing installation source, and then restore it afterwards, by following instructions at [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
|
||||
|
||||
There is currently no work being done to solve android `10` issues and *working* updates will not be resumed on Google Play Store any time soon. We will continue targeting sdk `28` for now. So there is not much point in staying on Play Store builds and waiting for updates to be resumed. If for some reason you don't want to move to `F-Droid` or `GitHub` sources for now, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise, you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
|
||||
|
||||
Note that by upgrading old packages to latest versions, like that of `python` may break your setups/scripts since they may not be compatible anymore. Moreover, you will not be able to downgrade the package versions since termux repos only keep the latest version and you will have to manually rebuild the old versions of the packages if required as per [Building packages](https://github.com/termux/termux-packages/wiki/Building-packages).
|
||||
|
||||
If you plan on staying on Play Store sources in future as well, then you may want to **disable automatic updates in Play Store** for Termux apps, since if and when updates to disable Termux apps are released, then **you will not be able to downgrade** and **will be forced** to move since apps won't work anymore. Only a way to backup `termux-app` data may be provided. The `termux-tools` [version `>= 0.135`](https://github.com/termux/termux-packages/pull/7493) will also show a banner at the top of the terminal saying `You are likely using a very old version of Termux, probably installed from the Google Play Store.`, you can remove it by running `rm -f /data/data/com.termux/files/usr/etc/motd-playstore` and restarting the app.
|
||||
|
||||
#### Why Disable?
|
||||
|
||||
<details>
|
||||
<summary></summary>
|
||||
|
||||
- Play store apps have multiple critical vulnerabilities as reported at https://termux.github.io/general/2022/02/15/termux-apps-vulnerability-disclosures.html and since they cannot be updated with fixes, any users using older versions would be vulnerable.
|
||||
|
||||
- They should be disabled because deprecated things get removed and are not supported after some time, its the standard practice. It has been many months now since deprecation was announced and updates have not been released on Play Store since after `29 September 2020`.
|
||||
|
||||
- The new versions have lots of **new features and fixes** which you can mostly check out in the Changelog of [`GitHub Releases`](https://github.com/termux/termux-app/releases) that you may be missing out. Extra detail is usually provided in [commit messages](https://github.com/termux/termux-app/commits/master).
|
||||
|
||||
- Users on old versions are quite often reporting issues in multiple repositories and support forums that were **fixed months ago**, which we then have to deal with. The maintainers of @termux work in their free time, majorly for free, to work on development and provide support and having to re-re-deal with old issues takes away the already limited time from current work and is not possible to continue doing. Play Store page of `termux-app` has been filled with bad reviews of *"broken app"*, even though its clearly mentioned on the page that app is not being updated, yet users don't read and still install and report issues.
|
||||
|
||||
- Asking people to pay for plugins when the `termux-app` at installation time is broken due to repository issues and has bugs is unethical.
|
||||
|
||||
- Old versions don't have proper logging/debugging and crash report support. Reporting bugs without logs or detailed info is not helpful in solving them.
|
||||
|
||||
- It's also easier for us to solve package related issues and provide custom functionality with app updates, which can't be done if users continue using old versions. For example, the [bintray shutdown](https://github.com/termux/termux-packages/wiki/Package-Management) causing package install/update failures for new Play Store users is/was not an issue for F-Droid users since it is being shipped with updated bootstrap and repo info, hence no reported issues from new F-Droid users.
|
||||
</details>
|
||||
|
||||
If for some reason you don't want to switch, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
|
||||
##
|
||||
|
||||
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#Installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation.
|
||||
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
|
||||
|
||||
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins).
|
||||
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#termux-app-and-plugins).
|
||||
|
||||
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if its available on your device and search `termux` in the applications list.
|
||||
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if it’s available on your device and search `termux` in the applications list.
|
||||
|
||||
Even if you think you have not installed any of the plugins, its strongly suggesting to go through the application list in Android settings and double check.
|
||||
Even if you think you have not installed any of the plugins, it's strongly suggested to go through the application list in Android settings and double-check.
|
||||
##
|
||||
|
||||
|
||||
@@ -88,10 +175,10 @@ All community links are available [here](https://wiki.termux.com/wiki/Community)
|
||||
The main ones are the following.
|
||||
|
||||
- [Termux Reddit community](https://reddit.com/r/termux)
|
||||
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
|
||||
- [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im)
|
||||
- [Termux Twitter](http://twitter.com/termux/)
|
||||
- [Termux Reports Email](mailto:termuxreports@groups.io)
|
||||
- [Termux User Matrix Channel](https://matrix.to/#/#termux_termux:gitter.im) ([Gitter](https://gitter.im/termux/termux))
|
||||
- [Termux Dev Matrix Channel](https://matrix.to/#/#termux_dev:gitter.im) ([Gitter](https://gitter.im/termux/dev))
|
||||
- [Termux X (Twitter)](https://twitter.com/termuxdevs)
|
||||
- [Termux Support Email](mailto:support@termux.dev)
|
||||
|
||||
### Wikis
|
||||
|
||||
@@ -104,42 +191,108 @@ The main ones are the following.
|
||||
- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout)
|
||||
- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux)
|
||||
- [Package Management](https://wiki.termux.com/wiki/Package_Management)
|
||||
- [Remote_Access](https://wiki.termux.com/wiki/Remote_Access)
|
||||
- [Remote Access](https://wiki.termux.com/wiki/Remote_Access)
|
||||
- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux)
|
||||
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
|
||||
- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard)
|
||||
- [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage)
|
||||
- [Android APIs](https://wiki.termux.com/wiki/Termux:API)
|
||||
- [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348)
|
||||
- [Running Commands in Termux From Other Apps via `RUN_COMMAND` intent](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent)
|
||||
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
|
||||
|
||||
|
||||
### Terminal
|
||||
|
||||
<details>
|
||||
<summary></summary>
|
||||
|
||||
### 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)
|
||||
- [XTerm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||
- [vt100.net](https://vt100.net/)
|
||||
- [Terminal codes (ANSI and terminfo equivalents)](https://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)).
|
||||
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](https://iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](https://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).
|
||||
- xterm: The grandfather of terminal emulators. [Source](https://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).
|
||||
</details>
|
||||
|
||||
##
|
||||
|
||||
|
||||
|
||||
## For Devs and Contributors
|
||||
### Debugging
|
||||
|
||||
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of Termux app and its plugins. It was created to allow for removal of all hardcoded paths in Termux app. The termux plugins will hopefully use this in future as well. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted.
|
||||
You can help debug problems of the `Termux` app and its plugins by setting appropriate `logcat` `Log Level` in `Termux` app settings -> `<APP_NAME>` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.118.0`). The `Log Level` defaults to `Normal` and log level `Verbose` currently logs additional information. Its best to revert log level to `Normal` after you have finished debugging since private data may otherwise be passed to `logcat` during normal operation and moreover, additional logging increases execution time.
|
||||
|
||||
The plugin apps **do not execute the commands themselves** but send execution intents to `Termux` app, which has its own log level which can be set in `Termux` app settings -> `Termux` -> `Debugging` -> `Log Level`. So you must set log level for both `Termux` and the respective plugin app settings to get all the info.
|
||||
|
||||
Once log levels have been set, you can run the `logcat` command in `Termux` app terminal to view the logs in realtime (`Ctrl+c` to stop) or use `logcat -d > logcat.txt` to take a dump of the log. You can also view the logs from a PC over `ADB`. For more information, check official android `logcat` guide [here](https://developer.android.com/studio/command-line/logcat).
|
||||
|
||||
Moreover, users can generate termux files `stat` info and `logcat` dump automatically too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead.
|
||||
|
||||
Users must post complete report (optionally without sensitive info) when reporting issues. Issues opened with **(partial) screenshots of error reports** instead of text will likely be automatically closed/deleted.
|
||||
|
||||
##### Log Levels
|
||||
|
||||
- `Off` - Log nothing.
|
||||
- `Normal` - Start logging error, warn and info messages and stacktraces.
|
||||
- `Debug` - Start logging debug messages.
|
||||
- `Verbose` - Start logging verbose messages.
|
||||
##
|
||||
|
||||
|
||||
|
||||
## For Maintainers and Contributors
|
||||
|
||||
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of the Termux app and its plugins. It was created to allow for the removal of all hardcoded paths in the Termux app. Some of the termux plugins are using this as well and rest will in future. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted. Termux app and plugin specific classes must be added under `com.termux.shared.termux` package and general classes outside it. The [`termux-shared` `LICENSE`](termux-shared/LICENSE.md) must also be checked and updated if necessary when contributing code. The licenses of any external library or code must be honoured.
|
||||
|
||||
The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info.
|
||||
|
||||
Check [Termux Libraries](https://github.com/termux/termux-app/wiki/Termux-Libraries) for how to import termux libraries in plugin apps and [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for how to update termux libraries for plugins.
|
||||
|
||||
The `versionName` in `build.gradle` files of Termux and its plugin apps must follow the [semantic version `2.0.0` spec](https://semver.org/spec/v2.0.0.html) in the format `major.minor.patch(-prerelease)(+buildmetadata)`. When bumping `versionName` in `build.gradle` files and when creating a tag for new releases on GitHub, make sure to include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow validates the version as well and the build/attachment will fail if `versionName` does not follow the spec.
|
||||
|
||||
### Commit Messages Guidelines
|
||||
|
||||
Commit messages **must** use the [Conventional Commits](https://www.conventionalcommits.org) spec so that chagelogs as per the [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) spec can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. **The first letter for `type` and `description` must be capital and description should be in the present tense.** The space after the colon `:` is necessary. For a breaking change, add an exclamation mark `!` before the colon `:`, so that it is highlighted in the chagelog automatically.
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
**Only the `types` listed below must be used exactly as they are used in the changelog headings.** For example, `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Fix some bug`. **Do not use anything else as type, like `add` instead of `Added`, etc.**
|
||||
|
||||
- **Added** for new features.
|
||||
- **Changed** for changes in existing functionality.
|
||||
- **Deprecated** for soon-to-be removed features.
|
||||
- **Removed** for now removed features.
|
||||
- **Fixed** for any bug fixes.
|
||||
- **Security** in case of vulnerabilities.
|
||||
##
|
||||
|
||||
|
||||
|
||||
## Forking
|
||||
|
||||
- Check [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) javadocs for instructions on what changes to make in the app to change package name.
|
||||
- You also need to recompile bootstrap zip for the new package name. Check [building bootstrap](https://github.com/termux/termux-packages/wiki/For-maintainers#build-bootstrap-archives), [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111).
|
||||
- Currently, not all plugins use `TermuxConstants` from `termux-shared` library and have hardcoded `com.termux` values and will need to be manually patched.
|
||||
- If forking termux plugins, check [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for info on how to use termux libraries for plugins.
|
||||
|
||||
142
app/build.gradle
142
app/build.gradle
@@ -1,17 +1,39 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "com.android.application"
|
||||
}
|
||||
// 读取秘钥配置文件
|
||||
//
|
||||
def keyProps = new Properties()
|
||||
def keyPropsFile = rootProject.file('appkey.keystore')
|
||||
if (keyPropsFile.exists()) {
|
||||
keyProps.load(new FileInputStream(keyPropsFile))
|
||||
}
|
||||
|
||||
ext {
|
||||
// The packageVariant defines the bootstrap variant that will be included in the app APK.
|
||||
// This must be supported by com.termux.shared.termux.TermuxBootstrap.PackageVariant or app will
|
||||
// crash at startup.
|
||||
// Bootstrap of a different variant must not be manually installed by the user after app installation
|
||||
// by replacing $PREFIX since app code is dependant on the variant used to build the APK.
|
||||
// Currently supported values are: [ "apt-android-7" "apt-android-5" ]
|
||||
packageVariant = System.getenv("TERMUX_PACKAGE_VARIANT") ?: "apt-android-7" // Default: "apt-android-7"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
ndkVersion project.properties.ndkVersion
|
||||
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||
def appVersionName = System.getenv("TERMUX_APP_VERSION_NAME") ?: ""
|
||||
def apkVersionTag = System.getenv("TERMUX_APK_VERSION_TAG") ?: ""
|
||||
def splitAPKsForDebugBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS") ?: "1"
|
||||
def splitAPKsForReleaseBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS") ?: "0" // F-Droid does not support split APKs #1904
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
implementation "androidx.core:core:1.5.0-rc01"
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
implementation "androidx.core:core:1.6.0"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
@@ -26,8 +48,13 @@ android {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
versionCode 113
|
||||
versionName "0.113"
|
||||
versionCode 118
|
||||
versionName "0.118.0"
|
||||
|
||||
if (appVersionName) versionName = appVersionName
|
||||
validateVersionName(versionName)
|
||||
|
||||
buildConfigField "String", "TERMUX_PACKAGE_VARIANT", "\"" + project.ext.packageVariant + "\"" // Used by TermuxApplication class
|
||||
|
||||
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
|
||||
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
||||
@@ -44,34 +71,55 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
splits {
|
||||
abi {
|
||||
enable ((gradle.startParameter.taskNames.any { it.contains("Debug") } && splitAPKsForDebugBuilds == "1") ||
|
||||
(gradle.startParameter.taskNames.any { it.contains("Release") } && splitAPKsForReleaseBuilds == "1"))
|
||||
reset ()
|
||||
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 配置签名
|
||||
signingConfigs {
|
||||
winboll {
|
||||
keyAlias keyProps['keyAlias']
|
||||
keyPassword keyProps['keyPassword']
|
||||
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null
|
||||
storePassword keyProps['storePassword']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
/*signingConfigs {
|
||||
debug {
|
||||
storeFile file('dev_keystore.jks')
|
||||
storeFile file('testkey_untrusted.jks')
|
||||
keyAlias 'alias'
|
||||
storePassword 'xrj45yWGLbsO7W0v'
|
||||
keyPassword 'xrj45yWGLbsO7W0v'
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.winboll
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
shrinkResources false // Reproducible builds
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
signingConfig signingConfigs.winboll
|
||||
//signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
@@ -91,17 +139,44 @@ android {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging true
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all { output ->
|
||||
if (variant.buildType.name == "debug") {
|
||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "debug") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
} else if (variant.buildType.name == "release") {
|
||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "release") + "_" + (abi ? abi : "universal") + ".apk")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
testImplementation "org.robolectric:robolectric:4.10"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
|
||||
}
|
||||
|
||||
task versionName {
|
||||
doLast {
|
||||
print android.defaultConfig.versionName
|
||||
}
|
||||
doLast {
|
||||
print android.defaultConfig.versionName
|
||||
}
|
||||
}
|
||||
|
||||
def validateVersionName(String versionName) {
|
||||
// https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
// ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
|
||||
if (!java.util.regex.Pattern.matches("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?\$", versionName))
|
||||
throw new GradleException("The versionName '" + versionName + "' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html.")
|
||||
}
|
||||
|
||||
def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
||||
@@ -118,10 +193,11 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
||||
digest.update(buffer, 0, readBytes)
|
||||
}
|
||||
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
||||
while (checksum.length() < 64) { checksum = "0" + checksum }
|
||||
if (checksum == expectedChecksum) {
|
||||
return
|
||||
} else {
|
||||
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
|
||||
logger.quiet("Deleting old local file with wrong hash: " + localUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
@@ -139,6 +215,7 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
||||
out.close()
|
||||
|
||||
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
||||
while (checksum.length() < 64) { checksum = "0" + checksum }
|
||||
if (checksum != expectedChecksum) {
|
||||
file.delete()
|
||||
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
|
||||
@@ -155,16 +232,27 @@ clean {
|
||||
|
||||
task downloadBootstraps() {
|
||||
doLast {
|
||||
def version = "2021.05.16-r1"
|
||||
downloadBootstrap("aarch64", "6e340d8ab11d1225b89ee920e0884cbbd944d37765d81c5b06ef34579564fd9a", version)
|
||||
downloadBootstrap("arm", "3f02bc2b5bd45c2ec5170527e39ee0413246698f11be4799c7bde6d364cfd780", version)
|
||||
downloadBootstrap("i686", "36a3733fb2d8531d7f8abd989b711919872b9e8a79d7eb2e8b00bef467199187", version)
|
||||
downloadBootstrap("x86_64", "3885376cc514220c0803e38f70b25f837854029fff2b7fda7a81452623cd9074", version)
|
||||
def packageVariant = project.ext.packageVariant
|
||||
if (packageVariant == "apt-android-7") {
|
||||
def version = "2022.04.28-r5" + "+" + packageVariant
|
||||
downloadBootstrap("aarch64", "4a51a7eb209fe82efc24d52e3cccc13165f27377290687cb82038cbd8e948430", version)
|
||||
downloadBootstrap("arm", "6459a786acbae50d4c8a36fa1c3de6a4dd2d482572f6d54f73274709bd627325", version)
|
||||
downloadBootstrap("i686", "919d212b2f19e08600938db4079e794e947365022dbfd50ac342c50fcedcd7be", version)
|
||||
downloadBootstrap("x86_64", "61b02fdc03ea4f5d9da8d8cf018013fdc6659e6da6cbf44e9b24d1c623580b89", version)
|
||||
} else if (packageVariant == "apt-android-5") {
|
||||
def version = "2022.04.28-r6" + "+" + packageVariant
|
||||
downloadBootstrap("aarch64", "913609d439415c828c5640be1b0561467e539cb1c7080662decaaca2fb4820e7", version)
|
||||
downloadBootstrap("arm", "26bfb45304c946170db69108e5eb6e3641aad751406ce106c80df80cad2eccf8", version)
|
||||
downloadBootstrap("i686", "46dcfeb5eef67ba765498db9fe4c50dc4690805139aa0dd141a9d8ee0693cd27", version)
|
||||
downloadBootstrap("x86_64", "615b590679ee6cd885b7fd2ff9473c845e920f9b422f790bb158c63fe42b8481", version)
|
||||
} else {
|
||||
throw new GradleException("Unsupported TERMUX_PACKAGE_VARIANT \"" + packageVariant + "\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
||||
}
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
||||
}
|
||||
}
|
||||
|
||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@@ -10,3 +10,8 @@
|
||||
-dontobfuscate
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Temp fix for androidx.window:window:1.0.0-alpha09 imported by termux-shared
|
||||
# https://issuetracker.google.com/issues/189001730
|
||||
# https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630
|
||||
-keep class androidx.window.** { *; }
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -32,6 +34,8 @@
|
||||
<uses-permission android:name="android.permission.DUMP" />
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<application
|
||||
android:name=".app.TermuxApplication"
|
||||
@@ -40,24 +44,21 @@
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/Theme.Termux">
|
||||
|
||||
<!--
|
||||
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
||||
mark the app with "This app is optimized to run in full screen."
|
||||
-->
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="10.0" />
|
||||
android:theme="@style/Theme.TermuxApp.DayNight.DarkActionBar"
|
||||
tools:targetApi="m">
|
||||
|
||||
<activity
|
||||
android:name=".app.TermuxActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:label="@string/application_name"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true">
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar"
|
||||
tools:targetApi="n">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -76,6 +77,7 @@
|
||||
|
||||
<activity-alias
|
||||
android:name=".HomeActivity"
|
||||
android:exported="true"
|
||||
android:targetActivity=".app.TermuxActivity">
|
||||
|
||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||
@@ -93,27 +95,33 @@
|
||||
android:label="@string/application_name"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
|
||||
tools:targetApi="n" />
|
||||
|
||||
<activity
|
||||
android:name=".app.activities.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_termux_settings"
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||
android:theme="@style/Theme.TermuxApp.DayNight.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".app.activities.ReportActivity"
|
||||
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
/>
|
||||
android:name=".shared.activities.ReportActivity"
|
||||
android:theme="@style/Theme.MarkdownViewActivity.DayNight"
|
||||
android:documentLaunchMode="intoExisting" />
|
||||
|
||||
<activity
|
||||
android:name=".filepicker.TermuxFileReceiverActivity"
|
||||
android:name=".app.api.file.FileReceiverActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:label="@string/application_name"
|
||||
android:exported="false"
|
||||
android:noHistory="true"
|
||||
android:resizeableActivity="true"
|
||||
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
|
||||
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver"
|
||||
tools:targetApi="n">
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".app.api.file.FileShareReceiverActivity"
|
||||
android:exported="true"
|
||||
android:targetActivity=".app.api.file.FileReceiverActivity">
|
||||
|
||||
<!-- Accept multiple file types when sending. -->
|
||||
<intent-filter>
|
||||
@@ -129,6 +137,13 @@
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".app.api.file.FileViewReceiverActivity"
|
||||
android:exported="true"
|
||||
android:targetActivity=".app.api.file.FileReceiverActivity">
|
||||
|
||||
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -141,7 +156,7 @@
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</activity-alias>
|
||||
|
||||
<provider
|
||||
android:name=".filepicker.TermuxDocumentsProvider"
|
||||
@@ -154,9 +169,35 @@
|
||||
</intent-filter>
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".app.TermuxOpenReceiver$ContentProvider"
|
||||
android:authorities="${TERMUX_PACKAGE_NAME}.files"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND" />
|
||||
|
||||
|
||||
<receiver
|
||||
android:name=".app.TermuxOpenReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".app.event.SystemEventReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".shared.activities.ReportActivity$ReportActivityBroadcastReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
|
||||
<service
|
||||
android:name=".app.TermuxService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".app.RunCommandService"
|
||||
android:exported="true"
|
||||
@@ -166,21 +207,30 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".app.TermuxOpenReceiver" />
|
||||
|
||||
<provider
|
||||
android:name=".app.TermuxOpenReceiver$ContentProvider"
|
||||
android:authorities="${TERMUX_PACKAGE_NAME}.files"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:readPermission="android.permission.permRead" />
|
||||
<!-- 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" />
|
||||
|
||||
|
||||
<!-- https://developer.samsung.com/samsung-dex/modify-optimizing.html -->
|
||||
|
||||
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||
<meta-data
|
||||
android:name="com.samsung.android.keepalive.density"
|
||||
android:value="true" />
|
||||
|
||||
<!-- Version >= 3.0. DeX Dual Mode support -->
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -10,15 +10,21 @@ import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.file.filesystem.FileType;
|
||||
import com.termux.shared.errors.Errno;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.shell.command.ExecutionCommand;
|
||||
import com.termux.shared.shell.command.ExecutionCommand.Runner;
|
||||
|
||||
/**
|
||||
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||
@@ -57,43 +63,85 @@ public class RunCommandService extends Service {
|
||||
// Run again in case service is already started and onCreate() is not called
|
||||
runStartForeground();
|
||||
|
||||
Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||
|
||||
Error error;
|
||||
String errmsg;
|
||||
|
||||
// If invalid action passed, then just return
|
||||
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||
executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
|
||||
executionCommand.stdin = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_STDIN);
|
||||
executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
|
||||
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||
String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
|
||||
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
|
||||
|
||||
/*
|
||||
* If intent was sent with `am` command, then normal comma characters may have been replaced
|
||||
* with alternate characters if a normal comma existed in an argument itself to prevent it
|
||||
* splitting into multiple arguments by `am` command.
|
||||
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
|
||||
* options can be used without passing the below extras, but native supports is helpful if
|
||||
* they are not being used.
|
||||
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
|
||||
*/
|
||||
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
|
||||
if (replaceCommaAlternativeCharsInArguments) {
|
||||
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
|
||||
if (commaAlternativeCharsInArguments == null)
|
||||
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
|
||||
// Replace any commaAlternativeCharsInArguments characters with normal commas
|
||||
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
|
||||
}
|
||||
|
||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
||||
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
||||
|
||||
// If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION
|
||||
executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RUNNER,
|
||||
(intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName()));
|
||||
if (Runner.runnerOf(executionCommand.runner) == null) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_invalid_execution_command_runner, executionCommand.runner);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
|
||||
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
|
||||
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
||||
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
|
||||
executionCommand.shellName = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SHELL_NAME, null);
|
||||
executionCommand.shellCreateMode = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SHELL_CREATE_MODE, null);
|
||||
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
|
||||
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||
executionCommand.isPluginExecutionCommand = true;
|
||||
executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||
|
||||
|
||||
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
|
||||
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
|
||||
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||
}
|
||||
|
||||
// If "allow-external-apps" property to not set to "true", then just return
|
||||
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||
// user knows someone tried to run a command in termux context, since it may be malicious
|
||||
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
|
||||
// also sent, then its creator is also logged and shown.
|
||||
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||
errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
|
||||
if (errmsg != null) {
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
return Service.START_NOT_STICKY;
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
||||
@@ -101,24 +149,23 @@ public class RunCommandService extends Service {
|
||||
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
|
||||
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
// Get canonical path of executable
|
||||
executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||
|
||||
// If executable is not a regular file, or is not readable or executable, then just return
|
||||
// Setting of missing read and execute permissions is not done
|
||||
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null,
|
||||
PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
|
||||
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||
false);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
if (error != null) {
|
||||
executionCommand.setStateFailed(error);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
|
||||
|
||||
@@ -126,29 +173,36 @@ public class RunCommandService extends Service {
|
||||
// If workingDirectory is not null or empty
|
||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||
// Get canonical path of workingDirectory
|
||||
executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||
|
||||
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
||||
// under allowed termux working directory paths.
|
||||
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||
// for working directories.
|
||||
errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true,
|
||||
PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
||||
true, true);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
|
||||
true, true, true,
|
||||
false, true);
|
||||
if (error != null) {
|
||||
executionCommand.setStateFailed(error);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return stopService();
|
||||
}
|
||||
}
|
||||
|
||||
// If the executable passed as the extra was an applet for coreutils/busybox, then we must
|
||||
// use it instead of the canonical path above since otherwise arguments would be passed to
|
||||
// coreutils/busybox instead and command would fail. Broken symlinks would already have been
|
||||
// validated so it should be fine to use it.
|
||||
executableExtra = TermuxFileUtils.getExpandedTermuxPath(executableExtra);
|
||||
if (FileUtils.getFileType(executableExtra, false) == FileType.SYMLINK) {
|
||||
Logger.logVerbose(LOG_TAG, "The executableExtra path \"" + executableExtra + "\" is a symlink so using it instead of the canonical path \"" + executionCommand.executable + "\"");
|
||||
executionCommand.executable = executableExtra;
|
||||
}
|
||||
|
||||
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build();
|
||||
|
||||
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||
|
||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
|
||||
|
||||
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
||||
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
|
||||
@@ -156,13 +210,24 @@ public class RunCommandService extends Service {
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
|
||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RUNNER, executionCommand.runner);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null));
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SHELL_NAME, executionCommand.shellName);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SHELL_CREATE_MODE, executionCommand.shellCreateMode);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
|
||||
}
|
||||
|
||||
// Start TERMUX_SERVICE and pass it execution intent
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -171,8 +236,11 @@ public class RunCommandService extends Service {
|
||||
this.startService(execIntent);
|
||||
}
|
||||
|
||||
runStopForeground();
|
||||
return stopService();
|
||||
}
|
||||
|
||||
private int stopService() {
|
||||
runStopForeground();
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@@ -194,7 +262,7 @@ public class RunCommandService extends Service {
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
|
||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
|
||||
null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||
null, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||
if (builder == null) return null;
|
||||
|
||||
// No need to show a timestamp:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.BroadcastReceiver;
|
||||
@@ -11,7 +9,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -26,36 +23,52 @@ import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.autofill.AutofillManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ListView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.api.file.FileReceiverActivity;
|
||||
import com.termux.app.terminal.TermuxActivityRootView;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||
import com.termux.app.terminal.io.TermuxTerminalExtraKeys;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.activity.ActivityUtils;
|
||||
import com.termux.shared.activity.media.AppCompatActivityUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.android.PermissionUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||
import com.termux.app.activities.HelpActivity;
|
||||
import com.termux.app.activities.SettingsActivity;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.app.terminal.TermuxSessionsListViewController;
|
||||
import com.termux.app.terminal.io.TerminalToolbarViewPager;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.termux.theme.TermuxThemeUtils;
|
||||
import com.termux.shared.theme.NightMode;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
import com.termux.app.utils.CrashUtils;
|
||||
import com.termux.view.TerminalView;
|
||||
import com.termux.view.TerminalViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A terminal emulator activity.
|
||||
* <p/>
|
||||
@@ -66,8 +79,7 @@ import androidx.viewpager.widget.ViewPager;
|
||||
* </ul>
|
||||
* about memory leaks.
|
||||
*/
|
||||
public final class TermuxActivity extends Activity implements ServiceConnection {
|
||||
|
||||
public final class TermuxActivity extends AppCompatActivity implements ServiceConnection {
|
||||
|
||||
/**
|
||||
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
|
||||
@@ -77,7 +89,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
TermuxService mTermuxService;
|
||||
|
||||
/**
|
||||
* The main view of the activity showing the terminal. Initialized in onCreate().
|
||||
* The {@link TerminalView} shown in {@link TermuxActivity} that displays the terminal.
|
||||
*/
|
||||
TerminalView mTerminalView;
|
||||
|
||||
@@ -91,7 +103,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
* The {@link TerminalSessionClient} interface implementation to allow for communication between
|
||||
* {@link TerminalSession} and {@link TermuxActivity}.
|
||||
*/
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
|
||||
/**
|
||||
* Termux app shared preferences manager.
|
||||
@@ -99,15 +111,30 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
private TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
/**
|
||||
* Termux app shared properties manager, loaded from termux.properties
|
||||
* Termux app SharedProperties loaded from termux.properties
|
||||
*/
|
||||
private TermuxAppSharedProperties mProperties;
|
||||
|
||||
/**
|
||||
* The root view of the {@link TermuxActivity}.
|
||||
*/
|
||||
TermuxActivityRootView mTermuxActivityRootView;
|
||||
|
||||
/**
|
||||
* The space at the bottom of {@link @mTermuxActivityRootView} of the {@link TermuxActivity}.
|
||||
*/
|
||||
View mTermuxActivityBottomSpaceView;
|
||||
|
||||
/**
|
||||
* The terminal extra keys view.
|
||||
*/
|
||||
ExtraKeysView mExtraKeysView;
|
||||
|
||||
/**
|
||||
* The client for the {@link #mExtraKeysView}.
|
||||
*/
|
||||
TermuxTerminalExtraKeys mTermuxTerminalExtraKeys;
|
||||
|
||||
/**
|
||||
* The termux sessions list controller.
|
||||
*/
|
||||
@@ -129,6 +156,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
*/
|
||||
private boolean mIsVisible;
|
||||
|
||||
/**
|
||||
* If onResume() was called after onCreate().
|
||||
*/
|
||||
private boolean mIsOnResumeAfterOnCreate = false;
|
||||
|
||||
/**
|
||||
* If activity was restarted like due to call to {@link #recreate()} after receiving
|
||||
* {@link TERMUX_ACTIVITY#ACTION_RELOAD_STYLE}, system dark night mode was changed or activity
|
||||
* was killed by android.
|
||||
*/
|
||||
private boolean mIsActivityRecreated = false;
|
||||
|
||||
/**
|
||||
* The {@link TermuxActivity} is in an invalid state and must not be run.
|
||||
*/
|
||||
@@ -136,11 +175,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
private int mNavBarHeight;
|
||||
|
||||
private int mTerminalToolbarDefaultHeight;
|
||||
private float mTerminalToolbarDefaultHeight;
|
||||
|
||||
|
||||
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
|
||||
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
|
||||
private static final int CONTEXT_MENU_SHARE_SELECTED_TEXT = 10;
|
||||
private static final int CONTEXT_MENU_AUTOFILL_ID = 2;
|
||||
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
|
||||
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
|
||||
@@ -150,23 +190,25 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
private static final int CONTEXT_MENU_SETTINGS_ID = 8;
|
||||
private static final int CONTEXT_MENU_REPORT_ID = 9;
|
||||
|
||||
private static final int REQUESTCODE_PERMISSION_STORAGE = 1234;
|
||||
|
||||
private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input";
|
||||
private static final String ARG_ACTIVITY_RECREATED = "activity_recreated";
|
||||
|
||||
private static final String LOG_TAG = "TermuxActivity";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "onCreate");
|
||||
mIsOnResumeAfterOnCreate = true;
|
||||
|
||||
// Check if a crash happened on last run of the app and show a
|
||||
// notification with the crash details if it did
|
||||
CrashUtils.notifyCrash(this, LOG_TAG);
|
||||
if (savedInstanceState != null)
|
||||
mIsActivityRecreated = savedInstanceState.getBoolean(ARG_ACTIVITY_RECREATED, false);
|
||||
|
||||
// Load termux shared properties
|
||||
mProperties = new TermuxAppSharedProperties(this);
|
||||
// Delete ReportInfo serialized object files from cache older than 14 days
|
||||
ReportActivity.deleteReportInfoFilesOlderThanXDays(this, 14, false);
|
||||
|
||||
// Load Termux app SharedProperties from disk
|
||||
mProperties = TermuxAppSharedProperties.getProperties();
|
||||
reloadProperties();
|
||||
|
||||
setActivityTheme();
|
||||
|
||||
@@ -183,6 +225,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
return;
|
||||
}
|
||||
|
||||
setMargins();
|
||||
|
||||
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
|
||||
mTermuxActivityRootView.setActivity(this);
|
||||
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
|
||||
mTermuxActivityRootView.setOnApplyWindowInsetsListener(new TermuxActivityRootView.WindowInsetsListener());
|
||||
|
||||
View content = findViewById(android.R.id.content);
|
||||
content.setOnApplyWindowInsetsListener((v, insets) -> {
|
||||
mNavBarHeight = insets.getSystemWindowInsetBottom();
|
||||
@@ -193,26 +242,38 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
setDrawerTheme();
|
||||
|
||||
setTermuxTerminalViewAndClients();
|
||||
|
||||
setTerminalToolbarView(savedInstanceState);
|
||||
|
||||
setSettingsButtonView();
|
||||
|
||||
setNewSessionButtonView();
|
||||
|
||||
setToggleKeyboardView();
|
||||
|
||||
registerForContextMenu(mTerminalView);
|
||||
|
||||
// Start the {@link TermuxService} and make it run regardless of who is bound to it
|
||||
Intent serviceIntent = new Intent(this, TermuxService.class);
|
||||
startService(serviceIntent);
|
||||
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
|
||||
|
||||
// Attempt to bind to the service, this will call the {@link #onServiceConnected(ComponentName, IBinder)}
|
||||
// callback if it succeeds.
|
||||
if (!bindService(serviceIntent, this, 0))
|
||||
throw new RuntimeException("bindService() failed");
|
||||
try {
|
||||
// Start the {@link TermuxService} and make it run regardless of who is bound to it
|
||||
Intent serviceIntent = new Intent(this, TermuxService.class);
|
||||
startService(serviceIntent);
|
||||
|
||||
// Attempt to bind to the service, this will call the {@link #onServiceConnected(ComponentName, IBinder)}
|
||||
// callback if it succeeds.
|
||||
if (!bindService(serviceIntent, this, 0))
|
||||
throw new RuntimeException("bindService() failed");
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG,"TermuxActivity failed to start TermuxService", e);
|
||||
Logger.showToast(this,
|
||||
getString(e.getMessage() != null && e.getMessage().contains("app is in background") ?
|
||||
R.string.error_termux_service_start_failed_bg : R.string.error_termux_service_start_failed_general),
|
||||
true);
|
||||
mIsInvalidState = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux
|
||||
// app has been opened.
|
||||
@@ -229,12 +290,15 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
mIsVisible = true;
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onStart();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onStart();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onStart();
|
||||
|
||||
if (mPreferences.isTerminalMarginAdjustmentEnabled())
|
||||
addTermuxActivityRootViewGlobalLayoutListener();
|
||||
|
||||
registerTermuxActivityBroadcastReceiver();
|
||||
}
|
||||
|
||||
@@ -246,11 +310,17 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
if (mIsInvalidState) return;
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onResume();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onResume();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onResume();
|
||||
|
||||
// Check if a crash happened on last run of the app or if a plugin crashed and show a
|
||||
// notification with the crash details if it did
|
||||
TermuxCrashUtils.notifyAppCrashFromCrashLogFile(this, LOG_TAG);
|
||||
|
||||
mIsOnResumeAfterOnCreate = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -263,13 +333,15 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
mIsVisible = false;
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onStop();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onStop();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onStop();
|
||||
|
||||
unregisterTermuxActivityBroadcastReceiever();
|
||||
removeTermuxActivityRootViewGlobalLayoutListener();
|
||||
|
||||
unregisterTermuxActivityBroadcastReceiver();
|
||||
getDrawer().closeDrawers();
|
||||
}
|
||||
|
||||
@@ -296,8 +368,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
Logger.logVerbose(LOG_TAG, "onSaveInstanceState");
|
||||
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
saveTerminalToolbarTextInput(savedInstanceState);
|
||||
savedInstanceState.putBoolean(ARG_ACTIVITY_RECREATED, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -311,24 +386,25 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
*/
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName componentName, IBinder service) {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "onServiceConnected");
|
||||
|
||||
mTermuxService = ((TermuxService.LocalBinder) service).service;
|
||||
|
||||
setTermuxSessionsListView();
|
||||
|
||||
final Intent intent = getIntent();
|
||||
setIntent(null);
|
||||
|
||||
if (mTermuxService.isTermuxSessionsEmpty()) {
|
||||
if (mIsVisible) {
|
||||
TermuxInstaller.setupBootstrapIfNeeded(TermuxActivity.this, () -> {
|
||||
if (mTermuxService == null) return; // Activity might have been destroyed.
|
||||
try {
|
||||
Bundle bundle = getIntent().getExtras();
|
||||
boolean launchFailsafe = false;
|
||||
if (bundle != null) {
|
||||
launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
||||
if (intent != null && intent.getExtras() != null) {
|
||||
launchFailsafe = intent.getExtras().getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
}
|
||||
mTermuxTerminalSessionClient.addNewSession(launchFailsafe, null);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(launchFailsafe, null);
|
||||
} catch (WindowManager.BadTokenException e) {
|
||||
// Activity finished - ignore.
|
||||
}
|
||||
@@ -338,23 +414,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
finishActivityIfNotFinishing();
|
||||
}
|
||||
} else {
|
||||
Intent i = getIntent();
|
||||
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
|
||||
// If termux was started from launcher "New session" shortcut and activity is recreated,
|
||||
// then the original intent will be re-delivered, resulting in a new session being re-added
|
||||
// each time.
|
||||
if (!mIsActivityRecreated && intent != null && Intent.ACTION_RUN.equals(intent.getAction())) {
|
||||
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
|
||||
boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
||||
mTermuxTerminalSessionClient.addNewSession(isFailSafe, null);
|
||||
boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null);
|
||||
} else {
|
||||
mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast());
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast());
|
||||
}
|
||||
}
|
||||
|
||||
// Update the {@link TerminalSession} and {@link TerminalEmulator} clients.
|
||||
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionClient);
|
||||
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionActivityClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "onServiceDisconnected");
|
||||
|
||||
// Respect being stopped from the {@link TermuxService} notification action.
|
||||
@@ -365,27 +442,50 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
|
||||
|
||||
private void setActivityTheme() {
|
||||
if (mProperties.isUsingBlackUI()) {
|
||||
this.setTheme(R.style.Theme_Termux_Black);
|
||||
} else {
|
||||
this.setTheme(R.style.Theme_Termux);
|
||||
}
|
||||
|
||||
private void reloadProperties() {
|
||||
mProperties.loadTermuxPropertiesFromDisk();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onReloadProperties();
|
||||
}
|
||||
|
||||
private void setDrawerTheme() {
|
||||
if (mProperties.isUsingBlackUI()) {
|
||||
findViewById(R.id.left_drawer).setBackgroundColor(ContextCompat.getColor(this,
|
||||
android.R.color.background_dark));
|
||||
}
|
||||
|
||||
|
||||
private void setActivityTheme() {
|
||||
// Update NightMode.APP_NIGHT_MODE
|
||||
TermuxThemeUtils.setAppNightMode(mProperties.getNightMode());
|
||||
|
||||
// Set activity night mode. If NightMode.SYSTEM is set, then android will automatically
|
||||
// trigger recreation of activity when uiMode/dark mode configuration is changed so that
|
||||
// day or night theme takes affect.
|
||||
AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true);
|
||||
}
|
||||
|
||||
private void setMargins() {
|
||||
RelativeLayout relativeLayout = findViewById(R.id.activity_termux_root_relative_layout);
|
||||
int marginHorizontal = mProperties.getTerminalMarginHorizontal();
|
||||
int marginVertical = mProperties.getTerminalMarginVertical();
|
||||
ViewUtils.setLayoutMarginsInDp(relativeLayout, marginHorizontal, marginVertical, marginHorizontal, marginVertical);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void addTermuxActivityRootViewGlobalLayoutListener() {
|
||||
getTermuxActivityRootView().getViewTreeObserver().addOnGlobalLayoutListener(getTermuxActivityRootView());
|
||||
}
|
||||
|
||||
public void removeTermuxActivityRootViewGlobalLayoutListener() {
|
||||
if (getTermuxActivityRootView() != null)
|
||||
getTermuxActivityRootView().getViewTreeObserver().removeOnGlobalLayoutListener(getTermuxActivityRootView());
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void setTermuxTerminalViewAndClients() {
|
||||
// Set termux terminal view and session clients
|
||||
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
|
||||
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionClient);
|
||||
mTermuxTerminalSessionActivityClient = new TermuxTerminalSessionActivityClient(this);
|
||||
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionActivityClient);
|
||||
|
||||
// Set termux terminal view
|
||||
mTerminalView = findViewById(R.id.terminal_view);
|
||||
@@ -394,8 +494,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onCreate();
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onCreate();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onCreate();
|
||||
}
|
||||
|
||||
private void setTermuxSessionsListView() {
|
||||
@@ -409,7 +509,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
|
||||
private void setTerminalToolbarView(Bundle savedInstanceState) {
|
||||
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
|
||||
mTermuxTerminalExtraKeys = new TermuxTerminalExtraKeys(this, mTerminalView,
|
||||
mTermuxTerminalViewClient, mTermuxTerminalSessionActivityClient);
|
||||
|
||||
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
||||
if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
|
||||
|
||||
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
||||
@@ -426,23 +529,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
|
||||
private void setTerminalToolbarHeight() {
|
||||
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
|
||||
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
||||
if (terminalToolbarViewPager == null) return;
|
||||
|
||||
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
||||
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
|
||||
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
|
||||
mProperties.getTerminalToolbarHeightScaleFactor());
|
||||
layoutParams.height = Math.round(mTerminalToolbarDefaultHeight *
|
||||
(mTermuxTerminalExtraKeys.getExtraKeysInfo() == null ? 0 : mTermuxTerminalExtraKeys.getExtraKeysInfo().getMatrix().length) *
|
||||
mProperties.getTerminalToolbarHeightScaleFactor());
|
||||
terminalToolbarViewPager.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
public void toggleTerminalToolbar() {
|
||||
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
|
||||
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
||||
if (terminalToolbarViewPager == null) return;
|
||||
|
||||
final boolean showNow = mPreferences.toogleShowTerminalToolbar();
|
||||
Logger.showToast(this, (showNow ? getString(R.string.msg_enabling_terminal_toolbar) : getString(R.string.msg_disabling_terminal_toolbar)), true);
|
||||
terminalToolbarViewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
|
||||
if (showNow && terminalToolbarViewPager.getCurrentItem() == 1) {
|
||||
if (showNow && isTerminalToolbarTextInputViewSelected()) {
|
||||
// Focus the text input view if just revealed.
|
||||
findViewById(R.id.terminal_toolbar_text_input).requestFocus();
|
||||
}
|
||||
@@ -451,7 +555,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
private void saveTerminalToolbarTextInput(Bundle savedInstanceState) {
|
||||
if (savedInstanceState == null) return;
|
||||
|
||||
final EditText textInputView = findViewById(R.id.terminal_toolbar_text_input);
|
||||
final EditText textInputView = findViewById(R.id.terminal_toolbar_text_input);
|
||||
if (textInputView != null) {
|
||||
String textInput = textInputView.getText().toString();
|
||||
if (!textInput.isEmpty()) savedInstanceState.putString(ARG_TERMINAL_TOOLBAR_TEXT_INPUT, textInput);
|
||||
@@ -460,13 +564,20 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
|
||||
|
||||
private void setSettingsButtonView() {
|
||||
ImageButton settingsButton = findViewById(R.id.settings_button);
|
||||
settingsButton.setOnClickListener(v -> {
|
||||
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
||||
});
|
||||
}
|
||||
|
||||
private void setNewSessionButtonView() {
|
||||
View newSessionButton = findViewById(R.id.new_session_button);
|
||||
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionClient.addNewSession(false, null));
|
||||
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionActivityClient.addNewSession(false, null));
|
||||
newSessionButton.setOnLongClickListener(v -> {
|
||||
DialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
||||
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionClient.addNewSession(false, text),
|
||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
|
||||
TextInputDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
||||
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionActivityClient.addNewSession(false, text),
|
||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionActivityClient.addNewSession(true, text),
|
||||
-1, null, null);
|
||||
return true;
|
||||
});
|
||||
@@ -531,7 +642,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript);
|
||||
if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password);
|
||||
if (!DataUtils.isNullOrEmpty(mTerminalView.getStoredSelectedText()))
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_SHARE_SELECTED_TEXT, Menu.NONE, R.string.action_share_selected_text);
|
||||
if (addAutoFillMenu)
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal);
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning());
|
||||
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
|
||||
@@ -559,11 +673,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
case CONTEXT_MENU_SHARE_TRANSCRIPT_ID:
|
||||
mTermuxTerminalViewClient.shareSessionTranscript();
|
||||
return true;
|
||||
case CONTEXT_MENU_SHARE_SELECTED_TEXT:
|
||||
mTermuxTerminalViewClient.shareSelectedText();
|
||||
return true;
|
||||
case CONTEXT_MENU_AUTOFILL_ID:
|
||||
requestAutoFill();
|
||||
return true;
|
||||
case CONTEXT_MENU_RESET_TERMINAL_ID:
|
||||
resetSession(session);
|
||||
onResetTerminalSession(session);
|
||||
return true;
|
||||
case CONTEXT_MENU_KILL_PROCESS_ID:
|
||||
showKillSessionDialog(session);
|
||||
@@ -575,10 +692,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
toggleKeepScreenOn();
|
||||
return true;
|
||||
case CONTEXT_MENU_HELP_ID:
|
||||
startActivity(new Intent(this, HelpActivity.class));
|
||||
ActivityUtils.startActivity(this, new Intent(this, HelpActivity.class));
|
||||
return true;
|
||||
case CONTEXT_MENU_SETTINGS_ID:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
case CONTEXT_MENU_REPORT_ID:
|
||||
mTermuxTerminalViewClient.reportIssueFromTranscript();
|
||||
@@ -588,6 +705,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
super.onContextMenuClosed(menu);
|
||||
// onContextMenuClosed() is triggered twice if back button is pressed to dismiss instead of tap for some reason
|
||||
mTerminalView.onContextMenuClosed(menu);
|
||||
}
|
||||
|
||||
private void showKillSessionDialog(TerminalSession session) {
|
||||
if (session == null) return;
|
||||
|
||||
@@ -602,10 +726,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
b.show();
|
||||
}
|
||||
|
||||
private void resetSession(TerminalSession session) {
|
||||
private void onResetTerminalSession(TerminalSession session) {
|
||||
if (session != null) {
|
||||
session.reset();
|
||||
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onResetTerminalSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,7 +745,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
// The startActivity() call is not documented to throw IllegalArgumentException.
|
||||
// However, crash reporting shows that it sometimes does, so catch it here.
|
||||
new AlertDialog.Builder(this).setMessage(getString(R.string.error_styling_not_installed))
|
||||
.setPositiveButton(R.string.action_styling_install, (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(TermuxConstants.TERMUX_STYLING_FDROID_PACKAGE_URL)))).setNegativeButton(android.R.string.cancel, null).show();
|
||||
.setPositiveButton(R.string.action_styling_install,
|
||||
(dialog, which) -> ActivityUtils.startActivity(this, new Intent(Intent.ACTION_VIEW, Uri.parse(TermuxConstants.TERMUX_STYLING_FDROID_PACKAGE_URL))))
|
||||
.setNegativeButton(android.R.string.cancel, null).show();
|
||||
}
|
||||
}
|
||||
private void toggleKeepScreenOn() {
|
||||
@@ -643,25 +772,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 primary external storage (/sdcard, /storage/emulated/0, ~/storage/shared),
|
||||
* termux needs to be granted legacy WRITE_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE permissions
|
||||
* if targeting targetSdkVersion 30 (android 11) and running on sdk 30 (android 11) and higher.
|
||||
*/
|
||||
public boolean ensureStoragePermissionGranted() {
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
} else {
|
||||
Logger.logDebug(LOG_TAG, "Storage permission not granted, requesting permission.");
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
|
||||
return false;
|
||||
public void requestStoragePermission(boolean isPermissionCallback) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Do not ask for permission again
|
||||
int requestCode = isPermissionCallback ? -1 : PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION;
|
||||
|
||||
// If permission is granted, then also setup storage symlinks.
|
||||
if(PermissionUtils.checkAndRequestLegacyOrManageExternalStoragePermission(
|
||||
TermuxActivity.this, requestCode, !isPermissionCallback)) {
|
||||
if (isPermissionCallback)
|
||||
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
|
||||
getString(com.termux.shared.R.string.msg_storage_permission_granted_on_request));
|
||||
|
||||
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
||||
} else {
|
||||
if (isPermissionCallback)
|
||||
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
|
||||
getString(com.termux.shared.R.string.msg_storage_permission_not_granted_on_request));
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
Logger.logVerbose(LOG_TAG, "onActivityResult: requestCode: " + requestCode + ", resultCode: " + resultCode + ", data: " + IntentUtils.getIntentString(data));
|
||||
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
|
||||
requestStoragePermission(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Logger.logDebug(LOG_TAG, "Storage permission granted by user on request.");
|
||||
TermuxInstaller.setupStorageSymlinks(this);
|
||||
} else {
|
||||
Logger.logDebug(LOG_TAG, "Storage permission denied by user on request.");
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
Logger.logVerbose(LOG_TAG, "onRequestPermissionsResult: requestCode: " + requestCode + ", permissions: " + Arrays.toString(permissions) + ", grantResults: " + Arrays.toString(grantResults));
|
||||
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
|
||||
requestStoragePermission(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,10 +824,22 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
return mNavBarHeight;
|
||||
}
|
||||
|
||||
public TermuxActivityRootView getTermuxActivityRootView() {
|
||||
return mTermuxActivityRootView;
|
||||
}
|
||||
|
||||
public View getTermuxActivityBottomSpaceView() {
|
||||
return mTermuxActivityBottomSpaceView;
|
||||
}
|
||||
|
||||
public ExtraKeysView getExtraKeysView() {
|
||||
return mExtraKeysView;
|
||||
}
|
||||
|
||||
public TermuxTerminalExtraKeys getTermuxTerminalExtraKeys() {
|
||||
return mTermuxTerminalExtraKeys;
|
||||
}
|
||||
|
||||
public void setExtraKeysView(ExtraKeysView extraKeysView) {
|
||||
mExtraKeysView = extraKeysView;
|
||||
}
|
||||
@@ -683,6 +848,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
return (DrawerLayout) findViewById(R.id.drawer_layout);
|
||||
}
|
||||
|
||||
|
||||
public ViewPager getTerminalToolbarViewPager() {
|
||||
return (ViewPager) findViewById(R.id.terminal_toolbar_view_pager);
|
||||
}
|
||||
|
||||
public float getTerminalToolbarDefaultHeight() {
|
||||
return mTerminalToolbarDefaultHeight;
|
||||
}
|
||||
|
||||
public boolean isTerminalViewSelected() {
|
||||
return getTerminalToolbarViewPager().getCurrentItem() == 0;
|
||||
}
|
||||
|
||||
public boolean isTerminalToolbarTextInputViewSelected() {
|
||||
return getTerminalToolbarViewPager().getCurrentItem() == 1;
|
||||
}
|
||||
|
||||
|
||||
public void termuxSessionListNotifyUpdated() {
|
||||
mTermuxSessionListViewController.notifyDataSetChanged();
|
||||
}
|
||||
@@ -691,6 +874,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
return mIsVisible;
|
||||
}
|
||||
|
||||
public boolean isOnResumeAfterOnCreate() {
|
||||
return mIsOnResumeAfterOnCreate;
|
||||
}
|
||||
|
||||
public boolean isActivityRecreated() {
|
||||
return mIsActivityRecreated;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public TermuxService getTermuxService() {
|
||||
@@ -705,8 +896,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
return mTermuxTerminalViewClient;
|
||||
}
|
||||
|
||||
public TermuxTerminalSessionClient getTermuxTerminalSessionClient() {
|
||||
return mTermuxTerminalSessionClient;
|
||||
public TermuxTerminalSessionActivityClient getTermuxTerminalSessionClient() {
|
||||
return mTermuxTerminalSessionActivityClient;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -728,25 +919,27 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
|
||||
|
||||
public static void updateTermuxActivityStyling(Context context) {
|
||||
public static void updateTermuxActivityStyling(Context context, boolean recreateActivity) {
|
||||
// Make sure that terminal styling is always applied.
|
||||
Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
||||
stylingIntent.putExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, recreateActivity);
|
||||
context.sendBroadcast(stylingIntent);
|
||||
}
|
||||
|
||||
private void registerTermuxActivityBroadcastReceiver() {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_NOTIFY_APP_CRASH);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
||||
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS);
|
||||
|
||||
registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter);
|
||||
}
|
||||
|
||||
private void unregisterTermuxActivityBroadcastReceiever() {
|
||||
private void unregisterTermuxActivityBroadcastReceiver() {
|
||||
unregisterReceiver(mTermuxActivityBroadcastReceiver);
|
||||
}
|
||||
|
||||
private void fixTermuxActivityBroadcastReceieverIntent(Intent intent) {
|
||||
private void fixTermuxActivityBroadcastReceiverIntent(Intent intent) {
|
||||
if (intent == null) return;
|
||||
|
||||
String extraReloadStyle = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE);
|
||||
@@ -762,17 +955,20 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
if (intent == null) return;
|
||||
|
||||
if (mIsVisible) {
|
||||
fixTermuxActivityBroadcastReceieverIntent(intent);
|
||||
fixTermuxActivityBroadcastReceiverIntent(intent);
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
||||
if (ensureStoragePermissionGranted())
|
||||
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
||||
case TERMUX_ACTIVITY.ACTION_NOTIFY_APP_CRASH:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to notify app crash");
|
||||
TermuxCrashUtils.notifyAppCrashFromCrashLogFile(context, LOG_TAG);
|
||||
return;
|
||||
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
|
||||
reloadActivityStyling();
|
||||
reloadActivityStyling(intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, true));
|
||||
return;
|
||||
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
||||
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
||||
requestStoragePermission(false);
|
||||
return;
|
||||
default:
|
||||
}
|
||||
@@ -780,36 +976,43 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadActivityStyling() {
|
||||
if (mProperties!= null) {
|
||||
mProperties.loadTermuxPropertiesFromDisk();
|
||||
private void reloadActivityStyling(boolean recreateActivity) {
|
||||
if (mProperties != null) {
|
||||
reloadProperties();
|
||||
|
||||
if (mExtraKeysView != null) {
|
||||
mExtraKeysView.reload(mProperties.getExtraKeysInfo());
|
||||
mExtraKeysView.setButtonTextAllCaps(mProperties.shouldExtraKeysTextBeAllCaps());
|
||||
mExtraKeysView.reload(mTermuxTerminalExtraKeys.getExtraKeysInfo(), mTerminalToolbarDefaultHeight);
|
||||
}
|
||||
|
||||
// Update NightMode.APP_NIGHT_MODE
|
||||
TermuxThemeUtils.setAppNightMode(mProperties.getNightMode());
|
||||
}
|
||||
|
||||
setMargins();
|
||||
setTerminalToolbarHeight();
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onReload();
|
||||
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
|
||||
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onReloadActivityStyling();
|
||||
|
||||
if (mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onReload();
|
||||
mTermuxTerminalViewClient.onReloadActivityStyling();
|
||||
|
||||
// To change the activity and drawer theme, activity needs to be recreated.
|
||||
// But this will destroy the activity, and will call the onCreate() again.
|
||||
// We need to investigate if enabling this is wise, since all stored variables and
|
||||
// views will be destroyed and bindService() will be called again. Extra keys input
|
||||
// text will we restored since that has already been implemented. Terminal sessions
|
||||
// and transcripts are also already preserved. Theme does change properly too.
|
||||
// TermuxActivity.this.recreate();
|
||||
// It will destroy the activity, including all stored variables and views, and onCreate()
|
||||
// will be called again. Extra keys input text, terminal sessions and transcripts will be preserved.
|
||||
if (recreateActivity) {
|
||||
Logger.logDebug(LOG_TAG, "Recreating activity");
|
||||
TermuxActivity.this.recreate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void startTermuxActivity(@NonNull final Context context) {
|
||||
context.startActivity(newInstance(context));
|
||||
ActivityUtils.startActivity(context, newInstance(context));
|
||||
}
|
||||
|
||||
public static Intent newInstance(@NonNull final Context context) {
|
||||
|
||||
@@ -1,29 +1,85 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.shared.crash.CrashHandler;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.BuildConfig;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import com.termux.shared.termux.TermuxBootstrap;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
import com.termux.shared.termux.shell.am.TermuxAmSocketServer;
|
||||
import com.termux.shared.termux.shell.TermuxShellManager;
|
||||
import com.termux.shared.termux.theme.TermuxThemeUtils;
|
||||
|
||||
public class TermuxApplication extends Application {
|
||||
|
||||
private static final String LOG_TAG = "TermuxApplication";
|
||||
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Set crash handler for the app
|
||||
CrashHandler.setCrashHandler(this);
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Set log level for the app
|
||||
setLogLevel();
|
||||
// Set crash handler for the app
|
||||
TermuxCrashUtils.setDefaultCrashHandler(this);
|
||||
|
||||
// Set log config for the app
|
||||
setLogConfig(context);
|
||||
|
||||
Logger.logDebug("Starting Application");
|
||||
|
||||
// Set TermuxBootstrap.TERMUX_APP_PACKAGE_MANAGER and TermuxBootstrap.TERMUX_APP_PACKAGE_VARIANT
|
||||
TermuxBootstrap.setTermuxPackageManagerAndVariant(BuildConfig.TERMUX_PACKAGE_VARIANT);
|
||||
|
||||
// Init app wide SharedProperties loaded from termux.properties
|
||||
TermuxAppSharedProperties properties = TermuxAppSharedProperties.init(context);
|
||||
|
||||
// Init app wide shell manager
|
||||
TermuxShellManager shellManager = TermuxShellManager.init(context);
|
||||
|
||||
// Set NightMode.APP_NIGHT_MODE
|
||||
TermuxThemeUtils.setAppNightMode(properties.getNightMode());
|
||||
|
||||
// Check and create termux files directory. If failed to access it like in case of secondary
|
||||
// user or external sd card installation, then don't run files directory related code
|
||||
Error error = TermuxFileUtils.isTermuxFilesDirectoryAccessible(this, true, true);
|
||||
boolean isTermuxFilesDirectoryAccessible = error == null;
|
||||
if (isTermuxFilesDirectoryAccessible) {
|
||||
Logger.logInfo(LOG_TAG, "Termux files directory is accessible");
|
||||
|
||||
error = TermuxFileUtils.isAppsTermuxAppDirectoryAccessible(true, true);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, "Create apps/termux-app directory failed\n" + error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup termux-am-socket server
|
||||
TermuxAmSocketServer.setupTermuxAmSocketServer(context);
|
||||
} else {
|
||||
Logger.logErrorExtended(LOG_TAG, "Termux files directory is not accessible\n" + error);
|
||||
}
|
||||
|
||||
// Init TermuxShellEnvironment constants and caches after everything has been setup including termux-am-socket server
|
||||
TermuxShellEnvironment.init(this);
|
||||
|
||||
if (isTermuxFilesDirectoryAccessible) {
|
||||
TermuxShellEnvironment.writeEnvironmentToFile(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void setLogLevel() {
|
||||
public static void setLogConfig(Context context) {
|
||||
Logger.setDefaultLogTag(TermuxConstants.TERMUX_APP_NAME);
|
||||
|
||||
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(getApplicationContext());
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||
Logger.logDebug("Starting Application");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,17 +4,24 @@ import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.UserManager;
|
||||
import android.system.Os;
|
||||
import android.util.Pair;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.termux.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.errors.Error;
|
||||
import com.termux.shared.android.PackageUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -26,6 +33,11 @@ import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR;
|
||||
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
||||
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR;
|
||||
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||
|
||||
/**
|
||||
* Install the Termux bootstrap packages if necessary by following the below steps:
|
||||
* <p/>
|
||||
@@ -51,34 +63,55 @@ final class TermuxInstaller {
|
||||
|
||||
/** Performs bootstrap setup if necessary. */
|
||||
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
|
||||
String bootstrapErrorMessage;
|
||||
Error filesDirectoryAccessibleError;
|
||||
|
||||
// This will also call Context.getFilesDir(), which should ensure that termux files directory
|
||||
// is created if it does not already exist
|
||||
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
|
||||
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
|
||||
|
||||
// Termux can only be run as the primary user (device owner) since only that
|
||||
// account has the expected file system paths. Verify that:
|
||||
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
|
||||
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
|
||||
if (!isPrimaryUser) {
|
||||
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PackageUtils.isCurrentUserThePrimaryUser(activity)) {
|
||||
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message,
|
||||
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
|
||||
Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
|
||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||
DialogUtils.exitAppWithErrorMessage(activity,
|
||||
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
||||
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
||||
activity.getString(R.string.bootstrap_error_title),
|
||||
bootstrapErrorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
||||
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
|
||||
if (!isFilesDirectoryAccessible) {
|
||||
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError);
|
||||
//noinspection SdCardPath
|
||||
if (PackageUtils.isAppInstalledOnExternalStorage(activity) &&
|
||||
!TermuxConstants.TERMUX_FILES_DIR_PATH.equals(activity.getFilesDir().getAbsolutePath().replaceAll("^/data/user/0/", "/data/data/"))) {
|
||||
bootstrapErrorMessage += "\n\n" + activity.getString(R.string.bootstrap_error_installed_on_portable_sd,
|
||||
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
|
||||
}
|
||||
|
||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
||||
MessageDialogUtils.showMessage(activity,
|
||||
activity.getString(R.string.bootstrap_error_title),
|
||||
bootstrapErrorMessage, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
||||
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
||||
// If prefix directory is empty or only contains the tmp directory
|
||||
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
|
||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||
} else {
|
||||
whenDone.run();
|
||||
return;
|
||||
}
|
||||
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
||||
if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
|
||||
if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) {
|
||||
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains specific unimportant files.");
|
||||
} else {
|
||||
whenDone.run();
|
||||
return;
|
||||
}
|
||||
} else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) {
|
||||
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" does not exist but another file exists at its destination.");
|
||||
}
|
||||
|
||||
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
||||
@@ -88,24 +121,37 @@ final class TermuxInstaller {
|
||||
try {
|
||||
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
||||
|
||||
String errmsg;
|
||||
|
||||
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
||||
Error error;
|
||||
|
||||
// Delete prefix staging directory or any file at its destination
|
||||
errmsg = FileUtils.deleteFile(activity, "prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||
if (errmsg != null) {
|
||||
throw new RuntimeException(errmsg);
|
||||
error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete prefix directory or any file at its destination
|
||||
errmsg = FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true);
|
||||
if (errmsg != null) {
|
||||
throw new RuntimeException(errmsg);
|
||||
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
||||
// Create prefix staging directory if it does not already exist and set required permissions
|
||||
error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true);
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create prefix directory if it does not already exist and set required permissions
|
||||
error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true);
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
|
||||
|
||||
final byte[] buffer = new byte[8096];
|
||||
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
||||
@@ -122,17 +168,25 @@ final class TermuxInstaller {
|
||||
if (parts.length != 2)
|
||||
throw new RuntimeException("Malformed symlink line: " + line);
|
||||
String oldPath = parts[0];
|
||||
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
||||
String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1];
|
||||
symlinks.add(Pair.create(oldPath, newPath));
|
||||
|
||||
ensureDirectoryExists(activity, new File(newPath).getParentFile());
|
||||
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String zipEntryName = zipEntry.getName();
|
||||
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
||||
File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);
|
||||
boolean isDirectory = zipEntry.isDirectory();
|
||||
|
||||
ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile());
|
||||
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDirectory) {
|
||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||
@@ -140,7 +194,8 @@ final class TermuxInstaller {
|
||||
while ((readBytes = zipInput.read(buffer)) != -1)
|
||||
outStream.write(buffer, 0, readBytes);
|
||||
}
|
||||
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
|
||||
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
|
||||
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods")) {
|
||||
//noinspection OctalInteger
|
||||
Os.chmod(targetFile.getAbsolutePath(), 0700);
|
||||
}
|
||||
@@ -155,30 +210,22 @@ final class TermuxInstaller {
|
||||
Os.symlink(symlink.first, symlink.second);
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
|
||||
Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory.");
|
||||
|
||||
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
|
||||
throw new RuntimeException("Moving prefix staging to prefix directory failed");
|
||||
if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) {
|
||||
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||
|
||||
// Recreate env file since termux prefix was wiped earlier
|
||||
TermuxShellEnvironment.writeEnvironmentToFile(activity);
|
||||
|
||||
activity.runOnUiThread(whenDone);
|
||||
|
||||
} catch (final Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
|
||||
activity.runOnUiThread(() -> {
|
||||
try {
|
||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
activity.finish();
|
||||
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||
}).show();
|
||||
} catch (WindowManager.BadTokenException e1) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
});
|
||||
showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
||||
|
||||
} finally {
|
||||
activity.runOnUiThread(() -> {
|
||||
try {
|
||||
@@ -192,28 +239,72 @@ final class TermuxInstaller {
|
||||
}.start();
|
||||
}
|
||||
|
||||
public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
|
||||
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
||||
|
||||
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
||||
sendBootstrapCrashReportNotification(activity, message);
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
try {
|
||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
activity.finish();
|
||||
})
|
||||
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
||||
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||
}).show();
|
||||
} catch (WindowManager.BadTokenException e1) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
|
||||
final String title = TermuxConstants.TERMUX_APP_NAME + " Bootstrap Error";
|
||||
|
||||
// Add info of all install Termux plugin apps as well since their target sdk or installation
|
||||
// on external/portable sd card can affect Termux app files directory access or exec.
|
||||
TermuxCrashUtils.sendCrashReportNotification(activity, LOG_TAG,
|
||||
title, null, "## " + title + "\n\n" + message + "\n\n" +
|
||||
TermuxUtils.getTermuxDebugMarkdownString(activity),
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES, true);
|
||||
}
|
||||
|
||||
static void setupStorageSymlinks(final Context context) {
|
||||
final String LOG_TAG = "termux-storage";
|
||||
final String title = TermuxConstants.TERMUX_APP_NAME + " Setup Storage Error";
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
||||
|
||||
new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
String errmsg;
|
||||
Error error;
|
||||
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||
|
||||
errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath());
|
||||
if (errmsg != null) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
|
||||
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
|
||||
if (error != null) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
||||
"## " + title + "\n\n" + Error.getErrorMarkdownString(error),
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
|
||||
|
||||
// Get primary storage root "/storage/emulated/0" symlink
|
||||
File sharedDir = Environment.getExternalStorageDirectory();
|
||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
||||
|
||||
File documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
|
||||
Os.symlink(documentsDir.getAbsolutePath(), new File(storageDir, "documents").getAbsolutePath());
|
||||
|
||||
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
|
||||
|
||||
@@ -229,9 +320,25 @@ final class TermuxInstaller {
|
||||
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
||||
|
||||
final File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs != null && dirs.length > 1) {
|
||||
for (int i = 1; i < dirs.length; i++) {
|
||||
File podcastsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PODCASTS);
|
||||
Os.symlink(podcastsDir.getAbsolutePath(), new File(storageDir, "podcasts").getAbsolutePath());
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
File audiobooksDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_AUDIOBOOKS);
|
||||
Os.symlink(audiobooksDir.getAbsolutePath(), new File(storageDir, "audiobooks").getAbsolutePath());
|
||||
}
|
||||
|
||||
// Dir 0 should ideally be for primary storage
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ContextImpl.java;l=818
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java;l=219
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java;l=181
|
||||
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/StorageManagerService.java;l=3796
|
||||
// https://cs.android.com/android/platform/superproject/+/android-7.0.0_r36:frameworks/base/services/core/java/com/android/server/MountService.java;l=3053
|
||||
|
||||
// Create "Android/data/com.termux" symlinks
|
||||
File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs != null && dirs.length > 0) {
|
||||
for (int i = 0; i < dirs.length; i++) {
|
||||
File dir = dirs[i];
|
||||
if (dir == null) continue;
|
||||
String symlinkName = "external-" + i;
|
||||
@@ -240,21 +347,32 @@ final class TermuxInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
// Create "Android/media/com.termux" symlinks
|
||||
dirs = context.getExternalMediaDirs();
|
||||
if (dirs != null && dirs.length > 0) {
|
||||
for (int i = 0; i < dirs.length; i++) {
|
||||
File dir = dirs[i];
|
||||
if (dir == null) continue;
|
||||
String symlinkName = "media-" + i;
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
|
||||
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
||||
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
||||
"## " + title + "\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)),
|
||||
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private static void ensureDirectoryExists(Context context, File directory) {
|
||||
String errmsg;
|
||||
|
||||
errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath());
|
||||
if (errmsg != null) {
|
||||
throw new RuntimeException(errmsg);
|
||||
}
|
||||
private static Error ensureDirectoryExists(File directory) {
|
||||
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
|
||||
}
|
||||
|
||||
public static byte[] loadZipBytes() {
|
||||
|
||||
@@ -13,7 +13,12 @@ import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.net.uri.UriUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.net.uri.UriScheme;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.File;
|
||||
@@ -30,11 +35,13 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final Uri data = intent.getData();
|
||||
if (data == null) {
|
||||
Logger.logError(LOG_TAG, "termux-open: Called without intent data");
|
||||
Logger.logError(LOG_TAG, "Called without intent data");
|
||||
return;
|
||||
}
|
||||
|
||||
final String filePath = data.getPath();
|
||||
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
Logger.logVerbose(LOG_TAG, "uri: \"" + data + "\", path: \"" + data.getPath() + "\", fragment: \"" + data.getFragment() + "\"");
|
||||
|
||||
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();
|
||||
@@ -48,8 +55,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
break;
|
||||
}
|
||||
|
||||
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
|
||||
if (isExternalUrl) {
|
||||
String scheme = data.getScheme();
|
||||
if (scheme != null && !UriScheme.SCHEME_FILE.equals(scheme)) {
|
||||
Intent urlIntent = new Intent(intentAction, data);
|
||||
if (intentAction.equals(Intent.ACTION_SEND)) {
|
||||
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
|
||||
@@ -61,14 +68,21 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
try {
|
||||
context.startActivity(urlIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
Logger.logError(LOG_TAG, "No app handles the url " + data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get full path including fragment (anything after last "#")
|
||||
String filePath = UriUtils.getUriFilePathWithFragment(data);
|
||||
if (DataUtils.isNullOrEmpty(filePath)) {
|
||||
Logger.logError(LOG_TAG, "filePath is null or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
final File fileToShare = new File(filePath);
|
||||
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
||||
Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
Logger.logError(LOG_TAG, "Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,7 +103,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
contentTypeToUse = contentTypeExtra;
|
||||
}
|
||||
|
||||
Uri uriToShare = Uri.parse("content://" + TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY + fileToShare.getAbsolutePath());
|
||||
// Do not create Uri with Uri.parse() and use Uri.Builder().path(), check UriUtils.getUriFilePath().
|
||||
Uri uriToShare = UriUtils.getContentUri(TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY, fileToShare.getAbsolutePath());
|
||||
|
||||
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||
@@ -105,12 +120,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
try {
|
||||
context.startActivity(sendIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
Logger.logError(LOG_TAG, "No app handles the url " + data);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ContentProvider extends android.content.ContentProvider {
|
||||
|
||||
private static final String LOG_TAG = "TermuxContentProvider";
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
@@ -178,15 +195,33 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
File file = new File(uri.getPath());
|
||||
try {
|
||||
String path = file.getCanonicalPath();
|
||||
String callingPackageName = getCallingPackage();
|
||||
Logger.logDebug(LOG_TAG, "Open file request received from " + callingPackageName + " for \"" + path + "\" with mode \"" + mode + "\"");
|
||||
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||
// See https://support.google.com/faqs/answer/7496913:
|
||||
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
|
||||
throw new IllegalArgumentException("Invalid path: " + path);
|
||||
}
|
||||
|
||||
// If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", then throw exception
|
||||
String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
|
||||
if (errmsg != null) {
|
||||
throw new IllegalArgumentException(errmsg);
|
||||
}
|
||||
|
||||
// **DO NOT** allow these files to be modified by ContentProvider exposed to external
|
||||
// apps, since they may silently modify the values for security properties like
|
||||
// TermuxConstants.PROP_ALLOW_EXTERNAL_APPS set by users without their explicit consent.
|
||||
if (TermuxConstants.TERMUX_PROPERTIES_FILE_PATHS_LIST.contains(path) ||
|
||||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_FILE_PATHS_LIST.contains(path)) {
|
||||
mode = "r";
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,36 +5,46 @@ import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.app.event.SystemEventReceiver;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionServiceClient;
|
||||
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.net.uri.UriUtils;
|
||||
import com.termux.shared.errors.Errno;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.shell.command.runner.app.AppShell;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
import com.termux.shared.termux.shell.TermuxShellUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.shell.TermuxShellManager;
|
||||
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
|
||||
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.packages.PermissionUtils;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.android.PermissionUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.shell.TermuxTask;
|
||||
import com.termux.shared.shell.command.ExecutionCommand;
|
||||
import com.termux.shared.shell.command.ExecutionCommand.Runner;
|
||||
import com.termux.shared.shell.command.ExecutionCommand.ShellCreateMode;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
@@ -42,11 +52,9 @@ import com.termux.terminal.TerminalSessionClient;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A service holding a list of {@link TermuxSession} in {@link #mTermuxSessions} and background {@link TermuxTask}
|
||||
* in {@link #mTermuxTasks}, showing a foreground notification while running so that it is not terminated.
|
||||
* A service holding a list of {@link TermuxSession} in {@link TermuxShellManager#mTermuxSessions} and background {@link AppShell}
|
||||
* in {@link TermuxShellManager#mTermuxTasks}, showing a foreground notification while running so that it is not terminated.
|
||||
* The user interacts with the session through {@link TermuxActivity}, but this service may outlive
|
||||
* the activity when the user or the system disposes of the activity. In that case the user may
|
||||
* restart {@link TermuxActivity} later to yet again access the sessions.
|
||||
@@ -57,9 +65,7 @@ import javax.annotation.Nullable;
|
||||
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
|
||||
* {@link #buildNotification()}.
|
||||
*/
|
||||
public final class TermuxService extends Service implements TermuxTask.TermuxTaskClient, TermuxSession.TermuxSessionClient {
|
||||
|
||||
private static int EXECUTION_ID = 1000;
|
||||
public final class TermuxService extends Service implements AppShell.AppShellClient, TermuxSession.TermuxSessionClient {
|
||||
|
||||
/** This service is only bound from inside the same process and never uses IPC. */
|
||||
class LocalBinder extends Binder {
|
||||
@@ -70,34 +76,27 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
private final Handler mHandler = new Handler();
|
||||
|
||||
/**
|
||||
* The foreground TermuxSessions which this service manages.
|
||||
* Note that this list is observed by {@link TermuxActivity#mTermuxSessionListViewController},
|
||||
* so any changes must be made on the UI thread and followed by a call to
|
||||
* {@link ArrayAdapter#notifyDataSetChanged()} }.
|
||||
*/
|
||||
final List<TermuxSession> mTermuxSessions = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The background TermuxTasks which this service manages.
|
||||
*/
|
||||
final List<TermuxTask> mTermuxTasks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The pending plugin ExecutionCommands that have yet to be processed by this service.
|
||||
*/
|
||||
final List<ExecutionCommand> mPendingPluginExecutionCommands = new ArrayList<>();
|
||||
|
||||
/** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||
* that holds activity references for activity related functions.
|
||||
* Note that the service may often outlive the activity, so need to clear this reference.
|
||||
*/
|
||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
private TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
|
||||
/** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||
* that does not hold activity references.
|
||||
* that does not hold activity references and only a service reference.
|
||||
*/
|
||||
final TermuxTerminalSessionClientBase mTermuxTerminalSessionClientBase = new TermuxTerminalSessionClientBase();
|
||||
private final TermuxTerminalSessionServiceClient mTermuxTerminalSessionServiceClient = new TermuxTerminalSessionServiceClient(this);
|
||||
|
||||
/**
|
||||
* Termux app shared properties manager, loaded from termux.properties
|
||||
*/
|
||||
private TermuxAppSharedProperties mProperties;
|
||||
|
||||
/**
|
||||
* Termux app shell manager
|
||||
*/
|
||||
private TermuxShellManager mShellManager;
|
||||
|
||||
/** The wake lock and wifi lock are always acquired and released together. */
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
@@ -111,7 +110,16 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||
|
||||
// Get Termux app SharedProperties without loading from disk since TermuxApplication handles
|
||||
// load and TermuxActivity handles reloads
|
||||
mProperties = TermuxAppSharedProperties.getProperties();
|
||||
|
||||
mShellManager = TermuxShellManager.getShellManager();
|
||||
|
||||
runStartForeground();
|
||||
|
||||
SystemEventReceiver.registerPackageUpdateEvents(this);
|
||||
}
|
||||
|
||||
@SuppressLint("Wakelock")
|
||||
@@ -122,7 +130,11 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
// Run again in case service is already started and onCreate() is not called
|
||||
runStartForeground();
|
||||
|
||||
String action = intent.getAction();
|
||||
String action = null;
|
||||
if (intent != null) {
|
||||
Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
action = intent.getAction();
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
switch (action) {
|
||||
@@ -157,11 +169,16 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
public void onDestroy() {
|
||||
Logger.logVerbose(LOG_TAG, "onDestroy");
|
||||
|
||||
ShellUtils.clearTermuxTMPDIR(this, true);
|
||||
TermuxShellUtils.clearTermuxTMPDIR(true);
|
||||
|
||||
actionReleaseWakeLock(false);
|
||||
if (!mWantsToStop)
|
||||
killAllTermuxExecutionCommands();
|
||||
|
||||
TermuxShellManager.onAppExit(this);
|
||||
|
||||
SystemEventReceiver.unregisterPackageUpdateEvents(this);
|
||||
|
||||
runStopForeground();
|
||||
}
|
||||
|
||||
@@ -178,7 +195,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
// Since we cannot rely on {@link TermuxActivity.onDestroy()} to always complete,
|
||||
// we unset clients here as well if it failed, so that we do not leave service and session
|
||||
// clients with references to the activity.
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
unsetTermuxTerminalSessionClient();
|
||||
return false;
|
||||
}
|
||||
@@ -246,28 +263,36 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
private synchronized void killAllTermuxExecutionCommands() {
|
||||
boolean processResult;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mTermuxSessions.size() + ", TermuxTasks=" + mTermuxTasks.size() + ", PendingPluginExecutionCommands=" + mPendingPluginExecutionCommands.size());
|
||||
Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mShellManager.mTermuxSessions.size() +
|
||||
", TermuxTasks=" + mShellManager.mTermuxTasks.size() +
|
||||
", PendingPluginExecutionCommands=" + mShellManager.mPendingPluginExecutionCommands.size());
|
||||
|
||||
List<TermuxSession> termuxSessions = new ArrayList<>(mShellManager.mTermuxSessions);
|
||||
List<AppShell> termuxTasks = new ArrayList<>(mShellManager.mTermuxTasks);
|
||||
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mShellManager.mPendingPluginExecutionCommands);
|
||||
|
||||
List<TermuxSession> termuxSessions = new ArrayList<>(mTermuxSessions);
|
||||
for (int i = 0; i < termuxSessions.size(); i++) {
|
||||
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
|
||||
processResult = mWantsToStop || (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null);
|
||||
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||
termuxSessions.get(i).killIfExecuting(this, processResult);
|
||||
if (!processResult)
|
||||
mShellManager.mTermuxSessions.remove(termuxSessions.get(i));
|
||||
}
|
||||
|
||||
List<TermuxTask> termuxTasks = new ArrayList<>(mTermuxTasks);
|
||||
|
||||
for (int i = 0; i < termuxTasks.size(); i++) {
|
||||
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
|
||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null)
|
||||
if (executionCommand.isPluginExecutionCommandWithPendingResult())
|
||||
termuxTasks.get(i).killIfExecuting(this, true);
|
||||
else
|
||||
mShellManager.mTermuxTasks.remove(termuxTasks.get(i));
|
||||
}
|
||||
|
||||
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands);
|
||||
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
|
||||
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
|
||||
if (!executionCommand.shouldNotProcessResults() && executionCommand.pluginPendingIntent != null) {
|
||||
if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_CANCELED, this.getString(com.termux.shared.R.string.error_execution_cancelled), null)) {
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
|
||||
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,18 +319,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
|
||||
mWifiLock.acquire();
|
||||
|
||||
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) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e);
|
||||
}
|
||||
if (!PermissionUtils.checkIfBatteryOptimizationsDisabled(this)) {
|
||||
PermissionUtils.requestDisableBatteryOptimizations(this);
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
@@ -347,35 +362,65 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
return;
|
||||
}
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(getNextExecutionId());
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId());
|
||||
|
||||
executionCommand.executableUri = intent.getData();
|
||||
executionCommand.inBackground = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false);
|
||||
executionCommand.isPluginExecutionCommand = true;
|
||||
|
||||
if (executionCommand.executableUri != null) {
|
||||
executionCommand.executable = executionCommand.executableUri.getPath();
|
||||
executionCommand.arguments = intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS);
|
||||
if (executionCommand.inBackground)
|
||||
executionCommand.stdin = intent.getStringExtra(TERMUX_SERVICE.EXTRA_STDIN);
|
||||
// If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION
|
||||
executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RUNNER,
|
||||
(intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName()));
|
||||
if (Runner.runnerOf(executionCommand.runner) == null) {
|
||||
String errmsg = this.getString(R.string.error_termux_service_invalid_execution_command_runner, executionCommand.runner);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return;
|
||||
}
|
||||
|
||||
executionCommand.workingDirectory = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR);
|
||||
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
||||
if (executionCommand.executableUri != null) {
|
||||
Logger.logVerbose(LOG_TAG, "uri: \"" + executionCommand.executableUri + "\", path: \"" + executionCommand.executableUri.getPath() + "\", fragment: \"" + executionCommand.executableUri.getFragment() + "\"");
|
||||
|
||||
// Get full path including fragment (anything after last "#")
|
||||
executionCommand.executable = UriUtils.getUriFilePathWithFragment(executionCommand.executableUri);
|
||||
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
|
||||
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
|
||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
|
||||
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
|
||||
}
|
||||
|
||||
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
|
||||
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
|
||||
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL), "Execution Intent Command");
|
||||
executionCommand.commandDescription = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
||||
executionCommand.commandHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP);
|
||||
executionCommand.pluginAPIHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP);
|
||||
executionCommand.isPluginExecutionCommand = true;
|
||||
executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
||||
executionCommand.shellName = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_SHELL_NAME, null);
|
||||
executionCommand.shellCreateMode = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_SHELL_CREATE_MODE, null);
|
||||
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
|
||||
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||
executionCommand.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null);
|
||||
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
||||
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
|
||||
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
|
||||
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||
}
|
||||
|
||||
if (executionCommand.shellCreateMode == null)
|
||||
executionCommand.shellCreateMode = ShellCreateMode.ALWAYS.getMode();
|
||||
|
||||
// Add the execution command to pending plugin execution commands list
|
||||
mPendingPluginExecutionCommands.add(executionCommand);
|
||||
mShellManager.mPendingPluginExecutionCommands.add(executionCommand);
|
||||
|
||||
if (executionCommand.inBackground) {
|
||||
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
|
||||
executeTermuxTaskCommand(executionCommand);
|
||||
} else {
|
||||
else if (Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner))
|
||||
executeTermuxSessionCommand(executionCommand);
|
||||
else {
|
||||
String errmsg = getString(R.string.error_termux_service_unsupported_execution_command_runner, executionCommand.runner);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,57 +428,84 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
|
||||
|
||||
/** Execute a shell command in background {@link TermuxTask}. */
|
||||
/** Execute a shell command in background TermuxTask. */
|
||||
private void executeTermuxTaskCommand(ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Executing background \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
|
||||
|
||||
TermuxTask newTermuxTask = createTermuxTask(executionCommand);
|
||||
// Transform executable path to shell/session name, e.g. "/bin/do-something.sh" => "do-something.sh".
|
||||
if (executionCommand.shellName == null && executionCommand.executable != null)
|
||||
executionCommand.shellName = ShellUtils.getExecutableBasename(executionCommand.executable);
|
||||
|
||||
AppShell newTermuxTask = null;
|
||||
ShellCreateMode shellCreateMode = processShellCreateMode(executionCommand);
|
||||
if (shellCreateMode == null) return;
|
||||
if (ShellCreateMode.NO_SHELL_WITH_NAME.equals(shellCreateMode)) {
|
||||
newTermuxTask = getTermuxTaskForShellName(executionCommand.shellName);
|
||||
if (newTermuxTask != null)
|
||||
Logger.logVerbose(LOG_TAG, "Existing TermuxTask with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\"");
|
||||
else
|
||||
Logger.logVerbose(LOG_TAG, "No existing TermuxTask with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\"");
|
||||
}
|
||||
|
||||
if (newTermuxTask == null)
|
||||
newTermuxTask = createTermuxTask(executionCommand);
|
||||
}
|
||||
|
||||
/** Create a {@link TermuxTask}. */
|
||||
/** Create a TermuxTask. */
|
||||
@Nullable
|
||||
public TermuxTask createTermuxTask(String executablePath, String[] arguments, String stdin, String workingDirectory) {
|
||||
return createTermuxTask(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, true, false));
|
||||
public AppShell createTermuxTask(String executablePath, String[] arguments, String stdin, String workingDirectory) {
|
||||
return createTermuxTask(new ExecutionCommand(TermuxShellManager.getNextShellId(), executablePath,
|
||||
arguments, stdin, workingDirectory, Runner.APP_SHELL.getName(), false));
|
||||
}
|
||||
|
||||
/** Create a {@link TermuxTask}. */
|
||||
/** Create a TermuxTask. */
|
||||
@Nullable
|
||||
public synchronized TermuxTask createTermuxTask(ExecutionCommand executionCommand) {
|
||||
public synchronized AppShell createTermuxTask(ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return null;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
||||
|
||||
if (!executionCommand.inBackground) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring a foreground execution command passed to createTermuxTask()");
|
||||
if (!Runner.APP_SHELL.equalsRunner(executionCommand.runner)) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxTask()");
|
||||
return null;
|
||||
}
|
||||
|
||||
executionCommand.setShellCommandShellEnvironment = true;
|
||||
|
||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
|
||||
|
||||
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, false);
|
||||
AppShell newTermuxTask = AppShell.execute(this, executionCommand, this,
|
||||
new TermuxShellEnvironment(), null,false);
|
||||
if (newTermuxTask == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||
// If the execution command was started for a plugin, then process the error
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else {
|
||||
Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs");
|
||||
Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
mTermuxTasks.add(newTermuxTask);
|
||||
mShellManager.mTermuxTasks.add(newTermuxTask);
|
||||
|
||||
// Remove the execution command from the pending plugin execution commands list since it has
|
||||
// now been processed
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
|
||||
updateNotification();
|
||||
|
||||
return newTermuxTask;
|
||||
}
|
||||
|
||||
/** Callback received when a {@link TermuxTask} finishes. */
|
||||
/** Callback received when a TermuxTask finishes. */
|
||||
@Override
|
||||
public void onTermuxTaskExited(final TermuxTask termuxTask) {
|
||||
public void onAppShellExited(final AppShell termuxTask) {
|
||||
mHandler.post(() -> {
|
||||
if (termuxTask != null) {
|
||||
ExecutionCommand executionCommand = termuxTask.getExecutionCommand();
|
||||
@@ -442,9 +514,9 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
// If the execution command was started for a plugin, then process the results
|
||||
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
|
||||
mTermuxTasks.remove(termuxTask);
|
||||
mShellManager.mTermuxTasks.remove(termuxTask);
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
@@ -461,14 +533,23 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Executing foreground \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
|
||||
|
||||
String sessionName = null;
|
||||
// Transform executable path to shell/session name, e.g. "/bin/do-something.sh" => "do-something.sh".
|
||||
if (executionCommand.shellName == null && executionCommand.executable != null)
|
||||
executionCommand.shellName = ShellUtils.getExecutableBasename(executionCommand.executable);
|
||||
|
||||
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
||||
if (executionCommand.executable != null) {
|
||||
sessionName = ShellUtils.getExecutableBasename(executionCommand.executable).replace('-', ' ');
|
||||
TermuxSession newTermuxSession = null;
|
||||
ShellCreateMode shellCreateMode = processShellCreateMode(executionCommand);
|
||||
if (shellCreateMode == null) return;
|
||||
if (ShellCreateMode.NO_SHELL_WITH_NAME.equals(shellCreateMode)) {
|
||||
newTermuxSession = getTermuxSessionForShellName(executionCommand.shellName);
|
||||
if (newTermuxSession != null)
|
||||
Logger.logVerbose(LOG_TAG, "Existing TermuxSession with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\"");
|
||||
else
|
||||
Logger.logVerbose(LOG_TAG, "No existing TermuxSession with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\"");
|
||||
}
|
||||
|
||||
TermuxSession newTermuxSession = createTermuxSession(executionCommand, sessionName);
|
||||
if (newTermuxSession == null)
|
||||
newTermuxSession = createTermuxSession(executionCommand);
|
||||
if (newTermuxSession == null) return;
|
||||
|
||||
handleSessionAction(DataUtils.getIntFromString(executionCommand.sessionAction,
|
||||
@@ -478,51 +559,68 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
/**
|
||||
* Create a {@link TermuxSession}.
|
||||
* Currently called by {@link TermuxTerminalSessionClient#addNewSession(boolean, String)} to add a new {@link TermuxSession}.
|
||||
* Currently called by {@link TermuxTerminalSessionActivityClient#addNewSession(boolean, String)} to add a new {@link TermuxSession}.
|
||||
*/
|
||||
@Nullable
|
||||
public TermuxSession createTermuxSession(String executablePath, String[] arguments, String stdin, String workingDirectory, boolean isFailSafe, String sessionName) {
|
||||
return createTermuxSession(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, false, isFailSafe), sessionName);
|
||||
public TermuxSession createTermuxSession(String executablePath, String[] arguments, String stdin,
|
||||
String workingDirectory, boolean isFailSafe, String sessionName) {
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId(),
|
||||
executablePath, arguments, stdin, workingDirectory, Runner.TERMINAL_SESSION.getName(), isFailSafe);
|
||||
executionCommand.shellName = sessionName;
|
||||
return createTermuxSession(executionCommand);
|
||||
}
|
||||
|
||||
/** Create a {@link TermuxSession}. */
|
||||
@Nullable
|
||||
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand, String sessionName) {
|
||||
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return null;
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||
|
||||
if (executionCommand.inBackground) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring a background execution command passed to createTermuxSession()");
|
||||
if (!Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxSession()");
|
||||
return null;
|
||||
}
|
||||
|
||||
executionCommand.setShellCommandShellEnvironment = true;
|
||||
executionCommand.terminalTranscriptRows = mProperties.getTerminalTranscriptRows();
|
||||
|
||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
|
||||
|
||||
// If the execution command was started for a plugin, only then will the stdout be set
|
||||
// Otherwise if command was manually started by the user like by adding a new terminal session,
|
||||
// then no need to set stdout
|
||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, sessionName, executionCommand.isPluginExecutionCommand);
|
||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(),
|
||||
this, new TermuxShellEnvironment(), null, executionCommand.isPluginExecutionCommand);
|
||||
if (newTermuxSession == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||
// If the execution command was started for a plugin, then process the error
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else {
|
||||
Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs");
|
||||
Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
mTermuxSessions.add(newTermuxSession);
|
||||
mShellManager.mTermuxSessions.add(newTermuxSession);
|
||||
|
||||
// Remove the execution command from the pending plugin execution commands list since it has
|
||||
// now been processed
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
|
||||
|
||||
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
||||
// activity in is foreground
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
|
||||
|
||||
updateNotification();
|
||||
TermuxActivity.updateTermuxActivityStyling(this);
|
||||
|
||||
// No need to recreate the activity since it likely just started and theme should already have applied
|
||||
TermuxActivity.updateTermuxActivityStyling(this, false);
|
||||
|
||||
return newTermuxSession;
|
||||
}
|
||||
@@ -532,7 +630,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
int index = getIndexOfSession(sessionToRemove);
|
||||
|
||||
if (index >= 0)
|
||||
mTermuxSessions.get(index).finish();
|
||||
mShellManager.mTermuxSessions.get(index).finish();
|
||||
|
||||
return index;
|
||||
}
|
||||
@@ -547,14 +645,14 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
// If the execution command was started for a plugin, then process the results
|
||||
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
|
||||
mTermuxSessions.remove(termuxSession);
|
||||
mShellManager.mTermuxSessions.remove(termuxSession);
|
||||
|
||||
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
||||
// activity in is foreground
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
@@ -564,6 +662,24 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
|
||||
|
||||
private ShellCreateMode processShellCreateMode(@NonNull ExecutionCommand executionCommand) {
|
||||
if (ShellCreateMode.ALWAYS.equalsMode(executionCommand.shellCreateMode))
|
||||
return ShellCreateMode.ALWAYS; // Default
|
||||
else if (ShellCreateMode.NO_SHELL_WITH_NAME.equalsMode(executionCommand.shellCreateMode))
|
||||
if (DataUtils.isNullOrEmpty(executionCommand.shellName)) {
|
||||
TermuxPluginUtils.setAndProcessPluginExecutionCommandError(this, LOG_TAG, executionCommand, false,
|
||||
getString(R.string.error_termux_service_execution_command_shell_name_unset, executionCommand.shellCreateMode));
|
||||
return null;
|
||||
} else {
|
||||
return ShellCreateMode.NO_SHELL_WITH_NAME;
|
||||
}
|
||||
else {
|
||||
TermuxPluginUtils.setAndProcessPluginExecutionCommandError(this, LOG_TAG, executionCommand, false,
|
||||
getString(R.string.error_termux_service_unsupported_execution_command_shell_create_mode, executionCommand.shellCreateMode));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Process session action for new session. */
|
||||
private void handleSessionAction(int sessionAction, TerminalSession newTerminalSession) {
|
||||
Logger.logDebug(LOG_TAG, "Processing sessionAction \"" + sessionAction + "\" for session \"" + newTerminalSession.mSessionName + "\"");
|
||||
@@ -571,8 +687,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
switch (sessionAction) {
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY:
|
||||
setCurrentStoredTerminalSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
|
||||
startTermuxActivity();
|
||||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY:
|
||||
@@ -582,8 +698,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY:
|
||||
setCurrentStoredTerminalSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
|
||||
break;
|
||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY:
|
||||
if (getTermuxSessionsSize() == 1)
|
||||
@@ -601,8 +717,13 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
// For android >= 10, apps require Display over other apps permission to start foreground activities
|
||||
// from background (services). If it is not granted, then TermuxSessions that are started will
|
||||
// show in Termux notification but will not run until user manually clicks the notification.
|
||||
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this)) {
|
||||
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this, true)) {
|
||||
TermuxActivity.startTermuxActivity(this);
|
||||
} else {
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||
if (preferences == null) return;
|
||||
if (preferences.arePluginErrorNotificationsEnabled(false))
|
||||
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted_to_start_terminal), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,35 +733,35 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
/** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then
|
||||
* interface functions requiring the activity should not be available to the terminal sessions,
|
||||
* so we just return the {@link #mTermuxTerminalSessionClientBase}. Once {@link TermuxActivity} bind
|
||||
* so we just return the {@link #mTermuxTerminalSessionServiceClient}. Once {@link TermuxActivity} bind
|
||||
* callback is received, it should call {@link #setTermuxTerminalSessionClient} to set the
|
||||
* {@link TermuxService#mTermuxTerminalSessionClient} so that further terminal sessions are directly
|
||||
* passed the {@link TermuxTerminalSessionClient} object which fully implements the
|
||||
* {@link TermuxService#mTermuxTerminalSessionActivityClient} so that further terminal sessions are directly
|
||||
* passed the {@link TermuxTerminalSessionActivityClient} object which fully implements the
|
||||
* {@link TerminalSessionClient} interface.
|
||||
*
|
||||
* @return Returns the {@link TermuxTerminalSessionClient} if {@link TermuxActivity} has bound with
|
||||
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionClientBase}.
|
||||
* @return Returns the {@link TermuxTerminalSessionActivityClient} if {@link TermuxActivity} has bound with
|
||||
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}.
|
||||
*/
|
||||
public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() {
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
return mTermuxTerminalSessionClient;
|
||||
if (mTermuxTerminalSessionActivityClient != null)
|
||||
return mTermuxTerminalSessionActivityClient;
|
||||
else
|
||||
return mTermuxTerminalSessionClientBase;
|
||||
return mTermuxTerminalSessionServiceClient;
|
||||
}
|
||||
|
||||
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
|
||||
* {@link TermuxService#mTermuxTerminalSessionClient} variable and update the {@link TerminalSession}
|
||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionClientBase}
|
||||
* {@link TermuxService#mTermuxTerminalSessionActivityClient} variable and update the {@link TerminalSession}
|
||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionServiceClient}
|
||||
* earlier.
|
||||
*
|
||||
* @param termuxTerminalSessionClient The {@link TermuxTerminalSessionClient} object that fully
|
||||
* @param termuxTerminalSessionActivityClient The {@link TermuxTerminalSessionActivityClient} object that fully
|
||||
* implements the {@link TerminalSessionClient} interface.
|
||||
*/
|
||||
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++)
|
||||
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClient);
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
|
||||
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient);
|
||||
}
|
||||
|
||||
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
|
||||
@@ -648,10 +769,10 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
* clients do not hold an activity references.
|
||||
*/
|
||||
public synchronized void unsetTermuxTerminalSessionClient() {
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++)
|
||||
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClientBase);
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
|
||||
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient);
|
||||
|
||||
mTermuxTerminalSessionClient = null;
|
||||
mTermuxTerminalSessionActivityClient = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -663,12 +784,12 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
// Set pending intent to be launched when notification is clicked
|
||||
Intent notificationIntent = TermuxActivity.newInstance(this);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
|
||||
|
||||
|
||||
// Set notification text
|
||||
int sessionCount = getTermuxSessionsSize();
|
||||
int taskCount = mTermuxTasks.size();
|
||||
int taskCount = mShellManager.mTermuxTasks.size();
|
||||
String notificationText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
|
||||
if (taskCount > 0) {
|
||||
notificationText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
|
||||
@@ -687,8 +808,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
// Build the notification
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||
TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority,
|
||||
getText(R.string.application_name), notificationText, null,
|
||||
pendingIntent, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||
TermuxConstants.TERMUX_APP_NAME, notificationText, null,
|
||||
contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||
if (builder == null) return null;
|
||||
|
||||
// No need to show a timestamp:
|
||||
@@ -729,7 +850,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
/** Update the shown foreground service notification after making any changes that affect it. */
|
||||
private synchronized void updateNotification() {
|
||||
if (mWakeLock == null && mTermuxSessions.isEmpty() && mTermuxTasks.isEmpty()) {
|
||||
if (mWakeLock == null && mShellManager.mTermuxSessions.isEmpty() && mShellManager.mTermuxTasks.isEmpty()) {
|
||||
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
|
||||
requestStopService();
|
||||
} else {
|
||||
@@ -741,41 +862,55 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
|
||||
|
||||
private void setCurrentStoredTerminalSession(TerminalSession session) {
|
||||
if (session == null) return;
|
||||
private void setCurrentStoredTerminalSession(TerminalSession terminalSession) {
|
||||
if (terminalSession == null) return;
|
||||
// Make the newly created session the current one to be displayed
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||
if (preferences == null) return;
|
||||
preferences.setCurrentSession(session.mHandle);
|
||||
preferences.setCurrentSession(terminalSession.mHandle);
|
||||
}
|
||||
|
||||
public synchronized boolean isTermuxSessionsEmpty() {
|
||||
return mTermuxSessions.isEmpty();
|
||||
return mShellManager.mTermuxSessions.isEmpty();
|
||||
}
|
||||
|
||||
public synchronized int getTermuxSessionsSize() {
|
||||
return mTermuxSessions.size();
|
||||
return mShellManager.mTermuxSessions.size();
|
||||
}
|
||||
|
||||
public synchronized List<TermuxSession> getTermuxSessions() {
|
||||
return mTermuxSessions;
|
||||
return mShellManager.mTermuxSessions;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized TermuxSession getTermuxSession(int index) {
|
||||
if (index >= 0 && index < mTermuxSessions.size())
|
||||
return mTermuxSessions.get(index);
|
||||
if (index >= 0 && index < mShellManager.mTermuxSessions.size())
|
||||
return mShellManager.mTermuxSessions.get(index);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized TermuxSession getTermuxSessionForTerminalSession(TerminalSession terminalSession) {
|
||||
if (terminalSession == null) return null;
|
||||
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) {
|
||||
if (mShellManager.mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||
return mShellManager.mTermuxSessions.get(i);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized TermuxSession getLastTermuxSession() {
|
||||
return mTermuxSessions.isEmpty() ? null : mTermuxSessions.get(mTermuxSessions.size() - 1);
|
||||
return mShellManager.mTermuxSessions.isEmpty() ? null : mShellManager.mTermuxSessions.get(mShellManager.mTermuxSessions.size() - 1);
|
||||
}
|
||||
|
||||
public synchronized int getIndexOfSession(TerminalSession terminalSession) {
|
||||
for (int i = 0; i < mTermuxSessions.size(); i++) {
|
||||
if (mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||
if (terminalSession == null) return -1;
|
||||
|
||||
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) {
|
||||
if (mShellManager.mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
@@ -783,20 +918,40 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
public synchronized TerminalSession getTerminalSessionForHandle(String sessionHandle) {
|
||||
TerminalSession terminalSession;
|
||||
for (int i = 0, len = mTermuxSessions.size(); i < len; i++) {
|
||||
terminalSession = mTermuxSessions.get(i).getTerminalSession();
|
||||
for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) {
|
||||
terminalSession = mShellManager.mTermuxSessions.get(i).getTerminalSession();
|
||||
if (terminalSession.mHandle.equals(sessionHandle))
|
||||
return terminalSession;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static synchronized int getNextExecutionId() {
|
||||
return EXECUTION_ID++;
|
||||
public synchronized AppShell getTermuxTaskForShellName(String name) {
|
||||
if (DataUtils.isNullOrEmpty(name)) return null;
|
||||
AppShell appShell;
|
||||
for (int i = 0, len = mShellManager.mTermuxTasks.size(); i < len; i++) {
|
||||
appShell = mShellManager.mTermuxTasks.get(i);
|
||||
String shellName = appShell.getExecutionCommand().shellName;
|
||||
if (shellName != null && shellName.equals(name))
|
||||
return appShell;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized TermuxSession getTermuxSessionForShellName(String name) {
|
||||
if (DataUtils.isNullOrEmpty(name)) return null;
|
||||
TermuxSession termuxSession;
|
||||
for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) {
|
||||
termuxSession = mShellManager.mTermuxSessions.get(i);
|
||||
String shellName = termuxSession.getExecutionCommand().shellName;
|
||||
if (shellName != null && shellName.equals(name))
|
||||
return termuxSession;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean wantsToStop() {
|
||||
return mWantsToStop;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.termux.app.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@@ -12,8 +11,12 @@ import android.webkit.WebViewClient;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
/** Basic embedded browser for viewing help pages. */
|
||||
public final class HelpActivity extends Activity {
|
||||
public final class HelpActivity extends AppCompatActivity {
|
||||
|
||||
WebView mWebView;
|
||||
|
||||
@@ -39,7 +42,7 @@ public final class HelpActivity extends Activity {
|
||||
mWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.startsWith("https://wiki.termux.com")) {
|
||||
if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
|
||||
// Inline help.
|
||||
setContentView(progressLayout);
|
||||
return false;
|
||||
@@ -60,7 +63,7 @@ public final class HelpActivity extends Activity {
|
||||
setContentView(mWebView);
|
||||
}
|
||||
});
|
||||
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
|
||||
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
package com.termux.app.activities;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.recycler.MarkwonAdapter;
|
||||
import io.noties.markwon.recycler.SimpleEntry;
|
||||
|
||||
public class ReportActivity extends AppCompatActivity {
|
||||
|
||||
private static final String EXTRA_REPORT_INFO = "report_info";
|
||||
|
||||
ReportInfo mReportInfo;
|
||||
String mReportMarkdownString;
|
||||
String mReportActivityMarkdownString;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_report);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
if (toolbar != null) {
|
||||
setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
Bundle bundle = null;
|
||||
Intent intent = getIntent();
|
||||
if (intent != null)
|
||||
bundle = intent.getExtras();
|
||||
else if (savedInstanceState != null)
|
||||
bundle = savedInstanceState;
|
||||
|
||||
updateUI(bundle);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
|
||||
if (intent != null)
|
||||
updateUI(intent.getExtras());
|
||||
}
|
||||
|
||||
private void updateUI(Bundle bundle) {
|
||||
|
||||
if (bundle == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
mReportInfo = (ReportInfo) bundle.getSerializable(EXTRA_REPORT_INFO);
|
||||
|
||||
if (mReportInfo == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
if (mReportInfo.reportTitle != null)
|
||||
actionBar.setTitle(mReportInfo.reportTitle);
|
||||
else
|
||||
actionBar.setTitle(TermuxConstants.TERMUX_APP_NAME + " App Report");
|
||||
}
|
||||
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.recycler_view);
|
||||
|
||||
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
|
||||
|
||||
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.markdown_adapter_node_default)
|
||||
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.markdown_adapter_node_code_block, R.id.code_text_view))
|
||||
.build();
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
|
||||
generateReportActivityMarkdownString();
|
||||
adapter.setMarkdown(markwon, mReportActivityMarkdownString);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putSerializable(EXTRA_REPORT_INFO, mReportInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_report, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// Remove activity from recents menu on back button press
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.menu_item_share_report) {
|
||||
if (mReportMarkdownString != null)
|
||||
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportMarkdownString);
|
||||
} else if (id == R.id.menu_item_copy_report) {
|
||||
if (mReportMarkdownString != null)
|
||||
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
|
||||
*/
|
||||
private void generateReportActivityMarkdownString() {
|
||||
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
|
||||
|
||||
mReportActivityMarkdownString = "";
|
||||
if (mReportInfo.reportStringPrefix != null)
|
||||
mReportActivityMarkdownString += mReportInfo.reportStringPrefix;
|
||||
|
||||
mReportActivityMarkdownString += mReportMarkdownString;
|
||||
|
||||
if (mReportInfo.reportStringSuffix != null)
|
||||
mReportActivityMarkdownString += mReportInfo.reportStringSuffix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void startReportActivity(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||
context.startActivity(newInstance(context, reportInfo));
|
||||
}
|
||||
|
||||
public static Intent newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||
Intent intent = new Intent(context, ReportActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putSerializable(EXTRA_REPORT_INFO, reportInfo);
|
||||
intent.putExtras(bundle);
|
||||
|
||||
// Note that ReportActivity task has documentLaunchMode="intoExisting" set in AndroidManifest.xml
|
||||
// which has equivalent behaviour to the following. The following dynamic way doesn't seem to
|
||||
// work for notification pending intent, i.e separate task isn't created and activity is
|
||||
// launched in the same task as TermuxActivity.
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
||||
return intent;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,27 +2,38 @@ package com.termux.app.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.android.PackageUtils;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
import com.termux.shared.android.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.activity.media.AppCompatActivityUtils;
|
||||
import com.termux.shared.theme.NightMode;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true);
|
||||
|
||||
setContentView(R.layout.activity_settings);
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager()
|
||||
@@ -30,11 +41,9 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
.replace(R.id.settings, new RootPreferencesFragment())
|
||||
.commit();
|
||||
}
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
AppCompatActivityUtils.setToolbar(this, com.termux.shared.R.id.toolbar);
|
||||
AppCompatActivityUtils.setShowBackButtonInActionBar(this, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,17 +60,52 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||
|
||||
configureTermuxTaskerPreference(context);
|
||||
configureAboutPreference(context);
|
||||
configureDonatePreference(context);
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
configureTermuxAPIPreference(context);
|
||||
configureTermuxFloatPreference(context);
|
||||
configureTermuxTaskerPreference(context);
|
||||
configureTermuxWidgetPreference(context);
|
||||
configureAboutPreference(context);
|
||||
configureDonatePreference(context);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void configureTermuxAPIPreference(@NonNull Context context) {
|
||||
Preference termuxAPIPreference = findPreference("termux_api");
|
||||
if (termuxAPIPreference != null) {
|
||||
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false);
|
||||
// If failed to get app preferences, then likely app is not installed, so do not show its preference
|
||||
termuxAPIPreference.setVisible(preferences != null);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureTermuxFloatPreference(@NonNull Context context) {
|
||||
Preference termuxFloatPreference = findPreference("termux_float");
|
||||
if (termuxFloatPreference != null) {
|
||||
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, false);
|
||||
// If failed to get app preferences, then likely app is not installed, so do not show its preference
|
||||
termuxFloatPreference.setVisible(preferences != null);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureTermuxTaskerPreference(@NonNull Context context) {
|
||||
Preference termuxTaskerPrefernce = findPreference("termux_tasker");
|
||||
if (termuxTaskerPrefernce != null) {
|
||||
Preference termuxTaskerPreference = findPreference("termux_tasker");
|
||||
if (termuxTaskerPreference != null) {
|
||||
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
|
||||
// If failed to get app preferences, then likely app is not installed, so do not show its preference
|
||||
termuxTaskerPrefernce.setVisible(preferences != null);
|
||||
termuxTaskerPreference.setVisible(preferences != null);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureTermuxWidgetPreference(@NonNull Context context) {
|
||||
Preference termuxWidgetPreference = findPreference("termux_widget");
|
||||
if (termuxWidgetPreference != null) {
|
||||
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, false);
|
||||
// If failed to get app preferences, then likely app is not installed, so do not show its preference
|
||||
termuxWidgetPreference.setVisible(preferences != null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +119,20 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
String title = "About";
|
||||
|
||||
StringBuilder aboutString = new StringBuilder();
|
||||
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, false));
|
||||
|
||||
String termuxPluginAppsInfo = TermuxUtils.getTermuxPluginAppsInfoMarkdownString(context);
|
||||
if (termuxPluginAppsInfo != null)
|
||||
aboutString.append("\n\n").append(termuxPluginAppsInfo);
|
||||
|
||||
aboutString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
||||
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
|
||||
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context, true));
|
||||
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||
|
||||
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT, TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
||||
String userActionName = UserAction.ABOUT.getName();
|
||||
|
||||
ReportInfo reportInfo = new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title);
|
||||
reportInfo.setReportString(aboutString.toString());
|
||||
reportInfo.setReportSaveFileLabelAndPath(userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
|
||||
|
||||
ReportActivity.startReportActivity(context, reportInfo);
|
||||
}
|
||||
}.start();
|
||||
|
||||
@@ -111,7 +159,7 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
donatePreference.setOnPreferenceClickListener(preference -> {
|
||||
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||
ShareUtils.openUrl(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.termux.app.api.file;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Patterns;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.android.PackageUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.net.uri.UriUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.net.uri.UriScheme;
|
||||
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FileReceiverActivity extends AppCompatActivity {
|
||||
|
||||
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
|
||||
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
|
||||
static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener";
|
||||
|
||||
/**
|
||||
* If the activity should be finished when the name input dialog is dismissed. This is disabled
|
||||
* before showing an error dialog, since the act of showing the error dialog will cause the
|
||||
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
|
||||
* when showing the error dialog.
|
||||
*/
|
||||
boolean mFinishOnDismissNameDialog = true;
|
||||
|
||||
private static final String API_TAG = TermuxConstants.TERMUX_APP_NAME + "FileReceiver";
|
||||
|
||||
private static final String LOG_TAG = "FileReceiverActivity";
|
||||
|
||||
static boolean isSharedTextAnUrl(String sharedText) {
|
||||
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
final Intent intent = getIntent();
|
||||
final String action = intent.getAction();
|
||||
final String type = intent.getType();
|
||||
final String scheme = intent.getScheme();
|
||||
|
||||
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
|
||||
final String sharedTitle = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_TITLE, null);
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null) {
|
||||
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
|
||||
if (sharedUri != null) {
|
||||
handleContentUri(sharedUri, sharedTitle);
|
||||
} else if (sharedText != null) {
|
||||
if (isSharedTextAnUrl(sharedText)) {
|
||||
handleUrlAndFinish(sharedText);
|
||||
} else {
|
||||
String subject = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_SUBJECT, null);
|
||||
if (subject == null) subject = sharedTitle;
|
||||
if (subject != null) subject += ".txt";
|
||||
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
|
||||
}
|
||||
} else {
|
||||
showErrorDialogAndQuit("Send action without content - nothing to save.");
|
||||
}
|
||||
} else {
|
||||
Uri dataUri = intent.getData();
|
||||
|
||||
if (dataUri == null) {
|
||||
showErrorDialogAndQuit("Data uri not passed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (UriScheme.SCHEME_CONTENT.equals(scheme)) {
|
||||
handleContentUri(dataUri, sharedTitle);
|
||||
} else if (UriScheme.SCHEME_FILE.equals(scheme)) {
|
||||
Logger.logVerbose(LOG_TAG, "uri: \"" + dataUri + "\", path: \"" + dataUri.getPath() + "\", fragment: \"" + dataUri.getFragment() + "\"");
|
||||
|
||||
// Get full path including fragment (anything after last "#")
|
||||
String path = UriUtils.getUriFilePathWithFragment(dataUri);
|
||||
if (DataUtils.isNullOrEmpty(path)) {
|
||||
showErrorDialogAndQuit("File path from data uri is null, empty or invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(path);
|
||||
try {
|
||||
FileInputStream in = new FileInputStream(file);
|
||||
promptNameAndSave(in, file.getName());
|
||||
} catch (FileNotFoundException e) {
|
||||
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
|
||||
}
|
||||
} else {
|
||||
showErrorDialogAndQuit("Unable to receive any file or URL.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showErrorDialogAndQuit(String message) {
|
||||
mFinishOnDismissNameDialog = false;
|
||||
MessageDialogUtils.showMessage(this,
|
||||
API_TAG, message,
|
||||
null, (dialog, which) -> finish(),
|
||||
null, null,
|
||||
dialog -> finish());
|
||||
}
|
||||
|
||||
void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) {
|
||||
try {
|
||||
Logger.logVerbose(LOG_TAG, "uri: \"" + uri + "\", path: \"" + uri.getPath() + "\", fragment: \"" + uri.getFragment() + "\"");
|
||||
|
||||
String attachmentFileName = null;
|
||||
|
||||
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
|
||||
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
|
||||
if (c != null && c.moveToFirst()) {
|
||||
final int fileNameColumnId = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (fileNameColumnId >= 0) attachmentFileName = c.getString(fileNameColumnId);
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
|
||||
if (attachmentFileName == null) attachmentFileName = UriUtils.getUriFileBasename(uri, true);
|
||||
|
||||
InputStream in = getContentResolver().openInputStream(uri);
|
||||
promptNameAndSave(in, attachmentFileName);
|
||||
} catch (Exception e) {
|
||||
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName,
|
||||
R.string.action_file_received_edit, text -> {
|
||||
File outFile = saveStreamWithName(in, text);
|
||||
if (outFile == null) return;
|
||||
|
||||
final File editorProgramFile = new File(EDITOR_PROGRAM);
|
||||
if (!editorProgramFile.isFile()) {
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Do this for the user if necessary:
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
editorProgramFile.setExecutable(true);
|
||||
|
||||
final Uri scriptUri = UriUtils.getFileUri(EDITOR_PROGRAM);
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
|
||||
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
R.string.action_file_received_open_directory, text -> {
|
||||
if (saveStreamWithName(in, text) == null) return;
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
|
||||
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
android.R.string.cancel, text -> finish(), dialog -> {
|
||||
if (mFinishOnDismissNameDialog) finish();
|
||||
});
|
||||
}
|
||||
|
||||
public File saveStreamWithName(InputStream in, String attachmentFileName) {
|
||||
File receiveDir = new File(TERMUX_RECEIVEDIR);
|
||||
|
||||
if (DataUtils.isNullOrEmpty(attachmentFileName)) {
|
||||
showErrorDialogAndQuit("File name cannot be null or empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
|
||||
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final File outFile = new File(receiveDir, attachmentFileName);
|
||||
try (FileOutputStream f = new FileOutputStream(outFile)) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int readBytes;
|
||||
while ((readBytes = in.read(buffer)) > 0) {
|
||||
f.write(buffer, 0, readBytes);
|
||||
}
|
||||
}
|
||||
return outFile;
|
||||
} catch (IOException e) {
|
||||
showErrorDialogAndQuit("Error saving file:\n\n" + e);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void handleUrlAndFinish(final String url) {
|
||||
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
|
||||
if (!urlOpenerProgramFile.isFile()) {
|
||||
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
|
||||
+ "Create this file as a script or a symlink - it will be called with the shared URL as the first argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Do this for the user if necessary:
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
urlOpenerProgramFile.setExecutable(true);
|
||||
|
||||
final Uri urlOpenerProgramUri = UriUtils.getFileUri(URL_OPENER_PROGRAM);
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
|
||||
executeIntent.setClass(FileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update {@link TERMUX_APP#FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on
|
||||
* {@link TermuxPropertyConstants#KEY_DISABLE_FILE_SHARE_RECEIVER} value and
|
||||
* {@link TERMUX_APP#FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on
|
||||
* {@link TermuxPropertyConstants#KEY_DISABLE_FILE_VIEW_RECEIVER} value.
|
||||
*/
|
||||
public static void updateFileReceiverActivityComponentsState(@NonNull Context context) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
TermuxAppSharedProperties properties = TermuxAppSharedProperties.getProperties();
|
||||
|
||||
String errmsg;
|
||||
boolean state;
|
||||
|
||||
state = !properties.isFileShareReceiverDisabled();
|
||||
Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state);
|
||||
errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME,
|
||||
TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME,
|
||||
state, null, false, false);
|
||||
if (errmsg != null)
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
|
||||
state = !properties.isFileViewReceiverDisabled();
|
||||
Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state);
|
||||
errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME,
|
||||
TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME,
|
||||
state, null, false, false);
|
||||
if (errmsg != null)
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.termux.app.event;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||
import com.termux.shared.termux.shell.TermuxShellManager;
|
||||
|
||||
public class SystemEventReceiver extends BroadcastReceiver {
|
||||
|
||||
private static SystemEventReceiver mInstance;
|
||||
|
||||
private static final String LOG_TAG = "SystemEventReceiver";
|
||||
|
||||
public static synchronized SystemEventReceiver getInstance() {
|
||||
if (mInstance == null) {
|
||||
mInstance = new SystemEventReceiver();
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(@NonNull Context context, @Nullable Intent intent) {
|
||||
if (intent == null) return;
|
||||
Logger.logDebug(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||
|
||||
String action = intent.getAction();
|
||||
if (action == null) return;
|
||||
|
||||
switch (action) {
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
onActionBootCompleted(context, intent);
|
||||
break;
|
||||
case Intent.ACTION_PACKAGE_ADDED:
|
||||
case Intent.ACTION_PACKAGE_REMOVED:
|
||||
case Intent.ACTION_PACKAGE_REPLACED:
|
||||
onActionPackageUpdated(context, intent);
|
||||
break;
|
||||
default:
|
||||
Logger.logError(LOG_TAG, "Invalid action \"" + action + "\" passed to " + LOG_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onActionBootCompleted(@NonNull Context context, @NonNull Intent intent) {
|
||||
TermuxShellManager.onActionBootCompleted(context, intent);
|
||||
}
|
||||
|
||||
public synchronized void onActionPackageUpdated(@NonNull Context context, @NonNull Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null && TermuxUtils.isUriDataForTermuxPluginPackage(data)) {
|
||||
Logger.logDebug(LOG_TAG, intent.getAction().replaceAll("^android.intent.action.", "") +
|
||||
" event received for \"" + data.toString().replaceAll("^package:", "") + "\"");
|
||||
if (TermuxFileUtils.isTermuxFilesDirectoryAccessible(context, false, false) == null)
|
||||
TermuxShellEnvironment.writeEnvironmentToFile(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Register {@link SystemEventReceiver} to listen to {@link Intent#ACTION_PACKAGE_ADDED},
|
||||
* {@link Intent#ACTION_PACKAGE_REMOVED} and {@link Intent#ACTION_PACKAGE_REPLACED} broadcasts.
|
||||
* They must be registered dynamically and cannot be registered implicitly in
|
||||
* the AndroidManifest.xml due to Android 8+ restrictions.
|
||||
*
|
||||
* https://developer.android.com/guide/components/broadcast-exceptions
|
||||
*/
|
||||
public synchronized static void registerPackageUpdateEvents(@NonNull Context context) {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
|
||||
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
|
||||
intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
|
||||
intentFilter.addDataScheme("package");
|
||||
context.registerReceiver(getInstance(), intentFilter);
|
||||
}
|
||||
|
||||
public synchronized static void unregisterPackageUpdateEvents(@NonNull Context context) {
|
||||
context.unregisterReceiver(getInstance());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.termux.app.fragments.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxAPIPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(TermuxAPIPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_api_preferences, rootKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TermuxAPIPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxAPIAppSharedPreferences mPreferences;
|
||||
|
||||
private static TermuxAPIPreferencesDataStore mInstance;
|
||||
|
||||
private TermuxAPIPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxAPIAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized TermuxAPIPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new TermuxAPIPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.termux.app.fragments.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxFloatPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(TermuxFloatPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_float_preferences, rootKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TermuxFloatPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxFloatAppSharedPreferences mPreferences;
|
||||
|
||||
private static TermuxFloatPreferencesDataStore mInstance;
|
||||
|
||||
private TermuxFloatPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxFloatAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized TermuxFloatPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new TermuxFloatPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.termux.app.fragments.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxWidgetPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(TermuxWidgetPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_widget_preferences, rootKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TermuxWidgetPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxWidgetAppSharedPreferences mPreferences;
|
||||
|
||||
private static TermuxWidgetPreferencesDataStore mInstance;
|
||||
|
||||
private TermuxWidgetPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxWidgetAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized TermuxWidgetPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new TermuxWidgetPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
@Keep
|
||||
@@ -144,9 +144,9 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||
case "terminal_view_key_logging_enabled":
|
||||
return mPreferences.isTerminalViewKeyLoggingEnabled();
|
||||
case "plugin_error_notifications_enabled":
|
||||
return mPreferences.arePluginErrorNotificationsEnabled();
|
||||
return mPreferences.arePluginErrorNotificationsEnabled(false);
|
||||
case "crash_report_notifications_enabled":
|
||||
return mPreferences.areCrashReportNotificationsEnabled();
|
||||
return mPreferences.areCrashReportNotificationsEnabled(false);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.termux.app.fragments.settings.termux;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TerminalViewPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(TerminalViewPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_terminal_view_preferences, rootKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TerminalViewPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
private static TerminalViewPreferencesDataStore mInstance;
|
||||
|
||||
private TerminalViewPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new TerminalViewPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void putBoolean(String key, boolean value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "terminal_margin_adjustment":
|
||||
mPreferences.setTerminalMarginAdjustment(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
if (mPreferences == null) return false;
|
||||
|
||||
switch (key) {
|
||||
case "terminal_margin_adjustment":
|
||||
return mPreferences.isTerminalMarginAdjustmentEnabled();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.termux.app.fragments.settings.termux_api;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_api_debugging_preferences, rootKey);
|
||||
|
||||
configureLoggingPreferences(context);
|
||||
}
|
||||
|
||||
private void configureLoggingPreferences(@NonNull Context context) {
|
||||
PreferenceCategory loggingCategory = findPreference("logging");
|
||||
if (loggingCategory == null) return;
|
||||
|
||||
ListPreference logLevelListPreference = findPreference("log_level");
|
||||
if (logLevelListPreference != null) {
|
||||
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, true);
|
||||
if (preferences == null) return;
|
||||
|
||||
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
|
||||
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
|
||||
loggingCategory.addPreference(logLevelListPreference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxAPIAppSharedPreferences mPreferences;
|
||||
|
||||
private static DebuggingPreferencesDataStore mInstance;
|
||||
|
||||
private DebuggingPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxAPIAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new DebuggingPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getString(String key, @Nullable String defValue) {
|
||||
if (mPreferences == null) return null;
|
||||
if (key == null) return null;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
return String.valueOf(mPreferences.getLogLevel(true));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String key, @Nullable String value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
if (value != null) {
|
||||
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.termux.app.fragments.settings.termux_float;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_float_debugging_preferences, rootKey);
|
||||
|
||||
configureLoggingPreferences(context);
|
||||
}
|
||||
|
||||
private void configureLoggingPreferences(@NonNull Context context) {
|
||||
PreferenceCategory loggingCategory = findPreference("logging");
|
||||
if (loggingCategory == null) return;
|
||||
|
||||
ListPreference logLevelListPreference = findPreference("log_level");
|
||||
if (logLevelListPreference != null) {
|
||||
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, true);
|
||||
if (preferences == null) return;
|
||||
|
||||
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
|
||||
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
|
||||
loggingCategory.addPreference(logLevelListPreference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxFloatAppSharedPreferences mPreferences;
|
||||
|
||||
private static DebuggingPreferencesDataStore mInstance;
|
||||
|
||||
private DebuggingPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxFloatAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new DebuggingPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getString(String key, @Nullable String defValue) {
|
||||
if (mPreferences == null) return null;
|
||||
if (key == null) return null;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
return String.valueOf(mPreferences.getLogLevel(true));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String key, @Nullable String value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
if (value != null) {
|
||||
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putBoolean(String key, boolean value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "terminal_view_key_logging_enabled":
|
||||
mPreferences.setTerminalViewKeyLoggingEnabled(value, true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
if (mPreferences == null) return false;
|
||||
switch (key) {
|
||||
case "terminal_view_key_logging_enabled":
|
||||
return mPreferences.isTerminalViewKeyLoggingEnabled(true);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.termux.app.fragments.settings.termux_widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_widget_debugging_preferences, rootKey);
|
||||
|
||||
configureLoggingPreferences(context);
|
||||
}
|
||||
|
||||
private void configureLoggingPreferences(@NonNull Context context) {
|
||||
PreferenceCategory loggingCategory = findPreference("logging");
|
||||
if (loggingCategory == null) return;
|
||||
|
||||
ListPreference logLevelListPreference = findPreference("log_level");
|
||||
if (logLevelListPreference != null) {
|
||||
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, true);
|
||||
if (preferences == null) return;
|
||||
|
||||
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
|
||||
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
|
||||
loggingCategory.addPreference(logLevelListPreference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxWidgetAppSharedPreferences mPreferences;
|
||||
|
||||
private static DebuggingPreferencesDataStore mInstance;
|
||||
|
||||
private DebuggingPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxWidgetAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new DebuggingPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getString(String key, @Nullable String defValue) {
|
||||
if (mPreferences == null) return null;
|
||||
if (key == null) return null;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
return String.valueOf(mPreferences.getLogLevel(true));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String key, @Nullable String value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
if (value != null) {
|
||||
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package com.termux.app.models;
|
||||
public enum UserAction {
|
||||
|
||||
ABOUT("about"),
|
||||
CRASH_REPORT("crash report"),
|
||||
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
|
||||
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package com.termux.app.settings.properties;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.SharedPropertiesParser;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class TermuxAppSharedProperties extends TermuxSharedProperties implements SharedPropertiesParser {
|
||||
|
||||
private ExtraKeysInfo mExtraKeysInfo;
|
||||
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
||||
|
||||
private static final String LOG_TAG = "TermuxAppSharedProperties";
|
||||
|
||||
public TermuxAppSharedProperties(@Nonnull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the termux properties from disk into an in-memory cache.
|
||||
*/
|
||||
@Override
|
||||
public void loadTermuxPropertiesFromDisk() {
|
||||
super.loadTermuxPropertiesFromDisk();
|
||||
|
||||
setExtraKeys();
|
||||
setSessionShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal extra keys and style.
|
||||
*/
|
||||
private void setExtraKeys() {
|
||||
mExtraKeysInfo = null;
|
||||
|
||||
try {
|
||||
// The mMap stores the extra key and style string values while loading properties
|
||||
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
|
||||
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
||||
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
|
||||
} catch (JSONException e) {
|
||||
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
||||
|
||||
try {
|
||||
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||
} catch (JSONException e2) {
|
||||
Logger.showToast(mContext, "Can't create default extra keys",true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||
mExtraKeysInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal sessions shortcuts.
|
||||
*/
|
||||
private void setSessionShortcuts() {
|
||||
if (mSessionShortcuts == null)
|
||||
mSessionShortcuts = new ArrayList<>();
|
||||
else
|
||||
mSessionShortcuts.clear();
|
||||
|
||||
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
|
||||
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
|
||||
// The mMap stores the code points for the session shortcuts while loading properties
|
||||
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
|
||||
// If codePoint is null, then session shortcut did not exist in properties or was invalid
|
||||
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
|
||||
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
|
||||
// add the code point to sessionShortcuts
|
||||
if (codePoint != null)
|
||||
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
public List<KeyboardShortcut> getSessionShortcuts() {
|
||||
return mSessionShortcuts;
|
||||
}
|
||||
|
||||
public ExtraKeysInfo getExtraKeysInfo() {
|
||||
return mExtraKeysInfo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
|
||||
|
||||
/**
|
||||
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
|
||||
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
|
||||
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
|
||||
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
|
||||
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
|
||||
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
|
||||
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
|
||||
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
|
||||
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
|
||||
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
|
||||
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
|
||||
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
|
||||
*
|
||||
* With the above configuration, the additional clipboard suggestions row partially covers the
|
||||
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
|
||||
* in the Android OS where it does not consider the candidate's view height in its calculation to push
|
||||
* up the view or because Gboard does not include the candidate's view height in the height reported
|
||||
* to android that should be used, hence causing an overlap.
|
||||
*
|
||||
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
|
||||
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
|
||||
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
|
||||
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
|
||||
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
|
||||
* Also check https://github.com/termux/termux-app/issues/1539.
|
||||
*
|
||||
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
|
||||
* like number row, etc.
|
||||
*
|
||||
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
|
||||
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
|
||||
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
|
||||
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
|
||||
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
|
||||
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
|
||||
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
|
||||
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
|
||||
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
|
||||
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
|
||||
*/
|
||||
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
public TermuxActivity mActivity;
|
||||
public Integer marginBottom;
|
||||
public Integer lastMarginBottom;
|
||||
public long lastMarginBottomTime;
|
||||
public long lastMarginBottomExtraTime;
|
||||
|
||||
/** Log root view events. */
|
||||
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
|
||||
|
||||
private static final String LOG_TAG = "TermuxActivityRootView";
|
||||
|
||||
private static int mStatusBarHeight;
|
||||
|
||||
public TermuxActivityRootView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setActivity(TermuxActivity activity) {
|
||||
mActivity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether root view logging is enabled or not.
|
||||
*
|
||||
* @param value The boolean value that defines the state.
|
||||
*/
|
||||
public void setIsRootViewLoggingEnabled(boolean value) {
|
||||
ROOT_VIEW_LOGGING_ENABLED = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
if (marginBottom != null) {
|
||||
if (ROOT_VIEW_LOGGING_ENABLED)
|
||||
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
|
||||
params.setMargins(0, 0, 0, marginBottom);
|
||||
setLayoutParams(params);
|
||||
marginBottom = null;
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
if (mActivity == null || !mActivity.isVisible()) return;
|
||||
|
||||
View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView();
|
||||
if (bottomSpaceView == null) return;
|
||||
|
||||
boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED;
|
||||
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:");
|
||||
|
||||
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
// Get the position Rects of the bottom space view and the main window holding it
|
||||
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight);
|
||||
if (windowAndViewRects == null)
|
||||
return;
|
||||
|
||||
Rect windowAvailableRect = windowAndViewRects[0];
|
||||
Rect bottomSpaceViewRect = windowAndViewRects[1];
|
||||
|
||||
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
|
||||
//boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape
|
||||
boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect);
|
||||
boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0;
|
||||
boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0);
|
||||
|
||||
if (root_view_logging_enabled) {
|
||||
Logger.logVerbose(LOG_TAG, "windowAvailableRect " + ViewUtils.toRectString(windowAvailableRect) + ", bottomSpaceViewRect " + ViewUtils.toRectString(bottomSpaceViewRect));
|
||||
Logger.logVerbose(LOG_TAG, "windowAvailableRect.bottom " + windowAvailableRect.bottom +
|
||||
", bottomSpaceViewRect.bottom " +bottomSpaceViewRect.bottom +
|
||||
", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin +
|
||||
", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) +
|
||||
", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
|
||||
}
|
||||
|
||||
// If the bottomSpaceViewRect is visible, then remove the margin if needed
|
||||
if (isVisible) {
|
||||
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
|
||||
// and a margin has been added
|
||||
// Necessary so that we don't get stuck in an infinite loop since setting margin
|
||||
// will call OnGlobalLayoutListener again and next time bottom space view
|
||||
// will be visible and margin will be set to 0, which again will call
|
||||
// OnGlobalLayoutListener...
|
||||
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
|
||||
// set appropriate margins when views are changed quickly since some changes
|
||||
// may be missed.
|
||||
if (isVisibleBecauseMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Visible due to margin");
|
||||
|
||||
// Once the view has been redrawn with new margin, we set margin back to 0 so that
|
||||
// when next time onMeasure() is called, margin 0 is used. This is necessary for
|
||||
// cases when view has been redrawn with new margin because bottom space view was
|
||||
// hidden by keyboard and then view was redrawn again due to layout change (like
|
||||
// keyboard symbol view is switched to), android will add margin below its new position
|
||||
// if its greater than 0, which was already above the keyboard creating x2x margin.
|
||||
// Adding time check since moving split screen divider in landscape causes jitter
|
||||
// and prevents some infinite loops
|
||||
if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) {
|
||||
lastMarginBottomTime = System.currentTimeMillis();
|
||||
marginBottom = 0;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Ignoring restoring marginBottom to 0 since called to quickly");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
boolean setMargin = params.bottomMargin != 0;
|
||||
|
||||
// If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect
|
||||
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||
// onGlobalLayout: Bottom margin already equals 0
|
||||
if (isVisibleBecauseExtraMargin) {
|
||||
// Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar
|
||||
if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Resetting margin since visible due to extra margin");
|
||||
lastMarginBottomExtraTime = System.currentTimeMillis();
|
||||
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
|
||||
lastMarginBottom = null;
|
||||
setMargin = true;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Ignoring resetting margin since visible due to extra margin since called to quickly");
|
||||
}
|
||||
}
|
||||
|
||||
if (setMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Setting bottom margin to 0");
|
||||
params.setMargins(0, 0, 0, 0);
|
||||
setLayoutParams(params);
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Bottom margin already equals 0");
|
||||
// This is done so that when next time onMeasure() is called, lastMarginBottom is used.
|
||||
// This is done since we **expect** the keyboard to have same dimensions next time layout
|
||||
// changes, so best set margin while view is drawn the first time, otherwise it will
|
||||
// cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the
|
||||
// likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic
|
||||
// works fine for all cases.
|
||||
marginBottom = lastMarginBottom;
|
||||
}
|
||||
}
|
||||
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
|
||||
else {
|
||||
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
|
||||
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
|
||||
|
||||
boolean setMargin = params.bottomMargin != pxHidden;
|
||||
|
||||
// If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect
|
||||
// is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener
|
||||
// again, so that margins are set properly. May happen when toolbar/extra keys is disabled
|
||||
// and enabled from left drawer, just like case for isVisibleBecauseExtraMargin.
|
||||
// onMeasure: Setting bottom margin to 176
|
||||
// onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||
// onGlobalLayout: Bottom margin already equals 176
|
||||
if (pxHidden > 0 && params.bottomMargin > 0) {
|
||||
if (pxHidden != params.bottomMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since not visible due to wrong margin");
|
||||
pxHidden = 0;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin since not visible despite required margin");
|
||||
}
|
||||
setMargin = true;
|
||||
}
|
||||
|
||||
if (pxHidden < 0) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative");
|
||||
pxHidden = 0;
|
||||
}
|
||||
|
||||
|
||||
if (setMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Setting bottom margin to " + pxHidden);
|
||||
params.setMargins(0, 0, 0, pxHidden);
|
||||
setLayoutParams(params);
|
||||
lastMarginBottom = pxHidden;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||
// Let view window handle insets however it wants
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,9 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
|
||||
import com.termux.shared.theme.NightMode;
|
||||
import com.termux.shared.theme.ThemeUtils;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.List;
|
||||
@@ -55,9 +57,9 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
|
||||
return sessionRowView;
|
||||
}
|
||||
|
||||
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||
boolean shouldEnableDarkTheme = ThemeUtils.shouldEnableDarkTheme(mActivity, NightMode.getAppNightMode().getName());
|
||||
|
||||
if (isUsingBlackUI) {
|
||||
if (shouldEnableDarkTheme) {
|
||||
sessionTitleView.setBackground(
|
||||
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||
);
|
||||
@@ -84,7 +86,7 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
|
||||
} else {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
}
|
||||
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||
int defaultColor = shouldEnableDarkTheme ? Color.WHITE : Color.BLACK;
|
||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||
sessionTitleView.setTextColor(color);
|
||||
return sessionRowView;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
@@ -12,18 +13,23 @@ import android.media.SoundPool;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
|
||||
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.app.terminal.io.BellHandler;
|
||||
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.termux.terminal.io.BellHandler;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
import com.termux.terminal.TextStyle;
|
||||
|
||||
import java.io.File;
|
||||
@@ -31,7 +37,8 @@ import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
||||
/** The {@link TerminalSessionClient} implementation that may require an {@link Activity} for its interface methods. */
|
||||
public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionClientBase {
|
||||
|
||||
private final TermuxActivity mActivity;
|
||||
|
||||
@@ -41,9 +48,9 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
|
||||
private int mBellSoundId;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionActivityClient";
|
||||
|
||||
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
||||
public TermuxTerminalSessionActivityClient(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
@@ -79,7 +86,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
// Just initialize the mBellSoundPool and load the sound, otherwise bell might not run
|
||||
// the first time bell key is pressed and play() is called, since sound may not be loaded
|
||||
// quickly enough before the call to play(). https://stackoverflow.com/questions/35435625
|
||||
getBellSoundPool();
|
||||
loadBellSoundPool();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +107,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReload() {
|
||||
public void onReloadActivityStyling() {
|
||||
// Set terminal fonts and colors
|
||||
checkForFontAndColors();
|
||||
}
|
||||
@@ -108,14 +115,14 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
public void onTextChanged(@NonNull TerminalSession changedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession updatedSession) {
|
||||
public void onTitleChanged(@NonNull TerminalSession updatedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (updatedSession != mActivity.getCurrentSession()) {
|
||||
@@ -129,7 +136,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
public void onSessionFinished(@NonNull TerminalSession finishedSession) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
|
||||
if (service == null || service.wantsToStop()) {
|
||||
@@ -138,39 +145,59 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
return;
|
||||
}
|
||||
|
||||
int index = service.getIndexOfSession(finishedSession);
|
||||
|
||||
// For plugin commands that expect the result back, we should immediately close the session
|
||||
// and send the result back instead of waiting fo the user to press enter.
|
||||
// The plugin can handle/show errors itself.
|
||||
boolean isPluginExecutionCommandWithPendingResult = false;
|
||||
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||
if (termuxSession != null) {
|
||||
isPluginExecutionCommandWithPendingResult = termuxSession.getExecutionCommand().isPluginExecutionCommandWithPendingResult();
|
||||
if (isPluginExecutionCommandWithPendingResult)
|
||||
Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending.");
|
||||
}
|
||||
|
||||
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
|
||||
// Show toast for non-current sessions that exit.
|
||||
int indexOfSession = service.getIndexOfSession(finishedSession);
|
||||
// Verify that session was not removed before we got told about it finishing:
|
||||
if (indexOfSession >= 0)
|
||||
if (index >= 0)
|
||||
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
|
||||
}
|
||||
|
||||
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||
// On Android TV devices we need to use older behaviour because we may
|
||||
// not be able to have multiple launcher icons.
|
||||
if (service.getTermuxSessionsSize() > 1) {
|
||||
if (service.getTermuxSessionsSize() > 1 || isPluginExecutionCommandWithPendingResult) {
|
||||
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) {
|
||||
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130 || isPluginExecutionCommandWithPendingResult) {
|
||||
removeFinishedSession(finishedSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClipboardText(TerminalSession session, String text) {
|
||||
public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||
ShareUtils.copyTextToClipboard(mActivity, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
public void onPasteTextFromClipboard(@Nullable TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
|
||||
if (text != null)
|
||||
mActivity.getTerminalView().mEmulator.paste(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(@NonNull TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||
@@ -178,7 +205,9 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
BellHandler.getInstance(mActivity).doBell();
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
||||
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
loadBellSoundPool();
|
||||
if (mBellSoundPool != null)
|
||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||
// Ignore the bell character.
|
||||
@@ -187,7 +216,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession changedSession) {
|
||||
public void onColorsChanged(@NonNull TerminalSession changedSession) {
|
||||
if (mActivity.getCurrentSession() == changedSession)
|
||||
updateBackgroundColor();
|
||||
}
|
||||
@@ -205,19 +234,49 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTerminalShellPid(@NonNull TerminalSession terminalSession, int pid) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
TermuxSession termuxSession = service.getTermuxSessionForTerminalSession(terminalSession);
|
||||
if (termuxSession != null)
|
||||
termuxSession.getExecutionCommand().mPid = pid;
|
||||
}
|
||||
|
||||
|
||||
/** Initialize and get mBellSoundPool */
|
||||
private synchronized SoundPool getBellSoundPool() {
|
||||
/**
|
||||
* Should be called when mActivity.onResetTerminalSession() is called
|
||||
*/
|
||||
public void onResetTerminalSession() {
|
||||
// Ensure blinker starts again after reset if cursor blinking was disabled before reset like
|
||||
// with "tput civis" which would have called onTerminalCursorStateChange()
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Integer getTerminalCursorStyle() {
|
||||
return mActivity.getProperties().getTerminalCursorStyle();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Load mBellSoundPool */
|
||||
private synchronized void loadBellSoundPool() {
|
||||
if (mBellSoundPool == null) {
|
||||
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
|
||||
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||
try {
|
||||
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||
} catch (Exception e){
|
||||
// Catch java.lang.RuntimeException: Unable to resume activity {com.termux/com.termux.app.TermuxActivity}: android.content.res.Resources$NotFoundException: File res/raw/bell.ogg from drawable resource ID
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to load bell sound pool", e);
|
||||
}
|
||||
}
|
||||
|
||||
return mBellSoundPool;
|
||||
}
|
||||
|
||||
/** Release mBellSoundPool resources */
|
||||
@@ -248,8 +307,10 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
void notifyOfSessionChange() {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
mActivity.showToast(toToastTitle(session), false);
|
||||
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
mActivity.showToast(toToastTitle(session), false);
|
||||
}
|
||||
}
|
||||
|
||||
public void switchToSession(boolean forward) {
|
||||
@@ -283,12 +344,23 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
public void renameSession(final TerminalSession sessionToRename) {
|
||||
if (sessionToRename == null) return;
|
||||
|
||||
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||
sessionToRename.mSessionName = text;
|
||||
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||
renameSession(sessionToRename, text);
|
||||
termuxSessionListNotifyUpdated();
|
||||
}, -1, null, -1, null, null);
|
||||
}
|
||||
|
||||
private void renameSession(TerminalSession sessionToRename, String text) {
|
||||
if (sessionToRename == null) return;
|
||||
sessionToRename.mSessionName = text;
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service != null) {
|
||||
TermuxSession termuxSession = service.getTermuxSessionForTerminalSession(sessionToRename);
|
||||
if (termuxSession != null)
|
||||
termuxSession.getExecutionCommand().shellName = text;
|
||||
}
|
||||
}
|
||||
|
||||
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.app.Service;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession;
|
||||
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
|
||||
/** The {@link TerminalSessionClient} implementation that may require a {@link Service} for its interface methods. */
|
||||
public class TermuxTerminalSessionServiceClient extends TermuxTerminalSessionClientBase {
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionServiceClient";
|
||||
|
||||
private final TermuxService mService;
|
||||
|
||||
public TermuxTerminalSessionServiceClient(TermuxService service) {
|
||||
this.mService = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTerminalShellPid(@NonNull TerminalSession terminalSession, int pid) {
|
||||
TermuxSession termuxSession = mService.getTermuxSessionForTerminalSession(terminalSession);
|
||||
if (termuxSession != null)
|
||||
termuxSession.getExecutionCommand().mPid = pid;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,46 +2,54 @@ package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.termux.TermuxBootstrap;
|
||||
import com.termux.shared.termux.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.termux.extrakeys.SpecialButton;
|
||||
import com.termux.shared.android.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.app.activities.ReportActivity;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.data.TermuxUrlUtils;
|
||||
import com.termux.shared.view.KeyboardUtils;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
@@ -49,24 +57,37 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
||||
private Runnable mShowSoftKeyboardRunnable;
|
||||
|
||||
private boolean mShowSoftKeyboardIgnoreOnce;
|
||||
private boolean mShowSoftKeyboardWithDelayOnce;
|
||||
|
||||
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
||||
|
||||
private List<KeyboardShortcut> mSessionShortcuts;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
||||
|
||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
this.mActivity = activity;
|
||||
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
this.mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||
}
|
||||
|
||||
public TermuxActivity getActivity() {
|
||||
return mActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onCreate() is called
|
||||
*/
|
||||
public void onCreate() {
|
||||
onReloadProperties();
|
||||
|
||||
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
|
||||
}
|
||||
@@ -75,10 +96,14 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
* Should be called when mActivity.onStart() is called
|
||||
*/
|
||||
public void onStart() {
|
||||
|
||||
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
|
||||
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
|
||||
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(mActivity.getPreferences().isTerminalViewKeyLoggingEnabled());
|
||||
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
|
||||
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||
|
||||
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
|
||||
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,10 +111,18 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
*/
|
||||
public void onResume() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(true, false);
|
||||
setSoftKeyboardState(true, mActivity.isActivityRecreated());
|
||||
|
||||
// Start terminal cursor blinking if enabled
|
||||
setTerminalCursorBlinkerState(true);
|
||||
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||
|
||||
if (mActivity.getTerminalView().mEmulator != null) {
|
||||
// Start terminal cursor blinking if enabled
|
||||
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
|
||||
// event to start it. This is needed since onEmulatorSet() may not be called after
|
||||
// TermuxActivity is started after device display timeout with double tap and not power button.
|
||||
setTerminalCursorBlinkerState(true);
|
||||
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,10 +133,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
setTerminalCursorBlinkerState(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.reloadProperties() is called
|
||||
*/
|
||||
public void onReloadProperties() {
|
||||
setSessionShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReload() {
|
||||
public void onReloadActivityStyling() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(false, true);
|
||||
|
||||
@@ -111,6 +151,23 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
setTerminalCursorBlinkerState(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when {@link com.termux.view.TerminalView#mEmulator} is set
|
||||
*/
|
||||
@Override
|
||||
public void onEmulatorSet() {
|
||||
if (!mTerminalCursorBlinkerStateAlreadySet) {
|
||||
// Start terminal cursor blinking if enabled
|
||||
// We need to wait for the first session to be attached that's set in
|
||||
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
|
||||
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
|
||||
// blinker will not start again if TermuxActivity is started again after exiting it with
|
||||
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
|
||||
setTerminalCursorBlinkerState(true);
|
||||
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@@ -127,10 +184,26 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
@Override
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
else
|
||||
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
|
||||
TerminalEmulator term = mActivity.getCurrentSession().getEmulator();
|
||||
|
||||
if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) {
|
||||
int[] columnAndRow = mActivity.getTerminalView().getColumnAndRow(e, true);
|
||||
String wordAtTap = term.getScreen().getWordAtLocation(columnAndRow[0], columnAndRow[1]);
|
||||
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(wordAtTap);
|
||||
|
||||
if (!urlSet.isEmpty()) {
|
||||
String url = (String) urlSet.iterator().next();
|
||||
ShareUtils.openUrl(mActivity, url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
else
|
||||
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,6 +221,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminalViewSelected() {
|
||||
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@@ -164,16 +242,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
||||
mTermuxTerminalSessionActivityClient.removeFinishedSession(currentSession);
|
||||
return true;
|
||||
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
||||
} else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
|
||||
e.isCtrlPressed() && e.isAltPressed()) {
|
||||
// Get the unmodified code point:
|
||||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(true);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(false);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
@@ -183,9 +262,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mActivity.getTerminalView().showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
mTermuxTerminalSessionClient.renameSession(currentSession);
|
||||
mTermuxTerminalSessionActivityClient.renameSession(currentSession);
|
||||
} else if (unicodeChar == 'c'/* create */) {
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
|
||||
} else if (unicodeChar == 'u' /* urls */) {
|
||||
showUrlSelection();
|
||||
} else if (unicodeChar == 'v') {
|
||||
@@ -198,7 +277,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
changeFontSize(false);
|
||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||
int index = unicodeChar - '1';
|
||||
mTermuxTerminalSessionClient.switchToSession(index);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -211,6 +290,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
|
||||
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
return true;
|
||||
}
|
||||
|
||||
return handleVirtualKeys(keyCode, e, false);
|
||||
}
|
||||
|
||||
@@ -236,12 +322,32 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
@Override
|
||||
public boolean readControlKey() {
|
||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||
return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readAltKey() {
|
||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||
return readExtraKeysSpecialButton(SpecialButton.ALT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readShiftKey() {
|
||||
return readExtraKeysSpecialButton(SpecialButton.SHIFT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readFnKey() {
|
||||
return readExtraKeysSpecialButton(SpecialButton.FN);
|
||||
}
|
||||
|
||||
public boolean readExtraKeysSpecialButton(SpecialButton specialButton) {
|
||||
if (mActivity.getExtraKeysView() == null) return false;
|
||||
Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true);
|
||||
if (state == null) {
|
||||
Logger.logError(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys.");
|
||||
return false;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -355,11 +461,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
return true;
|
||||
} else if (ctrlDown) {
|
||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
||||
mTermuxTerminalSessionActivityClient.removeFinishedSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
||||
List<KeyboardShortcut> shortcuts = mSessionShortcuts;
|
||||
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||
@@ -367,16 +473,16 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
if (codePointLowerCase == shortcut.codePoint) {
|
||||
switch (shortcut.shortcutAction) {
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(true);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
mTermuxTerminalSessionActivityClient.switchToSession(false);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
||||
mTermuxTerminalSessionActivityClient.renameSession(mActivity.getCurrentSession());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -387,6 +493,27 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal sessions shortcuts.
|
||||
*/
|
||||
private void setSessionShortcuts() {
|
||||
mSessionShortcuts = new ArrayList<>();
|
||||
|
||||
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
|
||||
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
|
||||
// The mMap stores the code points for the session shortcuts while loading properties
|
||||
Integer codePoint = (Integer) mActivity.getProperties().getInternalPropertyValue(entry.getKey(), true);
|
||||
// If codePoint is null, then session shortcut did not exist in properties or was invalid
|
||||
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
|
||||
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
|
||||
// add the code point to sessionShortcuts
|
||||
if (codePoint != null)
|
||||
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void changeFontSize(boolean increase) {
|
||||
@@ -409,10 +536,19 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
mActivity.getPreferences().setSoftKeyboardEnabled(false);
|
||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
} else {
|
||||
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
|
||||
// switching back from another app if keyboard was previously disabled by user.
|
||||
// Also request focus, since it wouldn't have been requested at startup by
|
||||
// setSoftKeyboardState if keyboard was disabled. #2112
|
||||
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
|
||||
mActivity.getPreferences().setSoftKeyboardEnabled(true);
|
||||
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
if(mShowSoftKeyboardWithDelayOnce) {
|
||||
mShowSoftKeyboardWithDelayOnce = false;
|
||||
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
} else
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
}
|
||||
}
|
||||
// If soft keyboard toggle behaviour is show/hide
|
||||
@@ -430,15 +566,31 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
}
|
||||
|
||||
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
||||
boolean noShowKeyboard = false;
|
||||
|
||||
// Requesting terminal view focus is necessary regardless of if soft keyboard is to be
|
||||
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
|
||||
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
|
||||
// tint will be added to the terminal as highlight for the focussed view. Test with a light
|
||||
// theme. For android 8.+, the "defaultFocusHighlightEnabled" attribute is also set to false
|
||||
// in TerminalView layout to fix the issue.
|
||||
|
||||
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
||||
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
||||
mActivity.getPreferences().isSoftKeyboardEnabled(),
|
||||
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
||||
mActivity.getPreferences().isSoftKeyboardEnabled(),
|
||||
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
||||
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
noShowKeyboard = true;
|
||||
// Delay is only required if onCreate() is called like when Termux app is exited with
|
||||
// double back press, not when Termux app is switched back from another app and keyboard
|
||||
// toggle is pressed to enable keyboard
|
||||
if (isStartup && mActivity.isOnResumeAfterOnCreate())
|
||||
mShowSoftKeyboardWithDelayOnce = true;
|
||||
} else {
|
||||
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
|
||||
KeyboardUtils.setResizeTerminalViewForSoftKeyboardFlags(mActivity);
|
||||
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
|
||||
|
||||
// Clear any previous flags to disable soft keyboard in case setting updated
|
||||
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||
@@ -446,33 +598,60 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
// If soft keyboard is to be hidden on startup
|
||||
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
|
||||
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
|
||||
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
// Required to keep keyboard hidden when Termux app is switched back from another app
|
||||
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
||||
} else {
|
||||
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
||||
if (isReloadTermuxProperties)
|
||||
return;
|
||||
|
||||
if (mShowSoftKeyboardRunnable == null) {
|
||||
mShowSoftKeyboardRunnable = () -> {
|
||||
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
};
|
||||
}
|
||||
|
||||
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View view, boolean hasFocus) {
|
||||
// Force show soft keyboard if TerminalView has focus and close it if it doesn't
|
||||
KeyboardUtils.setSoftKeyboardVisibility(mShowSoftKeyboardRunnable, mActivity, mActivity.getTerminalView(), hasFocus);
|
||||
}
|
||||
});
|
||||
|
||||
// Request focus for TerminalView
|
||||
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
noShowKeyboard = true;
|
||||
// Required to keep keyboard hidden on app startup
|
||||
mShowSoftKeyboardIgnoreOnce = true;
|
||||
}
|
||||
}
|
||||
|
||||
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View view, boolean hasFocus) {
|
||||
// Force show soft keyboard if TerminalView or toolbar text input view has
|
||||
// focus and close it if they don't
|
||||
boolean textInputViewHasFocus = false;
|
||||
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
|
||||
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
|
||||
|
||||
if (hasFocus || textInputViewHasFocus) {
|
||||
if (mShowSoftKeyboardIgnoreOnce) {
|
||||
mShowSoftKeyboardIgnoreOnce = false; return;
|
||||
}
|
||||
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
||||
} else {
|
||||
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
|
||||
}
|
||||
|
||||
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
|
||||
}
|
||||
});
|
||||
|
||||
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
||||
// or soft keyboard is to be hidden or is disabled
|
||||
if (!isReloadTermuxProperties && !noShowKeyboard) {
|
||||
// Request focus for TerminalView
|
||||
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
|
||||
// had focus on startup to show the keyboard, like when opening url with context menu
|
||||
// "Select URL" long press and returning to Termux app with back button. This
|
||||
// will also show keyboard even if it was closed before opening url. #2111
|
||||
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
private Runnable getShowSoftKeyboardRunnable() {
|
||||
if (mShowSoftKeyboardRunnable == null) {
|
||||
mShowSoftKeyboardRunnable = () -> {
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
};
|
||||
}
|
||||
return mShowSoftKeyboardRunnable;
|
||||
}
|
||||
|
||||
|
||||
@@ -499,17 +678,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||
if (transcriptText == null) return;
|
||||
|
||||
try {
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
||||
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||
}
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
|
||||
transcriptText, mActivity.getString(R.string.title_share_transcript_with));
|
||||
}
|
||||
|
||||
public void shareSelectedText() {
|
||||
String selectedText = mActivity.getTerminalView().getStoredSelectedText();
|
||||
if (DataUtils.isNullOrEmpty(selectedText)) return;
|
||||
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text),
|
||||
selectedText, mActivity.getString(R.string.title_share_selected_text_with));
|
||||
}
|
||||
|
||||
public void showUrlSelection() {
|
||||
@@ -518,7 +697,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
|
||||
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(text);
|
||||
if (urlSet.isEmpty()) {
|
||||
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||
return;
|
||||
@@ -530,9 +709,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
// Click to copy url to clipboard:
|
||||
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||
String url = (String) urls[which];
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
|
||||
}).setTitle(R.string.title_select_url_dialog).create();
|
||||
|
||||
// Long press to open URL:
|
||||
@@ -541,13 +718,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||
dialog.dismiss();
|
||||
String url = (String) urls[position];
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
try {
|
||||
mActivity.startActivity(i, null);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// If no applications match, Android displays a system message.
|
||||
mActivity.startActivity(Intent.createChooser(i, null));
|
||||
}
|
||||
ShareUtils.openUrl(mActivity, url);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -562,29 +733,58 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||
if (transcriptText == null) return;
|
||||
|
||||
MessageDialogUtils.showMessage(mActivity, TermuxConstants.TERMUX_APP_NAME + " Report Issue",
|
||||
mActivity.getString(R.string.msg_add_termux_debug_info),
|
||||
mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
|
||||
mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
|
||||
null);
|
||||
}
|
||||
|
||||
private void reportIssueFromTranscript(String transcriptText, boolean addTermuxDebugInfo) {
|
||||
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
String transcriptTextTruncated = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
||||
|
||||
reportString.append("## Transcript\n");
|
||||
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true));
|
||||
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
|
||||
reportString.append("\n##\n");
|
||||
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
|
||||
if (addTermuxDebugInfo) {
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
|
||||
} else {
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_PACKAGE));
|
||||
}
|
||||
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity, true));
|
||||
|
||||
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||
if (TermuxBootstrap.isAppPackageManagerAPT()) {
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
}
|
||||
|
||||
if (addTermuxDebugInfo) {
|
||||
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
|
||||
if (termuxDebugInfo != null)
|
||||
reportString.append("\n\n").append(termuxDebugInfo);
|
||||
}
|
||||
|
||||
String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
|
||||
|
||||
ReportInfo reportInfo = new ReportInfo(userActionName,
|
||||
TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title);
|
||||
reportInfo.setReportString(reportString.toString());
|
||||
reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity));
|
||||
reportInfo.setReportSaveFileLabelAndPath(userActionName,
|
||||
Environment.getExternalStorageDirectory() + "/" +
|
||||
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
|
||||
|
||||
ReportActivity.startReportActivity(mActivity, reportInfo);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
@@ -594,12 +794,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
if (session == null) return;
|
||||
if (!session.isRunning()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData == null) return;
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||
if (!TextUtils.isEmpty(paste))
|
||||
session.getEmulator().paste(paste.toString());
|
||||
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
|
||||
if (text != null)
|
||||
session.getEmulator().paste(text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysView;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
public class TerminalToolbarViewPager {
|
||||
@@ -44,9 +44,11 @@ public class TerminalToolbarViewPager {
|
||||
if (position == 0) {
|
||||
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
||||
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
|
||||
extraKeysView.setExtraKeysViewClient(mActivity.getTermuxTerminalExtraKeys());
|
||||
extraKeysView.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps());
|
||||
mActivity.setExtraKeysView(extraKeysView);
|
||||
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||
extraKeysView.reload(mActivity.getTermuxTerminalExtraKeys().getExtraKeysInfo(),
|
||||
mActivity.getTerminalToolbarDefaultHeight());
|
||||
|
||||
// apply extra keys fix if enabled in prefs
|
||||
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysConstants;
|
||||
import com.termux.shared.termux.extrakeys.ExtraKeysInfo;
|
||||
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.termux.settings.properties.TermuxSharedProperties;
|
||||
import com.termux.shared.termux.terminal.io.TerminalExtraKeys;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
|
||||
|
||||
private ExtraKeysInfo mExtraKeysInfo;
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
final TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalExtraKeys";
|
||||
|
||||
public TermuxTerminalExtraKeys(TermuxActivity activity, @NonNull TerminalView terminalView,
|
||||
TermuxTerminalViewClient termuxTerminalViewClient,
|
||||
TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||
super(terminalView);
|
||||
|
||||
mActivity = activity;
|
||||
mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||
|
||||
setExtraKeys();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the terminal extra keys and style.
|
||||
*/
|
||||
private void setExtraKeys() {
|
||||
mExtraKeysInfo = null;
|
||||
|
||||
try {
|
||||
// The mMap stores the extra key and style string values while loading properties
|
||||
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
|
||||
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
||||
String extrakeys = (String) mActivity.getProperties().getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||
String extraKeysStyle = (String) mActivity.getProperties().getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||
|
||||
ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle);
|
||||
if (ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(extraKeysStyle)) {
|
||||
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + extraKeysStyle + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
|
||||
extraKeysStyle = TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE;
|
||||
}
|
||||
|
||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
|
||||
} catch (JSONException e) {
|
||||
Logger.showToast(mActivity, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
||||
|
||||
try {
|
||||
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
|
||||
} catch (JSONException e2) {
|
||||
Logger.showToast(mActivity, "Can't create default extra keys",true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||
mExtraKeysInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ExtraKeysInfo getExtraKeysInfo() {
|
||||
return mExtraKeysInfo;
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
@Override
|
||||
public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
|
||||
if ("KEYBOARD".equals(key)) {
|
||||
if(mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
|
||||
} else if ("DRAWER".equals(key)) {
|
||||
DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer();
|
||||
if (drawerLayout.isDrawerOpen(Gravity.LEFT))
|
||||
drawerLayout.closeDrawer(Gravity.LEFT);
|
||||
else
|
||||
drawerLayout.openDrawer(Gravity.LEFT);
|
||||
} else if ("PASTE".equals(key)) {
|
||||
if(mTermuxTerminalSessionActivityClient != null)
|
||||
mTermuxTerminalSessionActivityClient.onPasteTextFromClipboard(null);
|
||||
} else if ("SCROLL".equals(key)) {
|
||||
TerminalView terminalView = mTermuxTerminalViewClient.getActivity().getTerminalView();
|
||||
if (terminalView != null && terminalView.mEmulator != null)
|
||||
terminalView.mEmulator.toggleAutoScrollDisabled();
|
||||
} else {
|
||||
super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExtraKeyButton {
|
||||
|
||||
/**
|
||||
* The key that will be sent to the terminal, either a control character
|
||||
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||
* some text.
|
||||
*/
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||
*/
|
||||
private final boolean macro;
|
||||
|
||||
/**
|
||||
* The text that will be shown on the button.
|
||||
*/
|
||||
private final String display;
|
||||
|
||||
/**
|
||||
* The information of the popup (triggered by swipe up).
|
||||
*/
|
||||
@Nullable
|
||||
private ExtraKeyButton popup;
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||
this(charDisplayMap, config, null);
|
||||
}
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
|
||||
String keyFromConfig = config.optString("key", null);
|
||||
String macroFromConfig = config.optString("macro", null);
|
||||
String[] keys;
|
||||
if (keyFromConfig != null && macroFromConfig != null) {
|
||||
throw new JSONException("Both key and macro can't be set for the same key");
|
||||
} else if (keyFromConfig != null) {
|
||||
keys = new String[]{keyFromConfig};
|
||||
this.macro = false;
|
||||
} else if (macroFromConfig != null) {
|
||||
keys = macroFromConfig.split(" ");
|
||||
this.macro = true;
|
||||
} else {
|
||||
throw new JSONException("All keys have to specify either key or macro");
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
|
||||
}
|
||||
|
||||
this.key = TextUtils.join(" ", keys);
|
||||
|
||||
String displayFromConfig = config.optString("display", null);
|
||||
if (displayFromConfig != null) {
|
||||
this.display = displayFromConfig;
|
||||
} else {
|
||||
this.display = Arrays.stream(keys)
|
||||
.map(key -> charDisplayMap.get(key, key))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public boolean isMacro() {
|
||||
return macro;
|
||||
}
|
||||
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ExtraKeyButton getPopup() {
|
||||
return popup;
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ExtraKeysInfo {
|
||||
|
||||
/**
|
||||
* Matrix of buttons displayed
|
||||
*/
|
||||
private final ExtraKeyButton[][] buttons;
|
||||
|
||||
/**
|
||||
* This corresponds to one of the CharMapDisplay below
|
||||
*/
|
||||
private String style;
|
||||
|
||||
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
|
||||
this.style = style;
|
||||
|
||||
// Convert String propertiesInfo to Array of Arrays
|
||||
JSONArray arr = new JSONArray(propertiesInfo);
|
||||
Object[][] matrix = new Object[arr.length()][];
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONArray line = arr.getJSONArray(i);
|
||||
matrix[i] = new Object[line.length()];
|
||||
for (int j = 0; j < line.length(); j++) {
|
||||
matrix[i][j] = line.get(j);
|
||||
}
|
||||
}
|
||||
|
||||
// convert matrix to buttons
|
||||
this.buttons = new ExtraKeyButton[matrix.length][];
|
||||
for (int i = 0; i < matrix.length; i++) {
|
||||
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
|
||||
for (int j = 0; j < matrix[i].length; j++) {
|
||||
Object key = matrix[i][j];
|
||||
|
||||
JSONObject jobject = normalizeKeyConfig(key);
|
||||
|
||||
ExtraKeyButton button;
|
||||
|
||||
if (! jobject.has("popup")) {
|
||||
// no popup
|
||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||
} else {
|
||||
// a popup
|
||||
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
|
||||
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
|
||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
|
||||
}
|
||||
|
||||
this.buttons[i][j] = button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "hello" -> {"key": "hello"}
|
||||
*/
|
||||
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||
JSONObject jobject;
|
||||
if (key instanceof String) {
|
||||
jobject = new JSONObject();
|
||||
jobject.put("key", key);
|
||||
} else if (key instanceof JSONObject) {
|
||||
jobject = (JSONObject) key;
|
||||
} else {
|
||||
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||
}
|
||||
return jobject;
|
||||
}
|
||||
|
||||
public ExtraKeyButton[][] getMatrix() {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* HashMap that implements Python dict.get(key, default) function.
|
||||
* Default java.util .get(key) is then the same as .get(key, null);
|
||||
*/
|
||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||
V get(K key, V defaultValue) {
|
||||
if (containsKey(key))
|
||||
return get(key);
|
||||
else
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
static class CharDisplayMap extends CleverMap<String, String> {}
|
||||
|
||||
/**
|
||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
||||
*/
|
||||
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
|
||||
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||
}};
|
||||
|
||||
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
|
||||
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
||||
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
||||
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
||||
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
||||
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
|
||||
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||
}};
|
||||
|
||||
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
|
||||
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
||||
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
||||
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
||||
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
||||
}};
|
||||
|
||||
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
|
||||
// alternative to classic arrow keys
|
||||
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||
}};
|
||||
|
||||
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
|
||||
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
||||
// put("FN", "FN"); // no ISO character exists
|
||||
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
||||
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
||||
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
||||
}};
|
||||
|
||||
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
|
||||
// nicer looking for most cases
|
||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||
}};
|
||||
|
||||
/*
|
||||
* Multiple maps are available to quickly change
|
||||
* the style of the keys.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some classic symbols everybody knows
|
||||
*/
|
||||
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(nicerLookingDisplay);
|
||||
// all other characters are displayed as themselves
|
||||
}};
|
||||
|
||||
/**
|
||||
* Classic symbols and less known symbols
|
||||
*/
|
||||
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(lessKnownCharactersDisplay); // NEW
|
||||
putAll(nicerLookingDisplay);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Only arrows
|
||||
*/
|
||||
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||
putAll(nicerLookingDisplay);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Full Iso
|
||||
*/
|
||||
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(lessKnownCharactersDisplay); // NEW
|
||||
putAll(nicerLookingDisplay);
|
||||
putAll(notKnownIsoCharacters); // NEW
|
||||
}};
|
||||
|
||||
/**
|
||||
* Some people might call our keys differently
|
||||
*/
|
||||
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
|
||||
put("ESCAPE", "ESC");
|
||||
put("CONTROL", "CTRL");
|
||||
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
||||
put("FUNCTION", "FN");
|
||||
// no alias for ALT
|
||||
|
||||
// Directions are sometimes written as first and last letter for brevety
|
||||
put("LT", "LEFT");
|
||||
put("RT", "RIGHT");
|
||||
put("DN", "DOWN");
|
||||
// put("UP", "UP"); well, "UP" is already two letters
|
||||
|
||||
put("PAGEUP", "PGUP");
|
||||
put("PAGE_UP", "PGUP");
|
||||
put("PAGE UP", "PGUP");
|
||||
put("PAGE-UP", "PGUP");
|
||||
|
||||
// no alias for HOME
|
||||
// no alias for END
|
||||
|
||||
put("PAGEDOWN", "PGDN");
|
||||
put("PAGE_DOWN", "PGDN");
|
||||
put("PAGE-DOWN", "PGDN");
|
||||
|
||||
put("DELETE", "DEL");
|
||||
put("BACKSPACE", "BKSP");
|
||||
|
||||
// easier for writing in termux.properties
|
||||
put("BACKSLASH", "\\");
|
||||
put("QUOTE", "\"");
|
||||
put("APOSTROPHE", "'");
|
||||
}};
|
||||
|
||||
CharDisplayMap getSelectedCharMap() {
|
||||
switch (style) {
|
||||
case "arrows-only":
|
||||
return arrowsOnlyCharDisplay;
|
||||
case "arrows-all":
|
||||
return lotsOfArrowsCharDisplay;
|
||||
case "all":
|
||||
return fullIsoCharDisplay;
|
||||
case "none":
|
||||
return new CharDisplayMap();
|
||||
default:
|
||||
if (!TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(style))
|
||||
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + style + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
|
||||
return defaultCharDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
|
||||
* Modifies the array, doesn't return a new one.
|
||||
*/
|
||||
public static String replaceAlias(String key) {
|
||||
return controlCharsAliases.get(key, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,388 +0,0 @@
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import android.view.Gravity;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
/**
|
||||
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||
* keyboard.
|
||||
*/
|
||||
public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int BUTTON_COLOR = 0x00000000;
|
||||
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
||||
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
|
||||
|
||||
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
|
||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
|
||||
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||
put("END", KeyEvent.KEYCODE_MOVE_END);
|
||||
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
||||
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
||||
put("INS", KeyEvent.KEYCODE_INSERT);
|
||||
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
||||
put("BKSP", KeyEvent.KEYCODE_DEL);
|
||||
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
||||
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||
put("F1", KeyEvent.KEYCODE_F1);
|
||||
put("F2", KeyEvent.KEYCODE_F2);
|
||||
put("F3", KeyEvent.KEYCODE_F3);
|
||||
put("F4", KeyEvent.KEYCODE_F4);
|
||||
put("F5", KeyEvent.KEYCODE_F5);
|
||||
put("F6", KeyEvent.KEYCODE_F6);
|
||||
put("F7", KeyEvent.KEYCODE_F7);
|
||||
put("F8", KeyEvent.KEYCODE_F8);
|
||||
put("F9", KeyEvent.KEYCODE_F9);
|
||||
put("F10", KeyEvent.KEYCODE_F10);
|
||||
put("F11", KeyEvent.KEYCODE_F11);
|
||||
put("F12", KeyEvent.KEYCODE_F12);
|
||||
}};
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||
if ("KEYBOARD".equals(keyName)) {
|
||||
if(mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
|
||||
} else if ("DRAWER".equals(keyName)) {
|
||||
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||
drawer.openDrawer(Gravity.LEFT);
|
||||
} else if (keyCodesForString.containsKey(keyName)) {
|
||||
Integer keyCode = keyCodesForString.get(keyName);
|
||||
if (keyCode == null) return;
|
||||
int metaState = 0;
|
||||
if (forceCtrlDown) {
|
||||
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||
}
|
||||
if (forceLeftAltDown) {
|
||||
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||
}
|
||||
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
|
||||
terminalView.onKeyDown(keyCode, keyEvent);
|
||||
} else {
|
||||
// not a control char
|
||||
keyName.codePoints().forEach(codePoint -> {
|
||||
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sendKey(View view, ExtraKeyButton buttonInfo) {
|
||||
if (buttonInfo.isMacro()) {
|
||||
String[] keys = buttonInfo.getKey().split(" ");
|
||||
boolean ctrlDown = false;
|
||||
boolean altDown = false;
|
||||
for (String key : keys) {
|
||||
if ("CTRL".equals(key)) {
|
||||
ctrlDown = true;
|
||||
} else if ("ALT".equals(key)) {
|
||||
altDown = true;
|
||||
} else {
|
||||
sendKey(view, key, ctrlDown, altDown);
|
||||
ctrlDown = false;
|
||||
altDown = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendKey(view, buttonInfo.getKey(), false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public enum SpecialButton {
|
||||
CTRL, ALT, FN
|
||||
}
|
||||
|
||||
private static class SpecialButtonState {
|
||||
boolean isOn = false;
|
||||
boolean isActive = false;
|
||||
List<Button> buttons = new ArrayList<>();
|
||||
|
||||
void setIsActive(boolean value) {
|
||||
isActive = value;
|
||||
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||
put(SpecialButton.CTRL, new SpecialButtonState());
|
||||
put(SpecialButton.ALT, new SpecialButtonState());
|
||||
put(SpecialButton.FN, new SpecialButtonState());
|
||||
}};
|
||||
|
||||
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
|
||||
|
||||
private boolean isSpecialButton(ExtraKeyButton button) {
|
||||
return specialButtonsKeys.contains(button.getKey());
|
||||
}
|
||||
|
||||
private ScheduledExecutorService scheduledExecutor;
|
||||
private PopupWindow popupWindow;
|
||||
private int longPressCount;
|
||||
|
||||
public boolean readSpecialButton(SpecialButton name) {
|
||||
SpecialButtonState state = specialButtons.get(name);
|
||||
if (state == null)
|
||||
throw new RuntimeException("Must be a valid special button (see source)");
|
||||
|
||||
if (!state.isOn || !state.isActive)
|
||||
return false;
|
||||
|
||||
state.setIsActive(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||
if (state == null) return null;
|
||||
state.isOn = true;
|
||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
|
||||
if (needUpdate) {
|
||||
state.buttons.add(button);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
void popup(View view, ExtraKeyButton extraButton) {
|
||||
int width = view.getMeasuredWidth();
|
||||
int height = view.getMeasuredHeight();
|
||||
Button button;
|
||||
if (isSpecialButton(extraButton)) {
|
||||
button = createSpecialButton(extraButton.getKey(), false);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
}
|
||||
button.setText(extraButton.getDisplay());
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
button.setMinHeight(0);
|
||||
button.setMinWidth(0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinimumHeight(0);
|
||||
button.setWidth(width);
|
||||
button.setHeight(height);
|
||||
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
popupWindow = new PopupWindow(this);
|
||||
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setContentView(button);
|
||||
popupWindow.setOutsideTouchable(true);
|
||||
popupWindow.setFocusable(false);
|
||||
popupWindow.showAsDropDown(view, 0, -2 * height);
|
||||
}
|
||||
|
||||
/**
|
||||
* General util function to compute the longest column length in a matrix.
|
||||
*/
|
||||
static int maximumLength(Object[][] matrix) {
|
||||
int m = 0;
|
||||
for (Object[] row : matrix)
|
||||
m = Math.max(m, row.length);
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the view given parameters in termux.properties
|
||||
*
|
||||
* @param infos matrix as defined in termux.properties extrakeys
|
||||
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
|
||||
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
|
||||
* Any string of length > 1 in total Uppercase will print a warning
|
||||
*
|
||||
* Examples:
|
||||
* "ENTER" will trigger the ENTER keycode
|
||||
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
|
||||
* "→" will input a "→" character
|
||||
* "−" will input a "−" character
|
||||
* "-_-" will input the string "-_-"
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void reload(ExtraKeysInfo infos) {
|
||||
if (infos == null)
|
||||
return;
|
||||
|
||||
for(SpecialButtonState state : specialButtons.values())
|
||||
state.buttons = new ArrayList<>();
|
||||
|
||||
removeAllViews();
|
||||
|
||||
ExtraKeyButton[][] buttons = infos.getMatrix();
|
||||
|
||||
setRowCount(buttons.length);
|
||||
setColumnCount(maximumLength(buttons));
|
||||
|
||||
for (int row = 0; row < buttons.length; row++) {
|
||||
for (int col = 0; col < buttons[row].length; col++) {
|
||||
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||
|
||||
Button button;
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
}
|
||||
|
||||
button.setText(buttonInfo.getDisplay());
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
|
||||
final Button finalButton = button;
|
||||
button.setOnClickListener(v -> {
|
||||
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else {
|
||||
// Perform haptic feedback only if no total silence mode enabled.
|
||||
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
|
||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
View root = getRootView();
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||
if (state == null) return;
|
||||
state.setIsActive(!state.isActive);
|
||||
} else {
|
||||
sendKey(root, buttonInfo);
|
||||
}
|
||||
});
|
||||
|
||||
button.setOnTouchListener((v, event) -> {
|
||||
final View root = getRootView();
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
longPressCount = 0;
|
||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
|
||||
// autorepeat
|
||||
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
scheduledExecutor.scheduleWithFixedDelay(() -> {
|
||||
longPressCount++;
|
||||
sendKey(root, buttonInfo);
|
||||
}, 400, 80, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
if (popupWindow == null && event.getY() < 0) {
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
popup(v, buttonInfo.getPopup());
|
||||
}
|
||||
if (popupWindow != null && event.getY() > 0) {
|
||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
popupWindow.dismiss();
|
||||
popupWindow = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
if (longPressCount == 0 || popupWindow != null) {
|
||||
if (popupWindow != null) {
|
||||
popupWindow.setContentView(null);
|
||||
popupWindow.dismiss();
|
||||
popupWindow = null;
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
if (isSpecialButton(buttonInfo.getPopup())) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
|
||||
if (state == null) return true;
|
||||
state.setIsActive(!state.isActive);
|
||||
} else {
|
||||
sendKey(root, buttonInfo.getPopup());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
v.performClick();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
LayoutParams param = new GridLayout.LayoutParams();
|
||||
param.width = 0;
|
||||
param.height = 0;
|
||||
param.setMargins(0, 0, 0, 0);
|
||||
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
|
||||
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
||||
button.setLayoutParams(param);
|
||||
|
||||
addView(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
|
||||
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.activities.ReportActivity;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public class CrashUtils {
|
||||
|
||||
private static final String LOG_TAG = "CrashUtils";
|
||||
|
||||
/**
|
||||
* Notify the user of a previous app crash by reading the crash info from the crash log file at
|
||||
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||
*
|
||||
* If the crash log file exists and is not empty and
|
||||
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
|
||||
* enabled, then a notification will be shown for the crash on the
|
||||
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
|
||||
*
|
||||
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTagParam The log tag to use for logging.
|
||||
*/
|
||||
public static void notifyCrash(final Context context, final String logTagParam) {
|
||||
if (context == null) return;
|
||||
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for crashes
|
||||
if (!preferences.areCrashReportNotificationsEnabled())
|
||||
return;
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
|
||||
|
||||
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
||||
return;
|
||||
|
||||
String errmsg;
|
||||
StringBuilder reportStringBuilder = new StringBuilder();
|
||||
|
||||
// Read report string from crash log file
|
||||
errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||
if (errmsg != null) {
|
||||
Logger.logError(logTag, errmsg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move crash log file to backup location if it exists
|
||||
FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||
if (errmsg != null) {
|
||||
Logger.logError(logTag, errmsg);
|
||||
}
|
||||
|
||||
String reportString = reportStringBuilder.toString();
|
||||
|
||||
if (reportString == null || reportString.isEmpty())
|
||||
return;
|
||||
|
||||
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||
// to show the details of the crash
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||
|
||||
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
|
||||
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT, logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupCrashReportsNotificationChannel(context);
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param title The title for the notification.
|
||||
* @param notificationText The second line text of the notification.
|
||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||
* @return Returns the {@link Notification.Builder}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||
|
||||
if (builder == null) return null;
|
||||
|
||||
// Enable timestamp
|
||||
builder.setShowWhen(true);
|
||||
|
||||
// Set notification icon
|
||||
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||
|
||||
// Set background color for small notification icon
|
||||
builder.setColor(0xFF607D8B);
|
||||
|
||||
// Dismiss on click
|
||||
builder.setAutoCancel(true);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the notification channel for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} and
|
||||
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
public static void setupCrashReportsNotificationChannel(final Context context) {
|
||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.activities.ReportActivity;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||
import com.termux.shared.settings.properties.SharedProperties;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class PluginUtils {
|
||||
|
||||
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
|
||||
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
|
||||
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
|
||||
* Execute permissions should be attempted to be set, but ignored if they are missing */
|
||||
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
|
||||
|
||||
private static final String LOG_TAG = "PluginUtils";
|
||||
|
||||
/**
|
||||
* Process {@link ExecutionCommand} result.
|
||||
*
|
||||
* The ExecutionCommand currentState must be greater or equal to
|
||||
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
|
||||
* are sent back to the {@link PendingIntent} creator.
|
||||
*
|
||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param executionCommand The {@link ExecutionCommand} to process.
|
||||
*/
|
||||
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
if (!executionCommand.hasExecuted()) {
|
||||
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
||||
|
||||
boolean result = true;
|
||||
|
||||
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
||||
// send pluginPendingIntent to its creator with the result
|
||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
||||
String errmsg = executionCommand.errmsg;
|
||||
|
||||
//Combine errmsg and stacktraces
|
||||
if (executionCommand.isStateFailed()) {
|
||||
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||
}
|
||||
|
||||
// Send pluginPendingIntent to its creator
|
||||
result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||
}
|
||||
|
||||
if (!executionCommand.isStateFailed() && result)
|
||||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {@link ExecutionCommand} error.
|
||||
*
|
||||
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
||||
* The {@link ExecutionCommand#errCode} must have been set to a value greater than
|
||||
* {@link ExecutionCommand#RESULT_CODE_OK}.
|
||||
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
|
||||
* be set with appropriate error info.
|
||||
*
|
||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands
|
||||
* are sent back to the {@link PendingIntent} creator.
|
||||
*
|
||||
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||
* enabled, then a flash and a notification will be shown for the error as well
|
||||
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
|
||||
* the error.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
||||
* regardless of if pending intent is {@code null} or
|
||||
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
*/
|
||||
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
||||
if (context == null || executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
if (!executionCommand.isStateFailed()) {
|
||||
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the error and any exception
|
||||
Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList);
|
||||
|
||||
|
||||
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
||||
// send pluginPendingIntent to its creator with the errors
|
||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
||||
String errmsg = executionCommand.errmsg;
|
||||
|
||||
//Combine errmsg and stacktraces
|
||||
if (executionCommand.isStateFailed()) {
|
||||
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||
}
|
||||
|
||||
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||
|
||||
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
||||
if (!forceNotification) return;
|
||||
}
|
||||
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for plugin, then just return
|
||||
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
||||
return;
|
||||
|
||||
// Flash the errmsg
|
||||
Logger.showToast(context, executionCommand.errmsg, true);
|
||||
|
||||
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
|
||||
// to show the details of the error
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
||||
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,true));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupPluginCommandErrorsNotificationChannel(context);
|
||||
|
||||
// Use markdown in notification
|
||||
CharSequence notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
|
||||
//CharSequence notificationText = executionCommand.errmsg;
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send {@link ExecutionCommand} result {@link PendingIntent} in the
|
||||
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle.
|
||||
*
|
||||
*
|
||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param label The label of {@link ExecutionCommand}.
|
||||
* @param stdout The stdout of {@link ExecutionCommand}.
|
||||
* @param stderr The stderr of {@link ExecutionCommand}.
|
||||
* @param exitCode The exitCode of {@link ExecutionCommand}.
|
||||
* @param errCode The errCode of {@link ExecutionCommand}.
|
||||
* @param errmsg The errmsg of {@link ExecutionCommand}.
|
||||
* @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}.
|
||||
* @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}.
|
||||
*/
|
||||
public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) {
|
||||
if (context == null || pluginPendingIntent == null) return false;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage());
|
||||
|
||||
String truncatedStdout = null;
|
||||
String truncatedStderr = null;
|
||||
|
||||
String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length());
|
||||
String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length());
|
||||
|
||||
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
||||
if (stderr == null || stderr.isEmpty()) {
|
||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||
} else if (stdout == null || stdout.isEmpty()) {
|
||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||
} else {
|
||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||
}
|
||||
|
||||
if (truncatedStdout != null && truncatedStdout.length() < stdout.length()) {
|
||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
||||
stdout = truncatedStdout;
|
||||
}
|
||||
|
||||
if (truncatedStderr != null && truncatedStderr.length() < stderr.length()) {
|
||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
||||
stderr = truncatedStderr;
|
||||
}
|
||||
|
||||
String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length());
|
||||
|
||||
// Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
||||
// trim from end to preserve start of stacktraces
|
||||
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
||||
if (truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()) {
|
||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
||||
errmsg = truncatedErrmsg;
|
||||
}
|
||||
|
||||
|
||||
final Bundle resultBundle = new Bundle();
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength);
|
||||
if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode);
|
||||
if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
|
||||
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
|
||||
|
||||
try {
|
||||
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
// The caller doesn't want the result? That's fine, just ignore
|
||||
Logger.logDebug(logTag, "The Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param title The title for the notification.
|
||||
* @param notificationText The second line text of the notification.
|
||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||
* @return Returns the {@link Notification.Builder}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||
|
||||
if (builder == null) return null;
|
||||
|
||||
// Enable timestamp
|
||||
builder.setShowWhen(true);
|
||||
|
||||
// Set notification icon
|
||||
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||
|
||||
// Set background color for small notification icon
|
||||
builder.setColor(0xFF607D8B);
|
||||
|
||||
// Dismiss on click
|
||||
builder.setAutoCancel(true);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the notification channel for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
|
||||
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||
*
|
||||
* @param context The {@link Context} to get error string.
|
||||
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
|
||||
*/
|
||||
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||
String errmsg = null;
|
||||
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
|
||||
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
|
||||
}
|
||||
|
||||
return errmsg;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package com.termux.filepicker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Patterns;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TermuxFileReceiverActivity extends Activity {
|
||||
|
||||
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
|
||||
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
|
||||
static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener";
|
||||
|
||||
/**
|
||||
* If the activity should be finished when the name input dialog is dismissed. This is disabled
|
||||
* before showing an error dialog, since the act of showing the error dialog will cause the
|
||||
* name input dialog to be implicitly dismissed, and we do not want to finish the activity directly
|
||||
* when showing the error dialog.
|
||||
*/
|
||||
boolean mFinishOnDismissNameDialog = true;
|
||||
|
||||
private static final String LOG_TAG = "TermuxFileReceiverActivity";
|
||||
|
||||
static boolean isSharedTextAnUrl(String sharedText) {
|
||||
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
final Intent intent = getIntent();
|
||||
final String action = intent.getAction();
|
||||
final String type = intent.getType();
|
||||
final String scheme = intent.getScheme();
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null) {
|
||||
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
|
||||
if (sharedText != null) {
|
||||
if (isSharedTextAnUrl(sharedText)) {
|
||||
handleUrlAndFinish(sharedText);
|
||||
} else {
|
||||
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||
if (subject == null) subject = intent.getStringExtra(Intent.EXTRA_TITLE);
|
||||
if (subject != null) subject += ".txt";
|
||||
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
|
||||
}
|
||||
} else if (sharedUri != null) {
|
||||
handleContentUri(sharedUri, intent.getStringExtra(Intent.EXTRA_TITLE));
|
||||
} else {
|
||||
showErrorDialogAndQuit("Send action without content - nothing to save.");
|
||||
}
|
||||
} else if ("content".equals(scheme)) {
|
||||
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
|
||||
} else if ("file".equals(scheme)) {
|
||||
// When e.g. clicking on a downloaded apk:
|
||||
String path = intent.getData().getPath();
|
||||
File file = new File(path);
|
||||
try {
|
||||
FileInputStream in = new FileInputStream(file);
|
||||
promptNameAndSave(in, file.getName());
|
||||
} catch (FileNotFoundException e) {
|
||||
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
|
||||
}
|
||||
} else {
|
||||
showErrorDialogAndQuit("Unable to receive any file or URL.");
|
||||
}
|
||||
}
|
||||
|
||||
void showErrorDialogAndQuit(String message) {
|
||||
mFinishOnDismissNameDialog = false;
|
||||
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(dialog -> finish()).setPositiveButton(android.R.string.ok, (dialog, which) -> finish()).show();
|
||||
}
|
||||
|
||||
void handleContentUri(final Uri uri, String subjectFromIntent) {
|
||||
try {
|
||||
String attachmentFileName = null;
|
||||
|
||||
String[] projection = new String[]{OpenableColumns.DISPLAY_NAME};
|
||||
try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) {
|
||||
if (c != null && c.moveToFirst()) {
|
||||
final int fileNameColumnId = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (fileNameColumnId >= 0) attachmentFileName = c.getString(fileNameColumnId);
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentFileName == null) attachmentFileName = subjectFromIntent;
|
||||
|
||||
InputStream in = getContentResolver().openInputStream(uri);
|
||||
promptNameAndSave(in, attachmentFileName);
|
||||
} catch (Exception e) {
|
||||
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||
DialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||
File outFile = saveStreamWithName(in, text);
|
||||
if (outFile == null) return;
|
||||
|
||||
final File editorProgramFile = new File(EDITOR_PROGRAM);
|
||||
if (!editorProgramFile.isFile()) {
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Do this for the user if necessary:
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
editorProgramFile.setExecutable(true);
|
||||
|
||||
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
R.string.action_file_received_open_directory, text -> {
|
||||
if (saveStreamWithName(in, text) == null) return;
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
android.R.string.cancel, text -> finish(), dialog -> {
|
||||
if (mFinishOnDismissNameDialog) finish();
|
||||
});
|
||||
}
|
||||
|
||||
public File saveStreamWithName(InputStream in, String attachmentFileName) {
|
||||
File receiveDir = new File(TERMUX_RECEIVEDIR);
|
||||
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
|
||||
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final File outFile = new File(receiveDir, attachmentFileName);
|
||||
try (FileOutputStream f = new FileOutputStream(outFile)) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int readBytes;
|
||||
while ((readBytes = in.read(buffer)) > 0) {
|
||||
f.write(buffer, 0, readBytes);
|
||||
}
|
||||
}
|
||||
return outFile;
|
||||
} catch (IOException e) {
|
||||
showErrorDialogAndQuit("Error saving file:\n\n" + e);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void handleUrlAndFinish(final String url) {
|
||||
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
|
||||
if (!urlOpenerProgramFile.isFile()) {
|
||||
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
|
||||
+ "Create this file as a script or a symlink - it will be called with the shared URL as only argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Do this for the user if necessary:
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
urlOpenerProgramFile.setExecutable(true);
|
||||
|
||||
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/partial_primary_toolbar"
|
||||
android:id="@+id/partial_primary_toolbar"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,80 +1,115 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/activity_termux_root_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
<RelativeLayout
|
||||
android:id="@+id/activity_termux_root_relative_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_marginVertical="0dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.termux.view.TerminalView
|
||||
android:id="@+id/terminal_view"
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginRight="3dp"
|
||||
android:layout_marginLeft="3dp"
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||
android:scrollbars="vertical"
|
||||
android:importantForAutofill="no"
|
||||
android:autofillHints="password" />
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/left_drawer"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:background="@android:color/white"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@android:color/transparent"
|
||||
android:dividerHeight="0dp"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/terminal_sessions_list"
|
||||
<com.termux.view.TerminalView
|
||||
android:id="@+id/terminal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_weight="1"
|
||||
android:choiceMode="singleChoice"
|
||||
android:longClickable="true" />
|
||||
android:layout_height="match_parent"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||
android:scrollbars="vertical"
|
||||
android:importantForAutofill="no"
|
||||
android:autofillHints="password"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<LinearLayout
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:id="@+id/left_drawer"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@android:color/transparent"
|
||||
android:dividerHeight="0dp"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/termuxActivityDrawerBackground">
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggle_keyboard_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_toggle_soft_keyboard" />
|
||||
android:orientation="horizontal">
|
||||
<ImageButton
|
||||
android:id="@+id/settings_button"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_settings"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/action_open_settings"
|
||||
app:tint="?attr/termuxActivityDrawerImageTint" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/new_session_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
<ListView
|
||||
android:id="@+id/terminal_sessions_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_weight="1"
|
||||
android:choiceMode="singleChoice"
|
||||
android:longClickable="true" />
|
||||
|
||||
<LinearLayout
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_new_session" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/toggle_keyboard_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_toggle_soft_keyboard" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/new_session_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_new_session" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/terminal_toolbar_view_pager"
|
||||
android:visibility="gone"
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/terminal_toolbar_view_pager"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="37.5dp"
|
||||
android:background="@color/black"
|
||||
android:layout_alignParentBottom="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/activity_termux_bottom_space_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="37.5dp"
|
||||
android:background="@android:drawable/screen_background_dark_transparent"
|
||||
android:layout_alignParentBottom="true" />
|
||||
</RelativeLayout>
|
||||
android:layout_height="1dp"
|
||||
android:background="@android:color/transparent" />
|
||||
|
||||
</com.termux.app.terminal.TermuxActivityRootView>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/session_title"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.termux.shared.termux.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/terminal_toolbar_extra_keys"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/black"/>
|
||||
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/black"/>
|
||||
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
||||
40
app/src/main/res/values-night/themes.xml
Normal file
40
app/src/main/res/values-night/themes.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!--
|
||||
https://material.io/develop/android/theming/dark
|
||||
-->
|
||||
|
||||
<!-- TermuxActivity DayNight NoActionBar theme. -->
|
||||
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
||||
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||
<style name="Theme.TermuxActivity.DayNight.NoActionBar" parent="Theme.TermuxApp.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/black</item>
|
||||
<item name="colorPrimaryVariant">@color/black</item>
|
||||
|
||||
<item name="android:windowBackground">@color/black</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>
|
||||
|
||||
<!-- Left drawer. -->
|
||||
<item name="buttonBarButtonStyle">@style/TermuxActivity.Drawer.ButtonBarStyle.Dark</item>
|
||||
<item name="termuxActivityDrawerBackground">@color/black</item>
|
||||
<item name="termuxActivityDrawerImageTint">@color/white</item>
|
||||
|
||||
<!-- Extra keys colors. -->
|
||||
<item name="extraKeysButtonTextColor">@color/white</item>
|
||||
<item name="extraKeysButtonActiveTextColor">@color/red_400</item>
|
||||
<item name="extraKeysButtonBackgroundColor">@color/black</item>
|
||||
<item name="extraKeysButtonActiveBackgroundColor">@color/grey_500</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
5
app/src/main/res/values/attrs.xml
Normal file
5
app/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<attr name="termuxActivityDrawerBackground" format="reference" />
|
||||
<attr name="termuxActivityDrawerImageTint" format="reference" />
|
||||
</resources>
|
||||
@@ -9,10 +9,10 @@
|
||||
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
|
||||
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
|
||||
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
|
||||
<!ENTITY TERMUX_PROPERTIES_PRIMARY_PATH_SHORT "~/.termux/termux.properties">
|
||||
]>
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="application_name">&TERMUX_APP_NAME;</string>
|
||||
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- Termux RUN_COMMAND permission -->
|
||||
<string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string>
|
||||
<string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME;
|
||||
environment</string>
|
||||
environment and access files</string>
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,13 @@
|
||||
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
|
||||
<string name="bootstrap_error_abort">Abort</string>
|
||||
<string name="bootstrap_error_try_again">Try again</string>
|
||||
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than \"%1$s\".</string>
|
||||
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.
|
||||
\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed
|
||||
under any path other than:\n%1$s.</string>
|
||||
<string name="bootstrap_error_installed_on_portable_sd">&TERMUX_APP_NAME; cannot be installed on
|
||||
portable/external/removable sd card on your device.
|
||||
\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed
|
||||
under any path other than:\n%1$s.</string>
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +69,10 @@
|
||||
<string name="title_share_transcript">Terminal transcript</string>
|
||||
<string name="title_share_transcript_with">Send transcript to:</string>
|
||||
|
||||
<string name="action_share_selected_text">Share selected text</string>
|
||||
<string name="title_share_selected_text">Terminal Text</string>
|
||||
<string name="title_share_selected_text_with">Send selected text to:</string>
|
||||
|
||||
<string name="action_autofill_password">Autofill password</string>
|
||||
|
||||
<string name="action_reset_terminal">Reset</string>
|
||||
@@ -78,6 +88,7 @@
|
||||
|
||||
<string name="action_report_issue">Report Issue</string>
|
||||
<string name="msg_generating_report">Generating Report</string>
|
||||
<string name="msg_add_termux_debug_info">Add termux debug info to report?</string>
|
||||
|
||||
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
|
||||
<string name="action_styling_install">Install</string>
|
||||
@@ -91,29 +102,25 @@
|
||||
|
||||
|
||||
|
||||
<!-- TermuxService -->
|
||||
<string name="error_display_over_other_apps_permission_not_granted_to_start_terminal">&TERMUX_APP_NAME; requires
|
||||
\"Display over other apps\" permission to start terminal sessions from background on Android >= 10.
|
||||
Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
||||
<string name="error_termux_service_invalid_execution_command_runner">Invalid execution command runner to TermuxService: `%1$s`</string>
|
||||
<string name="error_termux_service_unsupported_execution_command_runner">Unsupported execution command runner to TermuxService: `%1$s`</string>
|
||||
<string name="error_termux_service_unsupported_execution_command_shell_create_mode">Unsupported execution command shell create mode to TermuxService: `%1$s`</string>
|
||||
<string name="error_termux_service_execution_command_shell_name_unset">Shell name not set but `%1$s` shell create mode passed</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux RunCommandService -->
|
||||
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||
<string name="error_run_command_service_invalid_execution_command_runner">Invalid execution command runner to RunCommandService: `%1$s`</string>
|
||||
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
||||
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
|
||||
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux Execution Commands -->
|
||||
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
|
||||
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux Report And ShareUtils -->
|
||||
<string name="action_copy">Copy</string>
|
||||
<string name="action_share">Share</string>
|
||||
|
||||
<string name="title_share_with">Share With</string>
|
||||
<string name="title_report_text">Report Text</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux File Receiver -->
|
||||
<string name="title_file_received">Save file in ~/downloads/</string>
|
||||
<string name="action_file_received_edit">Edit</string>
|
||||
@@ -121,6 +128,12 @@
|
||||
|
||||
|
||||
|
||||
<!-- Miscellaneous -->
|
||||
<string name="error_termux_service_start_failed_general">Failed to start TermuxService. Check logcat for exception message.</string>
|
||||
<string name="error_termux_service_start_failed_bg">Failed to start TermuxService while app is in background due to android bg restrictions.</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux Settings -->
|
||||
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
|
||||
|
||||
@@ -141,7 +154,8 @@
|
||||
<!-- Terminal View Key Logging -->
|
||||
<string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string>
|
||||
<string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string>
|
||||
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
|
||||
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys.
|
||||
These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
|
||||
|
||||
<!-- Plugin Error Notifications -->
|
||||
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
|
||||
@@ -168,15 +182,52 @@
|
||||
|
||||
<!-- Soft Keyboard Only If No Hardware-->
|
||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string>
|
||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if hardware keyboard is connected. (Default)</string>
|
||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if no hardware keyboard is connected.</string>
|
||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if
|
||||
hardware keyboard is connected. (Default)</string>
|
||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if
|
||||
no hardware keyboard is connected.</string>
|
||||
|
||||
|
||||
<!-- Termux Tasker App Preferences -->
|
||||
<!-- Terminal View Preferences -->
|
||||
<string name="termux_terminal_view_preferences_title">Terminal View</string>
|
||||
<string name="termux_terminal_view_preferences_summary">Preferences for terminal view</string>
|
||||
|
||||
<!-- View Category -->
|
||||
<string name="termux_terminal_view_view_header">View</string>
|
||||
|
||||
<!-- Terminal View Margin Adjustment -->
|
||||
<string name="termux_terminal_view_terminal_margin_adjustment_title">Terminal Margin Adjustment</string>
|
||||
<string name="termux_terminal_view_terminal_margin_adjustment_off">Terminal margin adjustment will be disabled.</string>
|
||||
<string name="termux_terminal_view_terminal_margin_adjustment_on">Terminal margin adjustment will be enabled.
|
||||
It should be enabled to try to fix the issue where soft keyboard covers part of extra keys/terminal view.
|
||||
If it causes screen flickering on your devices, then disable it. (Default)</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux:API App Preferences -->
|
||||
<string name="termux_api_preferences_title">&TERMUX_API_APP_NAME;</string>
|
||||
<string name="termux_api_preferences_summary">Preferences for &TERMUX_API_APP_NAME; app</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux:Float App Preferences -->
|
||||
<string name="termux_float_preferences_title">&TERMUX_FLOAT_APP_NAME;</string>
|
||||
<string name="termux_float_preferences_summary">Preferences for &TERMUX_FLOAT_APP_NAME; app</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux:Tasker App Preferences -->
|
||||
<string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string>
|
||||
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux:Widget App Preferences -->
|
||||
<string name="termux_widget_preferences_title">&TERMUX_WIDGET_APP_NAME;</string>
|
||||
<string name="termux_widget_preferences_summary">Preferences for &TERMUX_WIDGET_APP_NAME; app</string>
|
||||
|
||||
|
||||
|
||||
<!-- About Preference -->
|
||||
<string name="about_preference_title">About</string>
|
||||
|
||||
|
||||
@@ -1,61 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.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">#212121</item>
|
||||
<item name="android:alertDialogTheme">@style/TermuxAlertDialogStyle</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>
|
||||
|
||||
|
||||
<!-- 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>
|
||||
|
||||
|
||||
<style name="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimaryDark">#FF0000</item>
|
||||
</style>
|
||||
|
||||
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||
<!-- Seen in buttons on alert dialog: -->
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
</style>
|
||||
|
||||
<style name="TermuxActivity.Drawer.ButtonBarStyle.Light" parent="@style/Widget.MaterialComponents.Button.TextButton">
|
||||
<item name="android:textColor">@color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="TermuxActivity.Drawer.ButtonBarStyle.Dark" parent="@style/Widget.MaterialComponents.Button.TextButton">
|
||||
<item name="android:textColor">@color/white</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
49
app/src/main/res/values/themes.xml
Normal file
49
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!--
|
||||
https://material.io/develop/android/theming/dark
|
||||
-->
|
||||
|
||||
<!-- TermuxApp Light DarkActionBar theme. -->
|
||||
<style name="Theme.TermuxApp.Light.DarkActionBar" parent="Theme.BaseActivity.Light.DarkActionBar"/>
|
||||
<!-- TermuxApp Light NoActionBar theme. -->
|
||||
<style name="Theme.TermuxApp.Light.NoActionBar" parent="Theme.BaseActivity.Light.NoActionBar"/>
|
||||
|
||||
<!-- TermuxApp DayNight DarkActionBar theme. -->
|
||||
<style name="Theme.TermuxApp.DayNight.DarkActionBar" parent="Theme.BaseActivity.DayNight.DarkActionBar"/>
|
||||
<!-- TermuxApp DayNight NoActionBar theme. -->
|
||||
<style name="Theme.TermuxApp.DayNight.NoActionBar" parent="Theme.BaseActivity.DayNight.NoActionBar"/>
|
||||
|
||||
|
||||
<!-- TermuxActivity DayNight NoActionBar theme. -->
|
||||
<style name="Theme.TermuxActivity.DayNight.NoActionBar" parent="Theme.TermuxApp.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/black</item>
|
||||
<item name="colorPrimaryVariant">@color/black</item>
|
||||
|
||||
<item name="android:windowBackground">@color/black</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>
|
||||
|
||||
<!-- Left drawer. -->
|
||||
<item name="buttonBarButtonStyle">@style/TermuxActivity.Drawer.ButtonBarStyle.Light</item>
|
||||
<item name="termuxActivityDrawerBackground">@color/white</item>
|
||||
<item name="termuxActivityDrawerImageTint">@color/black</item>
|
||||
|
||||
<!-- Extra keys colors. -->
|
||||
<item name="extraKeysButtonTextColor">@color/white</item>
|
||||
<item name="extraKeysButtonActiveTextColor">@color/red_400</item>
|
||||
<item name="extraKeysButtonBackgroundColor">@color/black</item>
|
||||
<item name="extraKeysButtonActiveBackgroundColor">@color/grey_500</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -6,6 +6,20 @@
|
||||
app:summary="@string/termux_preferences_summary"
|
||||
app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/>
|
||||
|
||||
<Preference
|
||||
app:key="termux_api"
|
||||
app:title="@string/termux_api_preferences_title"
|
||||
app:summary="@string/termux_api_preferences_summary"
|
||||
app:isPreferenceVisible="false"
|
||||
app:fragment="com.termux.app.fragments.settings.TermuxAPIPreferencesFragment"/>
|
||||
|
||||
<Preference
|
||||
app:key="termux_float"
|
||||
app:title="@string/termux_float_preferences_title"
|
||||
app:summary="@string/termux_float_preferences_summary"
|
||||
app:isPreferenceVisible="false"
|
||||
app:fragment="com.termux.app.fragments.settings.TermuxFloatPreferencesFragment"/>
|
||||
|
||||
<Preference
|
||||
app:key="termux_tasker"
|
||||
app:title="@string/termux_tasker_preferences_title"
|
||||
@@ -13,6 +27,13 @@
|
||||
app:isPreferenceVisible="false"
|
||||
app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/>
|
||||
|
||||
<Preference
|
||||
app:key="termux_widget"
|
||||
app:title="@string/termux_widget_preferences_title"
|
||||
app:summary="@string/termux_widget_preferences_summary"
|
||||
app:isPreferenceVisible="false"
|
||||
app:fragment="com.termux.app.fragments.settings.TermuxWidgetPreferencesFragment"/>
|
||||
|
||||
<Preference
|
||||
app:key="about"
|
||||
app:title="@string/about_preference_title"
|
||||
|
||||
15
app/src/main/res/xml/termux_api_debugging_preferences.xml
Normal file
15
app/src/main/res/xml/termux_api_debugging_preferences.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="logging"
|
||||
app:title="@string/termux_logging_header">
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="1"
|
||||
app:key="log_level"
|
||||
app:title="@string/termux_log_level_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
8
app/src/main/res/xml/termux_api_preferences.xml
Normal file
8
app/src/main/res/xml/termux_api_preferences.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
app:title="@string/termux_debugging_preferences_title"
|
||||
app:summary="@string/termux_debugging_preferences_summary"
|
||||
app:fragment="com.termux.app.fragments.settings.termux_api.DebuggingPreferencesFragment"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
21
app/src/main/res/xml/termux_float_debugging_preferences.xml
Normal file
21
app/src/main/res/xml/termux_float_debugging_preferences.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="logging"
|
||||
app:title="@string/termux_logging_header">
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="1"
|
||||
app:key="log_level"
|
||||
app:title="@string/termux_log_level_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="terminal_view_key_logging_enabled"
|
||||
app:summaryOff="@string/termux_terminal_view_key_logging_enabled_off"
|
||||
app:summaryOn="@string/termux_terminal_view_key_logging_enabled_on"
|
||||
app:title="@string/termux_terminal_view_key_logging_enabled_title" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
8
app/src/main/res/xml/termux_float_preferences.xml
Normal file
8
app/src/main/res/xml/termux_float_preferences.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
app:title="@string/termux_debugging_preferences_title"
|
||||
app:summary="@string/termux_debugging_preferences_summary"
|
||||
app:fragment="com.termux.app.fragments.settings.termux_float.DebuggingPreferencesFragment"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -10,4 +10,9 @@
|
||||
app:summary="@string/termux_terminal_io_preferences_summary"
|
||||
app:fragment="com.termux.app.fragments.settings.termux.TerminalIOPreferencesFragment"/>
|
||||
|
||||
<Preference
|
||||
app:title="@string/termux_terminal_view_preferences_title"
|
||||
app:summary="@string/termux_terminal_view_preferences_summary"
|
||||
app:fragment="com.termux.app.fragments.settings.termux.TerminalViewPreferencesFragment"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
15
app/src/main/res/xml/termux_terminal_view_preferences.xml
Normal file
15
app/src/main/res/xml/termux_terminal_view_preferences.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="view"
|
||||
app:title="@string/termux_terminal_view_view_header">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:key="terminal_margin_adjustment"
|
||||
app:summaryOff="@string/termux_terminal_view_terminal_margin_adjustment_off"
|
||||
app:summaryOn="@string/termux_terminal_view_terminal_margin_adjustment_on"
|
||||
app:title="@string/termux_terminal_view_terminal_margin_adjustment_title" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
15
app/src/main/res/xml/termux_widget_debugging_preferences.xml
Normal file
15
app/src/main/res/xml/termux_widget_debugging_preferences.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="logging"
|
||||
app:title="@string/termux_logging_header">
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="1"
|
||||
app:key="log_level"
|
||||
app:title="@string/termux_log_level_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
8
app/src/main/res/xml/termux_widget_preferences.xml
Normal file
8
app/src/main/res/xml/termux_widget_preferences.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
app:title="@string/termux_debugging_preferences_title"
|
||||
app:summary="@string/termux_debugging_preferences_summary"
|
||||
app:fragment="com.termux.app.fragments.settings.termux_widget.DebuggingPreferencesFragment"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.termux.app;
|
||||
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.termux.data.TermuxUrlUtils;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -13,7 +13,7 @@ public class TermuxActivityTest {
|
||||
private void assertUrlsAre(String text, String... urls) {
|
||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||
Collections.addAll(expected, urls);
|
||||
Assert.assertEquals(expected, DataUtils.extractUrls(text));
|
||||
Assert.assertEquals(expected, TermuxUrlUtils.extractUrls(text));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package com.termux.filepicker;
|
||||
package com.termux.app.api.file;
|
||||
|
||||
import com.termux.app.api.file.FileReceiverActivity;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -9,7 +11,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class TermuxFileReceiverActivityTest {
|
||||
public class FileReceiverActivityTest {
|
||||
|
||||
@Test
|
||||
public void testIsSharedTextAnUrl() {
|
||||
@@ -19,13 +21,13 @@ public class TermuxFileReceiverActivityTest {
|
||||
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));
|
||||
Assert.assertTrue(FileReceiverActivity.isSharedTextAnUrl(url));
|
||||
}
|
||||
|
||||
List<String> invalidUrls = new ArrayList<>();
|
||||
invalidUrls.add("a test with example.com");
|
||||
for (String url : invalidUrls) {
|
||||
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
||||
Assert.assertFalse(FileReceiverActivity.isSharedTextAnUrl(url));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
//classpath "com.android.tools.build:gradle:4.2.2"
|
||||
classpath "com.android.tools.build:gradle:7.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +13,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
docs/en/index.md
Normal file
13
docs/en/index.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
page_ref: /docs/apps/termux/index.html
|
||||
---
|
||||
|
||||
# Termux App Docs
|
||||
|
||||
<!--- DOC_HEADER_PLACEHOLDER -->
|
||||
|
||||
Welcome to documentation for the [Termux App].
|
||||
|
||||
##
|
||||
|
||||
[Termux App]: https://github.com/termux/termux-app
|
||||
@@ -15,13 +15,13 @@
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
android.useAndroidX=true
|
||||
|
||||
termuxVersion=0.111
|
||||
termuxVersionCode=111
|
||||
|
||||
minSdkVersion=24
|
||||
minSdkVersion=21
|
||||
targetSdkVersion=28
|
||||
ndkVersion=22.1.7171670
|
||||
|
||||
ndkVersion=24.0.8215888
|
||||
compileSdkVersion=30
|
||||
|
||||
markwonVersion=4.6.2
|
||||
|
||||
# 保持与旧版Gradle插件的兼容
|
||||
android.disableAutomaticComponentCreation=true
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
2
jitpack.yml
Normal file
2
jitpack.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
env:
|
||||
JITPACK_NDK_VERSION: "21.1.6352462"
|
||||
@@ -1,9 +1,10 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
apply from: 'localandwinboll-maven-publish.gradle'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
ndkVersion project.properties.ndkVersion
|
||||
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
@@ -50,7 +51,8 @@ tasks.withType(Test) {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
@@ -58,26 +60,17 @@ task sourceJar(type: Jar) {
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
bar(MavenPublication) {
|
||||
groupId 'com.termux'
|
||||
artifactId 'terminal-emulator'
|
||||
version "0.113"
|
||||
artifact(sourceJar)
|
||||
artifact("$buildDir/outputs/aar/terminal-emulator-release.aar")
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/termux/termux-app")
|
||||
|
||||
credentials {
|
||||
username = System.getenv("GH_USERNAME")
|
||||
password = System.getenv("GH_TOKEN")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//afterEvaluate {
|
||||
// publishing {
|
||||
// publications {
|
||||
// // Creates a Maven publication called "release".
|
||||
// release(MavenPublication) {
|
||||
// from components.release
|
||||
// groupId = 'com.termux'
|
||||
// artifactId = 'terminal-emulator'
|
||||
// version = '0.118.0'
|
||||
// artifact(sourceJar)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
206
terminal-emulator/localandwinboll-maven-publish.gradle
Normal file
206
terminal-emulator/localandwinboll-maven-publish.gradle
Normal file
@@ -0,0 +1,206 @@
|
||||
// 本机和 WinBoll Maven 仓库传输配置。
|
||||
//
|
||||
|
||||
def siteUrl = 'https://winboll.cc/studio/termux-app' // 项目主页
|
||||
def gitUrl = 'https://gitea.winboll.cc/Studio/termux-app.git' // 项目的git地址
|
||||
def DefaultGroupId = 'com.termux' // 类库GroupId
|
||||
def DefaultArtifactId = 'terminal-emulator' // 类库ArtifactId
|
||||
def DefaultVersion = '0.118.0' // 版本号
|
||||
def DeveloperId='zhangsken' // 开发者账号
|
||||
def DeveloperName='ZhanGSKen' // 开发者名称
|
||||
def DeveloperEMail='ZhanGSKen@QQ.COM' // 开发者邮箱地址
|
||||
def LicenseName='The Apache Software License, Version 2.0'
|
||||
def LicenseUrl='http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
def WinBollMavneConfigFile='winboll.properties'
|
||||
|
||||
Properties properties = new Properties()
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
repositories {
|
||||
if(project.rootProject.file(WinBollMavneConfigFile).exists()) {
|
||||
properties.load(project.rootProject.file(WinBollMavneConfigFile).newDataInputStream())
|
||||
def NexusUserName = properties.getProperty("Nexus.name")
|
||||
def NexusPassword = properties.getProperty("Nexus.password")
|
||||
// WinBoll Release 仓库
|
||||
maven{
|
||||
//仓库的名字和地址
|
||||
name = "WinBollRelease"
|
||||
url="https://nexus.winboll.cc/repository/maven-releases/"
|
||||
// 仓库用户名密码
|
||||
credentials {
|
||||
username = NexusUserName
|
||||
password = NexusPassword
|
||||
}
|
||||
}
|
||||
// WinBoll Snapshot 仓库
|
||||
maven{
|
||||
//仓库的名字和地址
|
||||
name = "WinBollSnapshot"
|
||||
url="https://nexus.winboll.cc/repository/maven-snapshots/"
|
||||
// 仓库用户名密码
|
||||
credentials {
|
||||
username = NexusUserName
|
||||
password = NexusPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
publications {
|
||||
// Local Maven 仓库传输任务
|
||||
//
|
||||
release(MavenPublication) {
|
||||
groupId = DefaultGroupId
|
||||
artifactId = DefaultArtifactId
|
||||
version = DefaultVersion
|
||||
|
||||
//from components.java
|
||||
// 必须有这个 否则不会上传AAR包
|
||||
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }
|
||||
// 上传source,这样使用方可以看到方法注释
|
||||
//artifact generateSourcesJar
|
||||
//要上传的aar路径
|
||||
//artifact "$buildDir/outputs/aar/${project.getName()}-release.aar"
|
||||
//artifact "$buildDir/outputs/aar/${project.getName()}-debug.aar"
|
||||
|
||||
//对pom进行的操作
|
||||
pom.withXml{
|
||||
Node pomNode = asNode()
|
||||
pomNode.dependencies.'*'.findAll() {
|
||||
//将所有的默认依赖移除
|
||||
//it.parent().remove(it)
|
||||
}
|
||||
}
|
||||
pom {
|
||||
name = artifactId
|
||||
url = siteUrl
|
||||
licenses {
|
||||
license { //证书说明
|
||||
name=LicenseName // 开源协议名称
|
||||
url=LicenseUrl // 协议地址
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id=DeveloperId // 开发者账号
|
||||
name=DeveloperName // 开发者名称
|
||||
email=DeveloperEMail // 开发者邮箱地址
|
||||
}
|
||||
}
|
||||
//软件配置管理
|
||||
scm {
|
||||
connection=gitUrl
|
||||
developerConnection=gitUrl
|
||||
url=siteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WinBoll Maven Release 仓库传输任务
|
||||
//
|
||||
releaseWinBoll(MavenPublication) {
|
||||
// 需要使用的变体,假设有free和pay两个变体,可以选择一个
|
||||
//from components.free
|
||||
|
||||
groupId = DefaultGroupId // 文件的groupId
|
||||
artifactId = DefaultArtifactId //文件的名字
|
||||
version = DefaultVersion //版本号
|
||||
|
||||
//from components.java
|
||||
// 必须有这个 否则不会上传AAR包
|
||||
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }
|
||||
// 上传source,这样使用方可以看到方法注释
|
||||
//artifact generateSourcesJar
|
||||
//要上传的aar路径
|
||||
//artifact "$buildDir/outputs/aar/${project.getName()}-release.aar"
|
||||
//artifact "$buildDir/outputs/aar/${project.getName()}-debug.aar"
|
||||
|
||||
//对pom进行的操作
|
||||
pom.withXml{
|
||||
Node pomNode = asNode()
|
||||
pomNode.dependencies.'*'.findAll() {
|
||||
//将所有的默认依赖移除
|
||||
//it.parent().remove(it)
|
||||
}
|
||||
}
|
||||
pom {
|
||||
name = artifactId
|
||||
url = siteUrl
|
||||
licenses {
|
||||
license { //证书说明
|
||||
name=LicenseName // 开源协议名称
|
||||
url=LicenseUrl // 协议地址
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id=DeveloperId // 开发者账号
|
||||
name=DeveloperName // 开发者名称
|
||||
email=DeveloperEMail // 开发者邮箱地址
|
||||
}
|
||||
}
|
||||
//软件配置管理
|
||||
scm {
|
||||
connection=gitUrl
|
||||
developerConnection=gitUrl
|
||||
url=siteUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // 创建名为 release 的任务结束
|
||||
|
||||
// WinBoll Maven Snapshot 仓库传输任务
|
||||
//
|
||||
snapshotWinBoll(MavenPublication) {
|
||||
// 需要使用的变体,假设有free和pay两个变体,可以选择一个
|
||||
//from components.free
|
||||
|
||||
groupId = DefaultGroupId // 文件的groupId
|
||||
artifactId = DefaultArtifactId //文件的名字
|
||||
version = DefaultVersion + "-SNAPSHOT" //版本号
|
||||
|
||||
//from components.java
|
||||
// 必须有这个 否则不会上传AAR包
|
||||
afterEvaluate { artifact(tasks.getByName("bundleReleaseAar")) }
|
||||
// 上传source,这样使用方可以看到方法注释
|
||||
//artifact generateSourcesJar
|
||||
//要上传的aar路径
|
||||
//artifact "$buildDir/outputs/aar/${project.getName()}-release.aar"
|
||||
//artifact "$buildDir/outputs/aar/${project.getName()}-debug.aar"
|
||||
|
||||
//对pom进行的操作
|
||||
pom.withXml{
|
||||
Node pomNode = asNode()
|
||||
pomNode.dependencies.'*'.findAll() {
|
||||
//将所有的默认依赖移除
|
||||
//it.parent().remove(it)
|
||||
}
|
||||
}
|
||||
pom {
|
||||
name = artifactId
|
||||
url = siteUrl
|
||||
licenses {
|
||||
license { //证书说明
|
||||
name=LicenseName // 开源协议名称
|
||||
url=LicenseUrl // 协议地址
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id=DeveloperId // 开发者账号
|
||||
name=DeveloperName // 开发者名称
|
||||
email=DeveloperEMail // 开发者邮箱地址
|
||||
}
|
||||
}
|
||||
//软件配置管理
|
||||
scm {
|
||||
connection=gitUrl
|
||||
developerConnection=gitUrl
|
||||
url=siteUrl
|
||||
}
|
||||
}
|
||||
} // 创建名为 snapshot 的任务结束
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,9 +227,9 @@ public final class KeyHandler {
|
||||
return transformForModifiers("\033[3", keyMode, '~');
|
||||
|
||||
case KEYCODE_PAGE_UP:
|
||||
return "\033[5~";
|
||||
return transformForModifiers("\033[5", keyMode, '~');
|
||||
case KEYCODE_PAGE_DOWN:
|
||||
return "\033[6~";
|
||||
return transformForModifiers("\033[6", keyMode, '~');
|
||||
case KEYCODE_DEL:
|
||||
String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
|
||||
// Just do what xterm and gnome-terminal does:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
public class Logger {
|
||||
|
||||
public static void logError(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logError(logTag, message);
|
||||
else
|
||||
Log.e(logTag, message);
|
||||
}
|
||||
|
||||
public static void logWarn(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logWarn(logTag, message);
|
||||
else
|
||||
Log.w(logTag, message);
|
||||
}
|
||||
|
||||
public static void logInfo(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logInfo(logTag, message);
|
||||
else
|
||||
Log.i(logTag, message);
|
||||
}
|
||||
|
||||
public static void logDebug(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logDebug(logTag, message);
|
||||
else
|
||||
Log.d(logTag, message);
|
||||
}
|
||||
|
||||
public static void logVerbose(TerminalSessionClient client, String logTag, String message) {
|
||||
if (client != null)
|
||||
client.logVerbose(logTag, message);
|
||||
else
|
||||
Log.v(logTag, message);
|
||||
}
|
||||
|
||||
public static void logStackTraceWithMessage(TerminalSessionClient client, String tag, String message, Throwable throwable) {
|
||||
logError(client, tag, getMessageAndStackTraceString(message, throwable));
|
||||
}
|
||||
|
||||
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
|
||||
if (message == null && throwable == null)
|
||||
return null;
|
||||
else if (message != null && throwable != null)
|
||||
return message + ":\n" + getStackTraceString(throwable);
|
||||
else if (throwable == null)
|
||||
return message;
|
||||
else
|
||||
return getStackTraceString(throwable);
|
||||
}
|
||||
|
||||
public static String getStackTraceString(Throwable throwable) {
|
||||
if (throwable == null) return null;
|
||||
|
||||
String stackTraceString = null;
|
||||
|
||||
try {
|
||||
StringWriter errors = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(errors);
|
||||
throwable.printStackTrace(pw);
|
||||
pw.close();
|
||||
stackTraceString = errors.toString();
|
||||
errors.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return stackTraceString;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public final class TerminalBuffer {
|
||||
}
|
||||
|
||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
|
||||
return getSelectedText(selX1, selY1, selX2, selY2, true, false);
|
||||
return getSelectedText(selX1, selY1, selX2, selY2, joinBackLines, false);
|
||||
}
|
||||
|
||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) {
|
||||
@@ -93,8 +93,11 @@ public final class TerminalBuffer {
|
||||
if (c != ' ') lastPrintingCharIndex = i;
|
||||
}
|
||||
}
|
||||
if (lastPrintingCharIndex != -1)
|
||||
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||
|
||||
int len = lastPrintingCharIndex - x1Index + 1;
|
||||
if (lastPrintingCharIndex != -1 && len > 0)
|
||||
builder.append(line, x1Index, len);
|
||||
|
||||
boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1;
|
||||
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||
@@ -102,6 +105,45 @@ public final class TerminalBuffer {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public String getWordAtLocation(int x, int y) {
|
||||
// Set y1 and y2 to the lines where the wrapped line starts and ends.
|
||||
// I.e. if a line that is wrapped to 3 lines starts at line 4, and this
|
||||
// is called with y=5, then y1 would be set to 4 and y2 would be set to 6.
|
||||
int y1 = y;
|
||||
int y2 = y;
|
||||
while (y1 > 0 && !getSelectedText(0, y1 - 1, mColumns, y, true, true).contains("\n")) {
|
||||
y1--;
|
||||
}
|
||||
while (y2 < mScreenRows && !getSelectedText(0, y, mColumns, y2 + 1, true, true).contains("\n")) {
|
||||
y2++;
|
||||
}
|
||||
|
||||
// Get the text for the whole wrapped line
|
||||
String text = getSelectedText(0, y1, mColumns, y2, true, true);
|
||||
// The index of x in text
|
||||
int textOffset = (y - y1) * mColumns + x;
|
||||
|
||||
if (textOffset >= text.length()) {
|
||||
// The click was to the right of the last word on the line, so
|
||||
// there's no word to return
|
||||
return "";
|
||||
}
|
||||
|
||||
// Set x1 and x2 to the indices of the last space before x and the
|
||||
// first space after x in text respectively
|
||||
int x1 = text.lastIndexOf(' ', textOffset);
|
||||
int x2 = text.indexOf(' ', textOffset);
|
||||
if (x2 == -1) {
|
||||
x2 = text.length();
|
||||
}
|
||||
|
||||
if (x1 == x2) {
|
||||
// The click was on a space, so there's no word to return
|
||||
return "";
|
||||
}
|
||||
return text.substring(x1 + 1, x2);
|
||||
}
|
||||
|
||||
public int getActiveTranscriptRows() {
|
||||
return mActiveTranscriptRows;
|
||||
}
|
||||
@@ -407,8 +449,8 @@ public final class TerminalBuffer {
|
||||
}
|
||||
|
||||
public void setChar(int column, int row, int codePoint, long style) {
|
||||
if (row >= mScreenRows || column >= mColumns)
|
||||
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
||||
if (row < 0 || row >= mScreenRows || column < 0 || column >= mColumns)
|
||||
throw new IllegalArgumentException("TerminalBuffer.setChar(): row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
||||
row = externalToInternalRow(row);
|
||||
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public final class TerminalColorScheme {
|
||||
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
|
||||
|
||||
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
|
||||
0xffffffff, 0xff000000, 0xffA9AAA9};
|
||||
0xffffffff, 0xff000000, 0xffffffff};
|
||||
|
||||
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
|
||||
|
||||
@@ -71,6 +71,7 @@ public final class TerminalColorScheme {
|
||||
|
||||
public void updateWith(Properties props) {
|
||||
reset();
|
||||
boolean cursorPropExists = false;
|
||||
for (Map.Entry<Object, Object> entries : props.entrySet()) {
|
||||
String key = (String) entries.getKey();
|
||||
String value = (String) entries.getValue();
|
||||
@@ -82,6 +83,7 @@ public final class TerminalColorScheme {
|
||||
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
|
||||
} else if (key.equals("cursor")) {
|
||||
colorIndex = TextStyle.COLOR_INDEX_CURSOR;
|
||||
cursorPropExists = true;
|
||||
} else if (key.startsWith("color")) {
|
||||
try {
|
||||
colorIndex = Integer.parseInt(key.substring(5));
|
||||
@@ -98,6 +100,27 @@ public final class TerminalColorScheme {
|
||||
|
||||
mDefaultColors[colorIndex] = colorValue;
|
||||
}
|
||||
|
||||
if (!cursorPropExists)
|
||||
setCursorColorForBackground();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the "cursor" color is not set by user, we need to decide on the appropriate color that will
|
||||
* be visible on the current terminal background. White will not be visible on light backgrounds
|
||||
* and black won't be visible on dark backgrounds. So we find the perceived brightness of the
|
||||
* background color and if its below the threshold (too dark), we use white cursor and if its
|
||||
* above (too bright), we use black cursor.
|
||||
*/
|
||||
public void setCursorColorForBackground() {
|
||||
int backgroundColor = mDefaultColors[TextStyle.COLOR_INDEX_BACKGROUND];
|
||||
int brightness = TerminalColors.getPerceivedBrightnessOfColor(backgroundColor);
|
||||
if (brightness > 0) {
|
||||
if (brightness < 130)
|
||||
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = 0xffffffff;
|
||||
else
|
||||
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = 0xff000000;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
/** Current terminal colors (if different from default). */
|
||||
public final class TerminalColors {
|
||||
|
||||
@@ -73,4 +75,22 @@ public final class TerminalColors {
|
||||
if (c != 0) mCurrentColors[intoIndex] = c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the perceived brightness of the color based on its RGB components.
|
||||
*
|
||||
* https://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
|
||||
* http://alienryderflex.com/hsp.html
|
||||
*
|
||||
* @param color The color code int.
|
||||
* @return Returns value between 0-255.
|
||||
*/
|
||||
public static int getPerceivedBrightnessOfColor(int color) {
|
||||
return (int)
|
||||
Math.floor(Math.sqrt(
|
||||
Math.pow(Color.red(color), 2) * 0.241 +
|
||||
Math.pow(Color.green(color), 2) * 0.691 +
|
||||
Math.pow(Color.blue(color), 2) * 0.068
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,10 +38,6 @@ public final class TerminalEmulator {
|
||||
public static final int MOUSE_WHEELUP_BUTTON = 64;
|
||||
public static final int MOUSE_WHEELDOWN_BUTTON = 65;
|
||||
|
||||
public static final int CURSOR_STYLE_BLOCK = 0;
|
||||
public static final int CURSOR_STYLE_UNDERLINE = 1;
|
||||
public static final int CURSOR_STYLE_BAR = 2;
|
||||
|
||||
/** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */
|
||||
public static final int UNICODE_REPLACEMENT_CHAR = 0xFFFD;
|
||||
|
||||
@@ -126,17 +122,39 @@ public final class TerminalEmulator {
|
||||
/** Not really DECSET bit... - http://www.vt100.net/docs/vt510-rm/DECSACE */
|
||||
private static final int DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE = 1 << 12;
|
||||
|
||||
|
||||
private String mTitle;
|
||||
private final Stack<String> mTitleStack = new Stack<>();
|
||||
|
||||
/** If processing first character of first parameter of {@link #ESC_CSI}. */
|
||||
private boolean mIsCSIStart;
|
||||
/** The last character processed of a parameter of {@link #ESC_CSI}. */
|
||||
private Integer mLastCSIArg;
|
||||
|
||||
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
|
||||
private int mCursorRow, mCursorCol;
|
||||
|
||||
private int mCursorStyle = CURSOR_STYLE_BLOCK;
|
||||
|
||||
/** The number of character rows and columns in the terminal screen. */
|
||||
public int mRows, mColumns;
|
||||
|
||||
/** The number of terminal transcript rows that can be scrolled back to. */
|
||||
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
|
||||
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
|
||||
public static final int DEFAULT_TERMINAL_TRANSCRIPT_ROWS = 2000;
|
||||
|
||||
|
||||
/* The supported terminal cursor styles. */
|
||||
|
||||
public static final int TERMINAL_CURSOR_STYLE_BLOCK = 0;
|
||||
public static final int TERMINAL_CURSOR_STYLE_UNDERLINE = 1;
|
||||
public static final int TERMINAL_CURSOR_STYLE_BAR = 2;
|
||||
public static final int DEFAULT_TERMINAL_CURSOR_STYLE = TERMINAL_CURSOR_STYLE_BLOCK;
|
||||
public static final Integer[] TERMINAL_CURSOR_STYLES_LIST = new Integer[]{TERMINAL_CURSOR_STYLE_BLOCK, TERMINAL_CURSOR_STYLE_UNDERLINE, TERMINAL_CURSOR_STYLE_BAR};
|
||||
|
||||
/** The terminal cursor styles. */
|
||||
private int mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
|
||||
|
||||
|
||||
/** The normal screen buffer. Stores the characters that appear on the screen of the emulated terminal. */
|
||||
private final TerminalBuffer mMainBuffer;
|
||||
/**
|
||||
@@ -234,6 +252,9 @@ public final class TerminalEmulator {
|
||||
*/
|
||||
private int mScrollCounter = 0;
|
||||
|
||||
/** If automatic scrolling of terminal is disabled */
|
||||
private boolean mAutoScrollDisabled;
|
||||
|
||||
private byte mUtf8ToFollow, mUtf8Index;
|
||||
private final byte[] mUtf8InputBuffer = new byte[4];
|
||||
private int mLastEmittedCodePoint = -1;
|
||||
@@ -294,9 +315,9 @@ public final class TerminalEmulator {
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows, TerminalSessionClient client) {
|
||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
|
||||
mSession = session;
|
||||
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
|
||||
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
|
||||
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
||||
mClient = client;
|
||||
mRows = rows;
|
||||
@@ -307,6 +328,8 @@ public final class TerminalEmulator {
|
||||
|
||||
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||
mClient = client;
|
||||
setCursorStyle();
|
||||
setCursorBlinkState(true);
|
||||
}
|
||||
|
||||
public TerminalBuffer getScreen() {
|
||||
@@ -317,6 +340,13 @@ public final class TerminalEmulator {
|
||||
return mScreen == mAltBuffer;
|
||||
}
|
||||
|
||||
private int getTerminalTranscriptRows(Integer transcriptRows) {
|
||||
if (transcriptRows == null || transcriptRows < TERMINAL_TRANSCRIPT_ROWS_MIN || transcriptRows > TERMINAL_TRANSCRIPT_ROWS_MAX)
|
||||
return DEFAULT_TERMINAL_TRANSCRIPT_ROWS;
|
||||
else
|
||||
return transcriptRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mouseButton one of the MOUSE_* constants of this class.
|
||||
*/
|
||||
@@ -384,11 +414,24 @@ public final class TerminalEmulator {
|
||||
return mCursorCol;
|
||||
}
|
||||
|
||||
/** {@link #CURSOR_STYLE_BAR}, {@link #CURSOR_STYLE_BLOCK} or {@link #CURSOR_STYLE_UNDERLINE} */
|
||||
/** Get the terminal cursor style. It will be one of {@link #TERMINAL_CURSOR_STYLES_LIST} */
|
||||
public int getCursorStyle() {
|
||||
return mCursorStyle;
|
||||
}
|
||||
|
||||
/** Set the terminal cursor style. */
|
||||
public void setCursorStyle() {
|
||||
Integer cursorStyle = null;
|
||||
|
||||
if (mClient != null)
|
||||
cursorStyle = mClient.getTerminalCursorStyle();
|
||||
|
||||
if (cursorStyle == null || !Arrays.asList(TERMINAL_CURSOR_STYLES_LIST).contains(cursorStyle))
|
||||
mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
|
||||
else
|
||||
mCursorStyle = cursorStyle;
|
||||
}
|
||||
|
||||
public boolean isReverseVideo() {
|
||||
return isDecsetInternalBitSet(DECSET_BIT_REVERSE_VIDEO);
|
||||
}
|
||||
@@ -760,7 +803,6 @@ public final class TerminalEmulator {
|
||||
int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor);
|
||||
int columnsToMove = columnsAfterCursor - columnsToDelete;
|
||||
mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0);
|
||||
blockClear(mCursorRow + columnsToMove, 0, columnsToDelete, mRows);
|
||||
} else {
|
||||
unknownSequence(b);
|
||||
}
|
||||
@@ -789,7 +831,7 @@ public final class TerminalEmulator {
|
||||
if (internalBit != -1) {
|
||||
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
||||
} else {
|
||||
mClient.logError(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||
Logger.logError(mClient, LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
|
||||
}
|
||||
}
|
||||
@@ -806,15 +848,15 @@ public final class TerminalEmulator {
|
||||
case 0: // Blinking block.
|
||||
case 1: // Blinking block.
|
||||
case 2: // Steady block.
|
||||
mCursorStyle = CURSOR_STYLE_BLOCK;
|
||||
mCursorStyle = TERMINAL_CURSOR_STYLE_BLOCK;
|
||||
break;
|
||||
case 3: // Blinking underline.
|
||||
case 4: // Steady underline.
|
||||
mCursorStyle = CURSOR_STYLE_UNDERLINE;
|
||||
mCursorStyle = TERMINAL_CURSOR_STYLE_UNDERLINE;
|
||||
break;
|
||||
case 5: // Blinking bar (xterm addition).
|
||||
case 6: // Steady bar (xterm addition).
|
||||
mCursorStyle = CURSOR_STYLE_BAR;
|
||||
mCursorStyle = TERMINAL_CURSOR_STYLE_BAR;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -900,10 +942,17 @@ public final class TerminalEmulator {
|
||||
for (String part : dcs.substring(2).split(";")) {
|
||||
if (part.length() % 2 == 0) {
|
||||
StringBuilder transBuffer = new StringBuilder();
|
||||
char c;
|
||||
for (int i = 0; i < part.length(); i += 2) {
|
||||
char c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue();
|
||||
try {
|
||||
c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue();
|
||||
} catch (NumberFormatException e) {
|
||||
Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Invalid device termcap/terminfo encoded name \"" + part + "\"", e);
|
||||
continue;
|
||||
}
|
||||
transBuffer.append(c);
|
||||
}
|
||||
|
||||
String trans = transBuffer.toString();
|
||||
String responseValue;
|
||||
switch (trans) {
|
||||
@@ -926,7 +975,7 @@ public final class TerminalEmulator {
|
||||
case "&8": // Undo key - ignore.
|
||||
break;
|
||||
default:
|
||||
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||
Logger.logWarn(mClient, LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||
}
|
||||
// Respond with invalid request:
|
||||
mSession.write("\033P0+r" + part + "\033\\");
|
||||
@@ -938,12 +987,12 @@ public final class TerminalEmulator {
|
||||
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
||||
}
|
||||
} else {
|
||||
mClient.logError(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
}
|
||||
finishSequence();
|
||||
}
|
||||
@@ -1033,7 +1082,7 @@ public final class TerminalEmulator {
|
||||
int externalBit = mArgs[i];
|
||||
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
||||
if (internalBit == -1) {
|
||||
mClient.logWarn(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||
Logger.logWarn(mClient, LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||
} else {
|
||||
if (b == 's') {
|
||||
mSavedDecSetFlags |= internalBit;
|
||||
@@ -1223,7 +1272,7 @@ public final class TerminalEmulator {
|
||||
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
|
||||
// some special control character cases, e.g., Control-Space to make a NUL.
|
||||
// (2) enables this feature for keys including the exceptions listed.
|
||||
mClient.logError(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||
Logger.logError(mClient, LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||
break;
|
||||
default:
|
||||
parseArg(b);
|
||||
@@ -1344,6 +1393,8 @@ public final class TerminalEmulator {
|
||||
break;
|
||||
case '[':
|
||||
continueSequence(ESC_CSI);
|
||||
mIsCSIStart = true;
|
||||
mLastCSIArg = null;
|
||||
break;
|
||||
case '=': // DECKPAM
|
||||
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true);
|
||||
@@ -1770,7 +1821,7 @@ public final class TerminalEmulator {
|
||||
int firstArg = mArgs[i + 1];
|
||||
if (firstArg == 2) {
|
||||
if (i + 4 > mArgIndex) {
|
||||
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||
Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||
} else {
|
||||
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
||||
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
||||
@@ -1795,7 +1846,7 @@ public final class TerminalEmulator {
|
||||
mBackColor = color;
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, "Invalid color index: " + color);
|
||||
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, "Invalid color index: " + color);
|
||||
}
|
||||
} else {
|
||||
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
||||
@@ -1812,7 +1863,7 @@ public final class TerminalEmulator {
|
||||
mBackColor = code - 100 + 8;
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
mClient.logWarn(LOG_TAG, String.format("SGR unknown code %d", code));
|
||||
Logger.logWarn(mClient, LOG_TAG, String.format("SGR unknown code %d", code));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1944,9 +1995,9 @@ public final class TerminalEmulator {
|
||||
int startIndex = textParameter.indexOf(";") + 1;
|
||||
try {
|
||||
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
||||
mSession.clipboardText(clipboardText);
|
||||
mSession.onCopyTextToClipboard(clipboardText);
|
||||
} catch (Exception e) {
|
||||
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||
Logger.logError(mClient, LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||
}
|
||||
break;
|
||||
case 104:
|
||||
@@ -2051,28 +2102,57 @@ public final class TerminalEmulator {
|
||||
}
|
||||
}
|
||||
|
||||
/** Process the next ASCII character of a parameter. */
|
||||
private void parseArg(int b) {
|
||||
if (b >= '0' && b <= '9') {
|
||||
if (mArgIndex < mArgs.length) {
|
||||
int oldValue = mArgs[mArgIndex];
|
||||
int thisDigit = b - '0';
|
||||
int value;
|
||||
if (oldValue >= 0) {
|
||||
value = oldValue * 10 + thisDigit;
|
||||
} else {
|
||||
value = thisDigit;
|
||||
/**
|
||||
* Process the next ASCII character of a parameter.
|
||||
*
|
||||
* Parameter characters modify the action or interpretation of the sequence. You can use up to
|
||||
* 16 parameters per sequence. You must use the ; character to separate parameters.
|
||||
* All parameters are unsigned, positive decimal integers, with the most significant
|
||||
* digit sent first. Any parameter greater than 9999 (decimal) is set to 9999
|
||||
* (decimal). If you do not specify a value, a 0 value is assumed. A 0 value
|
||||
* or omitted parameter indicates a default value for the sequence. For most
|
||||
* sequences, the default value is 1.
|
||||
*
|
||||
* https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.3
|
||||
* */
|
||||
private void parseArg(int inputByte) {
|
||||
int[] bytes = new int[]{inputByte};
|
||||
// Only doing this for ESC_CSI and not for other ESC_CSI_* since they seem to be using their
|
||||
// own defaults with getArg*() calls, but there may be missed cases
|
||||
if (mEscapeState == ESC_CSI) {
|
||||
if ((mIsCSIStart && inputByte == ';') || // If sequence starts with a ; character, like \033[;m
|
||||
(!mIsCSIStart && mLastCSIArg != null && mLastCSIArg == ';' && inputByte == ';')) { // If sequence contains sequential ; characters, like \033[;;m
|
||||
bytes = new int[]{'0', ';'}; // Assume 0 was passed
|
||||
}
|
||||
}
|
||||
|
||||
mIsCSIStart = false;
|
||||
|
||||
for (int b : bytes) {
|
||||
if (b >= '0' && b <= '9') {
|
||||
if (mArgIndex < mArgs.length) {
|
||||
int oldValue = mArgs[mArgIndex];
|
||||
int thisDigit = b - '0';
|
||||
int value;
|
||||
if (oldValue >= 0) {
|
||||
value = oldValue * 10 + thisDigit;
|
||||
} else {
|
||||
value = thisDigit;
|
||||
}
|
||||
if (value > 9999)
|
||||
value = 9999;
|
||||
mArgs[mArgIndex] = value;
|
||||
}
|
||||
mArgs[mArgIndex] = value;
|
||||
continueSequence(mEscapeState);
|
||||
} else if (b == ';') {
|
||||
if (mArgIndex < mArgs.length) {
|
||||
mArgIndex++;
|
||||
}
|
||||
continueSequence(mEscapeState);
|
||||
} else {
|
||||
unknownSequence(b);
|
||||
}
|
||||
continueSequence(mEscapeState);
|
||||
} else if (b == ';') {
|
||||
if (mArgIndex < mArgs.length) {
|
||||
mArgIndex++;
|
||||
}
|
||||
continueSequence(mEscapeState);
|
||||
} else {
|
||||
unknownSequence(b);
|
||||
mLastCSIArg = b;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2142,7 +2222,7 @@ public final class TerminalEmulator {
|
||||
}
|
||||
|
||||
private void finishSequenceAndLogError(String error) {
|
||||
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, error);
|
||||
if (LOG_ESCAPE_SEQUENCES) Logger.logWarn(mClient, LOG_TAG, error);
|
||||
finishSequence();
|
||||
}
|
||||
|
||||
@@ -2290,7 +2370,14 @@ public final class TerminalEmulator {
|
||||
}
|
||||
|
||||
int offsetDueToCombiningChar = ((displayWidth <= 0 && mCursorCol > 0 && !mAboutToAutoWrap) ? 1 : 0);
|
||||
mScreen.setChar(mCursorCol - offsetDueToCombiningChar, mCursorRow, codePoint, getStyle());
|
||||
int column = mCursorCol - offsetDueToCombiningChar;
|
||||
|
||||
// Fix TerminalRow.setChar() ArrayIndexOutOfBoundsException index=-1 exception reported
|
||||
// The offsetDueToCombiningChar would never be 1 if mCursorCol was 0 to get column/index=-1,
|
||||
// so was mCursorCol changed after the offsetDueToCombiningChar conditional by another thread?
|
||||
// TODO: Check if there are thread synchronization issues with mCursorCol and mCursorRow, possibly causing others bugs too.
|
||||
if (column < 0) column = 0;
|
||||
mScreen.setChar(column, mCursorRow, codePoint, getStyle());
|
||||
|
||||
if (autoWrap && displayWidth > 0)
|
||||
mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth);
|
||||
@@ -2328,9 +2415,18 @@ public final class TerminalEmulator {
|
||||
mScrollCounter = 0;
|
||||
}
|
||||
|
||||
public boolean isAutoScrollDisabled() {
|
||||
return mAutoScrollDisabled;
|
||||
}
|
||||
|
||||
public void toggleAutoScrollDisabled() {
|
||||
mAutoScrollDisabled = !mAutoScrollDisabled;
|
||||
}
|
||||
|
||||
|
||||
/** Reset terminal state so user can interact with it regardless of present state. */
|
||||
public void reset() {
|
||||
mCursorStyle = CURSOR_STYLE_BLOCK;
|
||||
setCursorStyle();
|
||||
mArgIndex = 0;
|
||||
mContinueSequence = false;
|
||||
mEscapeState = ESC_NONE;
|
||||
|
||||
@@ -18,8 +18,11 @@ public abstract class TerminalOutput {
|
||||
/** Notify the terminal client that the terminal title has changed. */
|
||||
public abstract void titleChanged(String oldTitle, String newTitle);
|
||||
|
||||
/** Notify the terminal client that the terminal title has changed. */
|
||||
public abstract void clipboardText(String text);
|
||||
/** Notify the terminal client that text should be copied to clipboard. */
|
||||
public abstract void onCopyTextToClipboard(String text);
|
||||
|
||||
/** Notify the terminal client that text should be pasted from clipboard. */
|
||||
public abstract void onPasteTextFromClipboard();
|
||||
|
||||
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
|
||||
public abstract void onBell();
|
||||
|
||||
@@ -11,11 +11,37 @@ public final class TerminalRow {
|
||||
|
||||
private static final float SPARE_CAPACITY_FACTOR = 1.5f;
|
||||
|
||||
/**
|
||||
* Max combining characters that can exist in a column, that are separate from the base character
|
||||
* itself. Any additional combining characters will be ignored and not added to the column.
|
||||
*
|
||||
* There does not seem to be limit in unicode standard for max number of combination characters
|
||||
* that can be combined but such characters are primarily under 10.
|
||||
*
|
||||
* "Section 3.6 Combination" of unicode standard contains combining characters info.
|
||||
* - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
|
||||
* - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
|
||||
* - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
|
||||
*
|
||||
* UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
|
||||
* > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
|
||||
* > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
|
||||
* > yet is well within the buffer size limits of practical implementations.
|
||||
* - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
|
||||
* - https://stackoverflow.com/a/11983435/14686958
|
||||
*
|
||||
* We choose the value 15 because it should be enough for terminal based applications and keep
|
||||
* the memory usage low for a terminal row, won't affect performance or cause terminal to
|
||||
* lag or hang, and will keep malicious applications from causing harm. The value can be
|
||||
* increased if ever needed for legitimate applications.
|
||||
*/
|
||||
private static final int MAX_COMBINING_CHARACTERS_PER_COLUMN = 15;
|
||||
|
||||
/** The number of columns in this terminal row. */
|
||||
private final int mColumns;
|
||||
/** The text filling this terminal row. */
|
||||
public char[] mText;
|
||||
/** The number of java char:s used in {@link #mText}. */
|
||||
/** The number of java chars used in {@link #mText}. */
|
||||
private short mSpaceUsed;
|
||||
/** If this row has been line wrapped due to text output at the end of line. */
|
||||
boolean mLineWrap;
|
||||
@@ -124,6 +150,9 @@ public final class TerminalRow {
|
||||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
public void setChar(int columnToSet, int codePoint, long style) {
|
||||
if (columnToSet < 0 || columnToSet >= mStyle.length)
|
||||
throw new IllegalArgumentException("TerminalRow.setChar(): columnToSet=" + columnToSet + ", codePoint=" + codePoint + ", style=" + style);
|
||||
|
||||
mStyle[columnToSet] = style;
|
||||
|
||||
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
|
||||
@@ -160,18 +189,25 @@ public final class TerminalRow {
|
||||
// Get the number of elements in the mText array this column uses now
|
||||
int oldCharactersUsedForColumn;
|
||||
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
|
||||
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
|
||||
int oldEndOfColumnIndex = findStartOfColumn(columnToSet + oldCodePointDisplayWidth);
|
||||
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex;
|
||||
} else {
|
||||
// Last character.
|
||||
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
|
||||
}
|
||||
|
||||
// If MAX_COMBINING_CHARACTERS_PER_COLUMN already exist in column, then ignore adding additional combining characters.
|
||||
if (newIsCombining) {
|
||||
int combiningCharsCount = WcWidth.zeroWidthCharsCount(mText, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn);
|
||||
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
|
||||
return;
|
||||
}
|
||||
|
||||
// Find how many chars this column will need
|
||||
int newCharactersUsedForColumn = Character.charCount(codePoint);
|
||||
if (newIsCombining) {
|
||||
// Combining characters are added to the contents of the column instead of overwriting them, so that they
|
||||
// modify the existing contents.
|
||||
// FIXME: Put a limit of combining characters.
|
||||
// FIXME: Unassigned characters also get width=0.
|
||||
newCharactersUsedForColumn += oldCharactersUsedForColumn;
|
||||
}
|
||||
@@ -186,7 +222,7 @@ public final class TerminalRow {
|
||||
if (mSpaceUsed + javaCharDifference > text.length) {
|
||||
// We need to grow the array
|
||||
char[] newText = new char[text.length + mColumns];
|
||||
System.arraycopy(text, 0, newText, 0, oldStartOfColumnIndex + oldCharactersUsedForColumn);
|
||||
System.arraycopy(text, 0, newText, 0, oldNextColumnIndex);
|
||||
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
|
||||
mText = text = newText;
|
||||
} else {
|
||||
|
||||
@@ -74,14 +74,17 @@ public final class TerminalSession extends TerminalOutput {
|
||||
private final String mCwd;
|
||||
private final String[] mArgs;
|
||||
private final String[] mEnv;
|
||||
private final Integer mTranscriptRows;
|
||||
|
||||
|
||||
private static final String LOG_TAG = "TerminalSession";
|
||||
|
||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, TerminalSessionClient client) {
|
||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, Integer transcriptRows, TerminalSessionClient client) {
|
||||
this.mShellPath = shellPath;
|
||||
this.mCwd = cwd;
|
||||
this.mArgs = args;
|
||||
this.mEnv = env;
|
||||
this.mTranscriptRows = transcriptRows;
|
||||
this.mClient = client;
|
||||
}
|
||||
|
||||
@@ -118,11 +121,12 @@ public final class TerminalSession extends TerminalOutput {
|
||||
* @param rows The number of rows in the terminal window.
|
||||
*/
|
||||
public void initializeEmulator(int columns, int rows) {
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000, mClient);
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, mTranscriptRows, mClient);
|
||||
|
||||
int[] processId = new int[1];
|
||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||
mShellPid = processId[0];
|
||||
mClient.setTerminalShellPid(this, mShellPid);
|
||||
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
||||
|
||||
@@ -233,7 +237,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
try {
|
||||
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||
} catch (ErrnoException e) {
|
||||
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||
Logger.logWarn(mClient, LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,8 +270,13 @@ public final class TerminalSession extends TerminalOutput {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clipboardText(String text) {
|
||||
mClient.onClipboardText(this, text);
|
||||
public void onCopyTextToClipboard(String text) {
|
||||
mClient.onCopyTextToClipboard(this, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasteTextFromClipboard() {
|
||||
mClient.onPasteTextFromClipboard(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -300,7 +309,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
return outputPath;
|
||||
}
|
||||
} catch (IOException | SecurityException e) {
|
||||
mClient.logStackTraceWithMessage(LOG_TAG, "Error getting current directory", e);
|
||||
Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Error getting current directory", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -318,7 +327,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
descriptorField.setAccessible(true);
|
||||
descriptorField.set(result, fileDescriptor);
|
||||
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
||||
client.logStackTraceWithMessage(LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
Logger.logStackTraceWithMessage(client, LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
System.exit(1);
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The interface for communication between {@link TerminalSession} and its client. It is used to
|
||||
* send callbacks to the client when {@link TerminalSession} changes or for sending other
|
||||
@@ -7,20 +10,29 @@ package com.termux.terminal;
|
||||
*/
|
||||
public interface TerminalSessionClient {
|
||||
|
||||
void onTextChanged(TerminalSession changedSession);
|
||||
void onTextChanged(@NonNull TerminalSession changedSession);
|
||||
|
||||
void onTitleChanged(TerminalSession changedSession);
|
||||
void onTitleChanged(@NonNull TerminalSession changedSession);
|
||||
|
||||
void onSessionFinished(TerminalSession finishedSession);
|
||||
void onSessionFinished(@NonNull TerminalSession finishedSession);
|
||||
|
||||
void onClipboardText(TerminalSession session, String text);
|
||||
void onCopyTextToClipboard(@NonNull TerminalSession session, String text);
|
||||
|
||||
void onBell(TerminalSession session);
|
||||
void onPasteTextFromClipboard(@Nullable TerminalSession session);
|
||||
|
||||
void onColorsChanged(TerminalSession session);
|
||||
void onBell(@NonNull TerminalSession session);
|
||||
|
||||
void onColorsChanged(@NonNull TerminalSession session);
|
||||
|
||||
void onTerminalCursorStateChange(boolean state);
|
||||
|
||||
void setTerminalShellPid(@NonNull TerminalSession session, int pid);
|
||||
|
||||
|
||||
|
||||
Integer getTerminalCursorStyle();
|
||||
|
||||
|
||||
|
||||
void logError(String tag, String message);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* Implementation of wcwidth(3) for Unicode 9.
|
||||
* Implementation of wcwidth(3) for Unicode 15.
|
||||
*
|
||||
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
|
||||
*
|
||||
@@ -9,12 +9,13 @@ package com.termux.terminal;
|
||||
* Must be kept in sync with the following:
|
||||
* https://github.com/termux/wcwidth
|
||||
* https://github.com/termux/libandroid-support
|
||||
* https://github.com/termux/termux-packages/tree/master/libandroid-support
|
||||
* https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
|
||||
*/
|
||||
public final class WcWidth {
|
||||
|
||||
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
||||
// at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14):
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private static final int[][] ZERO_WIDTH = {
|
||||
{0x00300, 0x0036f}, // Combining Grave Accent ..Combining Latin Small Le
|
||||
{0x00483, 0x00489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
||||
@@ -40,7 +41,8 @@ public final class WcWidth {
|
||||
{0x00825, 0x00827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||
{0x00829, 0x0082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||
{0x00859, 0x0085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
|
||||
{0x008d3, 0x008e1}, // Arabic Small Low Waw ..Arabic Small High Sign S
|
||||
{0x00898, 0x0089f}, // Arabic Small High Word A..Arabic Half Madda Over M
|
||||
{0x008ca, 0x008e1}, // Arabic Small High Farsi ..Arabic Small High Sign S
|
||||
{0x008e3, 0x00902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||
{0x0093a, 0x0093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||
{0x0093c, 0x0093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
||||
@@ -74,13 +76,14 @@ public final class WcWidth {
|
||||
{0x00b3f, 0x00b3f}, // Oriya Vowel Sign I ..Oriya Vowel Sign I
|
||||
{0x00b41, 0x00b44}, // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
|
||||
{0x00b4d, 0x00b4d}, // Oriya Sign Virama ..Oriya Sign Virama
|
||||
{0x00b55, 0x00b56}, // (nil) ..Oriya Ai Length Mark
|
||||
{0x00b55, 0x00b56}, // Oriya Sign Overline ..Oriya Ai Length Mark
|
||||
{0x00b62, 0x00b63}, // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
|
||||
{0x00b82, 0x00b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||
{0x00bc0, 0x00bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||
{0x00bcd, 0x00bcd}, // Tamil Sign Virama ..Tamil Sign Virama
|
||||
{0x00c00, 0x00c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||
{0x00c04, 0x00c04}, // Telugu Sign Combining An..Telugu Sign Combining An
|
||||
{0x00c3c, 0x00c3c}, // Telugu Sign Nukta ..Telugu Sign Nukta
|
||||
{0x00c3e, 0x00c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||
{0x00c46, 0x00c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||
{0x00c4a, 0x00c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
|
||||
@@ -97,7 +100,7 @@ public final class WcWidth {
|
||||
{0x00d41, 0x00d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||
{0x00d4d, 0x00d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||
{0x00d62, 0x00d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
||||
{0x00d81, 0x00d81}, // (nil) ..(nil)
|
||||
{0x00d81, 0x00d81}, // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
|
||||
{0x00dca, 0x00dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||
{0x00dd2, 0x00dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||
{0x00dd6, 0x00dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
||||
@@ -106,7 +109,7 @@ public final class WcWidth {
|
||||
{0x00e47, 0x00e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||
{0x00eb1, 0x00eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||
{0x00eb4, 0x00ebc}, // Lao Vowel Sign I ..Lao Semivowel Sign Lo
|
||||
{0x00ec8, 0x00ecd}, // Lao Tone Mai Ek ..Lao Niggahita
|
||||
{0x00ec8, 0x00ece}, // Lao Tone Mai Ek ..(nil)
|
||||
{0x00f18, 0x00f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||
{0x00f35, 0x00f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
{0x00f37, 0x00f37}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
@@ -130,7 +133,7 @@ public final class WcWidth {
|
||||
{0x0109d, 0x0109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||
{0x0135d, 0x0135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||
{0x01712, 0x01714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
||||
{0x01732, 0x01734}, // Hanunoo Vowel Sign I ..Hanunoo Sign Pamudpod
|
||||
{0x01732, 0x01733}, // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
|
||||
{0x01752, 0x01753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||
{0x01772, 0x01773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||
{0x017b4, 0x017b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
||||
@@ -139,6 +142,7 @@ public final class WcWidth {
|
||||
{0x017c9, 0x017d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||
{0x017dd, 0x017dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||
{0x0180b, 0x0180d}, // Mongolian Free Variation..Mongolian Free Variation
|
||||
{0x0180f, 0x0180f}, // Mongolian Free Variation..Mongolian Free Variation
|
||||
{0x01885, 0x01886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
{0x018a9, 0x018a9}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
{0x01920, 0x01922}, // Limbu Vowel Sign A ..Limbu Vowel Sign U
|
||||
@@ -154,7 +158,7 @@ public final class WcWidth {
|
||||
{0x01a65, 0x01a6c}, // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
|
||||
{0x01a73, 0x01a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||
{0x01a7f, 0x01a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
||||
{0x01ab0, 0x01ac0}, // Combining Doubled Circum..(nil)
|
||||
{0x01ab0, 0x01ace}, // Combining Doubled Circum..Combining Latin Small Le
|
||||
{0x01b00, 0x01b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||
{0x01b34, 0x01b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||
{0x01b36, 0x01b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
||||
@@ -177,8 +181,7 @@ public final class WcWidth {
|
||||
{0x01ced, 0x01ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||
{0x01cf4, 0x01cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||
{0x01cf8, 0x01cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||
{0x01dc0, 0x01df9}, // Combining Dotted Grave A..Combining Wide Inverted
|
||||
{0x01dfb, 0x01dff}, // Combining Deletion Mark ..Combining Right Arrowhea
|
||||
{0x01dc0, 0x01dff}, // Combining Dotted Grave A..Combining Right Arrowhea
|
||||
{0x020d0, 0x020f0}, // Combining Left Harpoon A..Combining Asterisk Above
|
||||
{0x02cef, 0x02cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||
{0x02d7f, 0x02d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
||||
@@ -193,7 +196,7 @@ public final class WcWidth {
|
||||
{0x0a806, 0x0a806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||
{0x0a80b, 0x0a80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||
{0x0a825, 0x0a826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
||||
{0x0a82c, 0x0a82c}, // (nil) ..(nil)
|
||||
{0x0a82c, 0x0a82c}, // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
|
||||
{0x0a8c4, 0x0a8c5}, // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
|
||||
{0x0a8e0, 0x0a8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
|
||||
{0x0a8ff, 0x0a8ff}, // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
|
||||
@@ -233,13 +236,18 @@ public final class WcWidth {
|
||||
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
|
||||
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||
{0x10d24, 0x10d27}, // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
|
||||
{0x10eab, 0x10eac}, // (nil) ..(nil)
|
||||
{0x10eab, 0x10eac}, // Yezidi Combining Hamza M..Yezidi Combining Madda M
|
||||
{0x10efd, 0x10eff}, // (nil) ..(nil)
|
||||
{0x10f46, 0x10f50}, // Sogdian Combining Dot Be..Sogdian Combining Stroke
|
||||
{0x10f82, 0x10f85}, // Old Uyghur Combining Dot..Old Uyghur Combining Two
|
||||
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
|
||||
{0x11070, 0x11070}, // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
|
||||
{0x11073, 0x11074}, // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
|
||||
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
|
||||
{0x110c2, 0x110c2}, // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
|
||||
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||
@@ -247,11 +255,12 @@ public final class WcWidth {
|
||||
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||
{0x111c9, 0x111cc}, // Sharada Sandhi Mark ..Sharada Extra Short Vowe
|
||||
{0x111cf, 0x111cf}, // (nil) ..(nil)
|
||||
{0x111cf, 0x111cf}, // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
|
||||
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||
{0x1123e, 0x1123e}, // Khojki Sign Sukun ..Khojki Sign Sukun
|
||||
{0x11241, 0x11241}, // (nil) ..(nil)
|
||||
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
|
||||
@@ -283,9 +292,9 @@ public final class WcWidth {
|
||||
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||
{0x1182f, 0x11837}, // Dogra Vowel Sign U ..Dogra Sign Anusvara
|
||||
{0x11839, 0x1183a}, // Dogra Sign Virama ..Dogra Sign Nukta
|
||||
{0x1193b, 0x1193c}, // (nil) ..(nil)
|
||||
{0x1193e, 0x1193e}, // (nil) ..(nil)
|
||||
{0x11943, 0x11943}, // (nil) ..(nil)
|
||||
{0x1193b, 0x1193c}, // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
|
||||
{0x1193e, 0x1193e}, // Dives Akuru Virama ..Dives Akuru Virama
|
||||
{0x11943, 0x11943}, // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
|
||||
{0x119d4, 0x119d7}, // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
|
||||
{0x119da, 0x119db}, // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
|
||||
{0x119e0, 0x119e0}, // Nandinagari Sign Virama ..Nandinagari Sign Virama
|
||||
@@ -313,12 +322,20 @@ public final class WcWidth {
|
||||
{0x11d95, 0x11d95}, // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
|
||||
{0x11d97, 0x11d97}, // Gunjala Gondi Virama ..Gunjala Gondi Virama
|
||||
{0x11ef3, 0x11ef4}, // Makasar Vowel Sign I ..Makasar Vowel Sign U
|
||||
{0x11f00, 0x11f01}, // (nil) ..(nil)
|
||||
{0x11f36, 0x11f3a}, // (nil) ..(nil)
|
||||
{0x11f40, 0x11f40}, // (nil) ..(nil)
|
||||
{0x11f42, 0x11f42}, // (nil) ..(nil)
|
||||
{0x13440, 0x13440}, // (nil) ..(nil)
|
||||
{0x13447, 0x13455}, // (nil) ..(nil)
|
||||
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
|
||||
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||
{0x16f4f, 0x16f4f}, // Miao Sign Consonant Modi..Miao Sign Consonant Modi
|
||||
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
|
||||
{0x16fe4, 0x16fe4}, // (nil) ..(nil)
|
||||
{0x16fe4, 0x16fe4}, // Khitan Small Script Fill..Khitan Small Script Fill
|
||||
{0x1bc9d, 0x1bc9e}, // Duployan Thick Letter Se..Duployan Double Mark
|
||||
{0x1cf00, 0x1cf2d}, // Znamenny Combining Mark ..Znamenny Combining Mark
|
||||
{0x1cf30, 0x1cf46}, // Znamenny Combining Tonal..Znamenny Priznak Modifie
|
||||
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
{0x1d185, 0x1d18b}, // Musical Symbol Combining..Musical Symbol Combining
|
||||
@@ -335,15 +352,19 @@ public final class WcWidth {
|
||||
{0x1e01b, 0x1e021}, // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
{0x1e023, 0x1e024}, // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
{0x1e026, 0x1e02a}, // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
{0x1e08f, 0x1e08f}, // (nil) ..(nil)
|
||||
{0x1e130, 0x1e136}, // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
|
||||
{0x1e2ae, 0x1e2ae}, // Toto Sign Rising Tone ..Toto Sign Rising Tone
|
||||
{0x1e2ec, 0x1e2ef}, // Wancho Tone Tup ..Wancho Tone Koini
|
||||
{0x1e4ec, 0x1e4ef}, // (nil) ..(nil)
|
||||
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||
{0x1e944, 0x1e94a}, // Adlam Alif Lengthener ..Adlam Nukta
|
||||
{0xe0100, 0xe01ef}, // Variation Selector-17 ..Variation Selector-256
|
||||
};
|
||||
|
||||
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
||||
// at commit b29897e5a1b403a0e36f7fc991614981cbc42475 (2020-07-14):
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private static final int[][] WIDE_EASTASIAN = {
|
||||
{0x01100, 0x0115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||
{0x0231a, 0x0231b}, // Watch ..Hourglass
|
||||
@@ -392,7 +413,7 @@ public final class WcWidth {
|
||||
{0x03190, 0x031e3}, // Ideographic Annotation L..Cjk Stroke Q
|
||||
{0x031f0, 0x0321e}, // Katakana Letter Small Ku..Parenthesized Korean Cha
|
||||
{0x03220, 0x03247}, // Parenthesized Ideograph ..Circled Ideograph Koto
|
||||
{0x03250, 0x04dbf}, // Partnership Sign ..(nil)
|
||||
{0x03250, 0x04dbf}, // Partnership Sign ..Cjk Unified Ideograph-4d
|
||||
{0x04e00, 0x0a48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||
{0x0a490, 0x0a4c6}, // Yi Radical Qot ..Yi Radical Ke
|
||||
{0x0a960, 0x0a97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
||||
@@ -404,13 +425,18 @@ public final class WcWidth {
|
||||
{0x0fe68, 0x0fe6b}, // Small Reverse Solidus ..Small Commercial At
|
||||
{0x0ff01, 0x0ff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||
{0x0ffe0, 0x0ffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..(nil)
|
||||
{0x16ff0, 0x16ff1}, // (nil) ..(nil)
|
||||
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..Khitan Small Script Fill
|
||||
{0x16ff0, 0x16ff1}, // Vietnamese Alternate Rea..Vietnamese Alternate Rea
|
||||
{0x17000, 0x187f7}, // (nil) ..(nil)
|
||||
{0x18800, 0x18cd5}, // Tangut Component-001 ..(nil)
|
||||
{0x18800, 0x18cd5}, // Tangut Component-001 ..Khitan Small Script Char
|
||||
{0x18d00, 0x18d08}, // (nil) ..(nil)
|
||||
{0x1b000, 0x1b11e}, // Katakana Letter Archaic ..Hentaigana Letter N-mu-m
|
||||
{0x1aff0, 0x1aff3}, // Katakana Letter Minnan T..Katakana Letter Minnan T
|
||||
{0x1aff5, 0x1affb}, // Katakana Letter Minnan T..Katakana Letter Minnan N
|
||||
{0x1affd, 0x1affe}, // Katakana Letter Minnan N..Katakana Letter Minnan N
|
||||
{0x1b000, 0x1b122}, // Katakana Letter Archaic ..Katakana Letter Archaic
|
||||
{0x1b132, 0x1b132}, // (nil) ..(nil)
|
||||
{0x1b150, 0x1b152}, // Hiragana Letter Small Wi..Hiragana Letter Small Wo
|
||||
{0x1b155, 0x1b155}, // (nil) ..(nil)
|
||||
{0x1b164, 0x1b167}, // Katakana Letter Small Wi..Katakana Letter Small N
|
||||
{0x1b170, 0x1b2fb}, // Nushu Character-1b170 ..Nushu Character-1b2fb
|
||||
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||
@@ -443,24 +469,24 @@ public final class WcWidth {
|
||||
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
|
||||
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
|
||||
{0x1f6d0, 0x1f6d2}, // Place Of Worship ..Shopping Trolley
|
||||
{0x1f6d5, 0x1f6d7}, // Hindu Temple ..(nil)
|
||||
{0x1f6d5, 0x1f6d7}, // Hindu Temple ..Elevator
|
||||
{0x1f6dc, 0x1f6df}, // (nil) ..Ring Buoy
|
||||
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
|
||||
{0x1f6f4, 0x1f6fc}, // Scooter ..(nil)
|
||||
{0x1f6f4, 0x1f6fc}, // Scooter ..Roller Skate
|
||||
{0x1f7e0, 0x1f7eb}, // Large Orange Circle ..Large Brown Square
|
||||
{0x1f90c, 0x1f93a}, // (nil) ..Fencer
|
||||
{0x1f7f0, 0x1f7f0}, // Heavy Equals Sign ..Heavy Equals Sign
|
||||
{0x1f90c, 0x1f93a}, // Pinched Fingers ..Fencer
|
||||
{0x1f93c, 0x1f945}, // Wrestlers ..Goal Net
|
||||
{0x1f947, 0x1f978}, // First Place Medal ..(nil)
|
||||
{0x1f97a, 0x1f9cb}, // Face With Pleading Eyes ..(nil)
|
||||
{0x1f9cd, 0x1f9ff}, // Standing Person ..Nazar Amulet
|
||||
{0x1fa70, 0x1fa74}, // Ballet Shoes ..(nil)
|
||||
{0x1fa78, 0x1fa7a}, // Drop Of Blood ..Stethoscope
|
||||
{0x1fa80, 0x1fa86}, // Yo-yo ..(nil)
|
||||
{0x1fa90, 0x1faa8}, // Ringed Planet ..(nil)
|
||||
{0x1fab0, 0x1fab6}, // (nil) ..(nil)
|
||||
{0x1fac0, 0x1fac2}, // (nil) ..(nil)
|
||||
{0x1fad0, 0x1fad6}, // (nil) ..(nil)
|
||||
{0x1f947, 0x1f9ff}, // First Place Medal ..Nazar Amulet
|
||||
{0x1fa70, 0x1fa7c}, // Ballet Shoes ..Crutch
|
||||
{0x1fa80, 0x1fa88}, // Yo-yo ..(nil)
|
||||
{0x1fa90, 0x1fabd}, // Ringed Planet ..(nil)
|
||||
{0x1fabf, 0x1fac5}, // (nil) ..Person With Crown
|
||||
{0x1face, 0x1fadb}, // (nil) ..(nil)
|
||||
{0x1fae0, 0x1fae8}, // Melting Face ..(nil)
|
||||
{0x1faf0, 0x1faf8}, // Hand With Index Finger A..(nil)
|
||||
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..(nil)
|
||||
{0x30000, 0x3fffd}, // (nil) ..(nil)
|
||||
{0x30000, 0x3fffd}, // Cjk Unified Ideograph-30..(nil)
|
||||
};
|
||||
|
||||
|
||||
@@ -512,4 +538,29 @@ public final class WcWidth {
|
||||
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* The zero width characters count like combining characters in the `chars` array from start
|
||||
* index to end index (exclusive).
|
||||
*/
|
||||
public static int zeroWidthCharsCount(char[] chars, int start, int end) {
|
||||
if (start < 0 || start >= chars.length)
|
||||
return 0;
|
||||
|
||||
int count = 0;
|
||||
for (int i = start; i < end && i < chars.length;) {
|
||||
if (Character.isHighSurrogate(chars[i])) {
|
||||
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
|
||||
count++;
|
||||
}
|
||||
i += 2;
|
||||
} else {
|
||||
if (width(chars[i]) <= 0) {
|
||||
count++;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,4 +45,21 @@ public class ScreenBufferTest extends TerminalTestCase {
|
||||
withTerminalSized(5, 3).enterString("ABC\r\nFG");
|
||||
assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
|
||||
}
|
||||
|
||||
public void testGetWordAtLocation() {
|
||||
withTerminalSized(5, 3).enterString("ABCDEFGHIJ\r\nKLMNO");
|
||||
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(0, 0));
|
||||
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 1));
|
||||
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 2));
|
||||
|
||||
withTerminalSized(5, 3).enterString("ABC DEF GHI ");
|
||||
assertEquals("ABC", mTerminal.getScreen().getWordAtLocation(0, 0));
|
||||
assertEquals("", mTerminal.getScreen().getWordAtLocation(3, 0));
|
||||
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(4, 0));
|
||||
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(0, 1));
|
||||
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(1, 1));
|
||||
assertEquals("GHI", mTerminal.getScreen().getWordAtLocation(0, 2));
|
||||
assertEquals("", mTerminal.getScreen().getWordAtLocation(1, 2));
|
||||
assertEquals("", mTerminal.getScreen().getWordAtLocation(2, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,23 +103,23 @@ public class TerminalTest extends TerminalTestCase {
|
||||
/** Test the cursor shape changes using DECSCUSR. */
|
||||
public void testSetCursorStyle() throws Exception {
|
||||
withTerminalSized(5, 5);
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
enterString("\033[3 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
enterString("\033[5 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
||||
enterString("\033[0 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
enterString("\033[6 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
||||
enterString("\033[4 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
enterString("\033[1 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
enterString("\033[4 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||
enterString("\033[2 q");
|
||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||
}
|
||||
|
||||
public void testPaste() {
|
||||
@@ -151,6 +151,19 @@ public class TerminalTest extends TerminalTestCase {
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
||||
|
||||
// Test CSI resetting to default if sequence starts with ; or has sequential ;;
|
||||
// Check TerminalEmulator.parseArg()
|
||||
enterString("\033[31m\033[m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31m\033[;m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31m\033[0m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31m\033[0;m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
enterString("\033[31;;m");
|
||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||
|
||||
// 256 colors:
|
||||
enterString("\033[38;5;119m");
|
||||
assertEquals(119, mTerminal.mForeColor);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user