Compare commits
411 Commits
v0.116
...
v0.119.0-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c5992d379 | ||
|
|
2cfbfcd79f | ||
|
|
afe22941ce | ||
|
|
e85d078f04 | ||
|
|
8cdeb55271 | ||
|
|
3ae0d601db | ||
|
|
3f6ebd33cd | ||
|
|
f12697a0f8 | ||
|
|
b466e9c88d | ||
|
|
bf33a54fe9 | ||
|
|
57e4ef456b | ||
|
|
d90be9cd50 | ||
|
|
062c9771a9 | ||
|
|
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 |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
|||||||
patreon: termux
|
custom: https://termux.dev/donate
|
||||||
custom: https://paypal.me/fornwall
|
|
||||||
|
|||||||
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"
|
||||||
83
.github/workflows/attach_debug_apks_to_release.yml
vendored
Normal file
83
.github/workflows/attach_debug_apks_to_release.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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 ! gh release upload "$RELEASE_VERSION_NAME" \
|
||||||
|
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
|
||||||
|
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
|
||||||
|
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
|
||||||
|
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
|
||||||
|
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
|
||||||
|
"$APK_DIR_PATH/${APK_BASENAME_PREFIX}_sha256sums" \
|
||||||
|
; then
|
||||||
|
exit_on_error "Attach APKs to release failed for '$APK_VERSION_TAG' release."
|
||||||
|
fi
|
||||||
119
.github/workflows/debug_build.yml
vendored
119
.github/workflows/debug_build.yml
vendored
@@ -4,23 +4,124 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- android-10
|
- 'github-releases/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- android-10
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
package_variant: [ apt-android-7, apt-android-5 ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Build
|
|
||||||
|
- name: Build APKs
|
||||||
|
shell: bash {0}
|
||||||
|
env:
|
||||||
|
PACKAGE_VARIANT: ${{ matrix.package_variant }}
|
||||||
run: |
|
run: |
|
||||||
./gradlew assembleDebug
|
exit_on_error() { echo "$1"; exit 1; }
|
||||||
- name: Store generated APK file
|
|
||||||
uses: actions/upload-artifact@v2
|
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:
|
with:
|
||||||
name: termux-app
|
name: ${{ env.APK_BASENAME_PREFIX }}_universal
|
||||||
path: ./app/build/outputs/apk/debug
|
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"
|
name: "Validation"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v3
|
||||||
|
|||||||
2
.github/workflows/run_tests.yml
vendored
2
.github/workflows/run_tests.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Execute tests
|
- name: Execute tests
|
||||||
run: |
|
run: |
|
||||||
./gradlew test
|
./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"
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -4,15 +4,13 @@
|
|||||||
|
|
||||||
# Built application files
|
# Built application files
|
||||||
build/
|
build/
|
||||||
|
release/
|
||||||
*.apk
|
*.apk
|
||||||
*.so
|
*.so
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
*.zip
|
*.zip
|
||||||
|
|
||||||
# Crashlytics configuations
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
# Local configuration file (sdk path, etc)
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
@@ -26,6 +24,10 @@ local.properties
|
|||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
|
# Vim
|
||||||
|
*.swo
|
||||||
|
*.swp
|
||||||
|
|
||||||
# OS-specific files
|
# OS-specific files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.DS_Store?
|
.DS_Store?
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ The `termux/termux-app` repository is released under [GPLv3 only](https://www.gn
|
|||||||
|
|
||||||
### Exceptions
|
### Exceptions
|
||||||
|
|
||||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
- [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.
|
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
|
||||||
|
|||||||
193
README.md
193
README.md
@@ -3,6 +3,7 @@
|
|||||||
[](https://github.com/termux/termux-app/actions)
|
[](https://github.com/termux/termux-app/actions)
|
||||||
[](https://github.com/termux/termux-app/actions)
|
[](https://github.com/termux/termux-app/actions)
|
||||||
[](https://gitter.im/termux/termux)
|
[](https://gitter.im/termux/termux)
|
||||||
|
[](https://discord.gg/HXpF69X)
|
||||||
[](https://jitpack.io/#termux/termux-app)
|
[](https://jitpack.io/#termux/termux-app)
|
||||||
|
|
||||||
|
|
||||||
@@ -12,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.
|
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.
|
||||||
|
|
||||||
***
|
**We are looking for Termux Android application maintainers.**
|
||||||
|
|
||||||
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.**
|
|
||||||
|
|
||||||
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
|
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### Contents
|
**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.
|
||||||
- [Termux App and Plugins](#Termux-App-and-Plugins)
|
|
||||||
- [Installation](#Installation)
|
***
|
||||||
- [Uninstallation](#Uninstallation)
|
|
||||||
- [Important Links](#Important-Links)
|
## Contents
|
||||||
- [For Devs and Contributors](#For-Devs-and-Contributors)
|
- [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)
|
||||||
##
|
##
|
||||||
|
|
||||||
|
|
||||||
@@ -46,38 +49,88 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
|
|||||||
|
|
||||||
## Installation
|
## 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). See [below](#google-play-store-experimental-branch) for information regarding Termux on Google Play.**
|
||||||
|
|
||||||
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
|
### 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.
|
||||||
|
|
||||||
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.
|
### 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 **(Experimental branch)**
|
||||||
|
|
||||||
|
There is currently a build of Termux available on Google Play for Android 11+ devices, with extensive adjustments in order to pass policy requirements there. This is under development and has missing functionality and bugs (see [here](https://github.com/termux-play-store/) for status updates) compared to the stable F-Droid build, which is why most users who can should still use F-Droid or GitHub build as mentioned above.
|
||||||
|
|
||||||
|
Currently, Google Play will try to update installations away from F-Droid ones. Updating will still fail as [sharedUserId](https://developer.android.com/guide/topics/manifest/manifest-element#uid) has been removed. A planned 0.118.1 F-Droid release will fix this by setting a higher version code than used for the PlayStore app. Meanwhile, to prevent Google Play from attempting to download and then fail to install the Google Play releases over existing installations, you can open the Termux apps pages on Google Play and then click on the 3 dots options button in the top right and then disable the Enable auto update toggle. However, the Termux apps updates will still show in the PlayStore app updates list.
|
||||||
|
|
||||||
|
If you want to help out with testing the Google Play build (or cannot install Termux from other sources), be aware that it's built from a separate repository (https://github.com/termux-play-store/) - be sure to report issues [there](https://github.com/termux-play-store/termux-issues/issues/new/choose), as any issues encountered might very well be specific to that repository.
|
||||||
|
|
||||||
## Uninstallation
|
## 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.
|
||||||
##
|
##
|
||||||
|
|
||||||
|
|
||||||
@@ -90,10 +143,10 @@ All community links are available [here](https://wiki.termux.com/wiki/Community)
|
|||||||
The main ones are the following.
|
The main ones are the following.
|
||||||
|
|
||||||
- [Termux Reddit community](https://reddit.com/r/termux)
|
- [Termux Reddit community](https://reddit.com/r/termux)
|
||||||
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
|
- [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)
|
- [Termux Dev Matrix Channel](https://matrix.to/#/#termux_dev:gitter.im) ([Gitter](https://gitter.im/termux/dev))
|
||||||
- [Termux Twitter](http://twitter.com/termux/)
|
- [Termux X (Twitter)](https://twitter.com/termuxdevs)
|
||||||
- [Termux Reports Email](mailto:termuxreports@groups.io)
|
- [Termux Support Email](mailto:support@termux.dev)
|
||||||
|
|
||||||
### Wikis
|
### Wikis
|
||||||
|
|
||||||
@@ -106,42 +159,108 @@ The main ones are the following.
|
|||||||
- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout)
|
- [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)
|
- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux)
|
||||||
- [Package Management](https://wiki.termux.com/wiki/Package_Management)
|
- [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)
|
- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux)
|
||||||
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
|
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
|
||||||
- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard)
|
- [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 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)
|
- [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)
|
- [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)
|
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Terminal
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary></summary>
|
||||||
|
|
||||||
### Terminal resources
|
### Terminal resources
|
||||||
|
|
||||||
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
- [XTerm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||||
- [vt100.net](http://vt100.net/)
|
- [vt100.net](https://vt100.net/)
|
||||||
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
- [Terminal codes (ANSI and terminfo equivalents)](https://wiki.bash-hackers.org/scripting/terminalcodes)
|
||||||
|
|
||||||
### Terminal emulators
|
### 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).
|
- 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).
|
- 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).
|
- 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)
|
- 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).
|
- 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.
|
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.
|
||||||
|
|||||||
100
app/build.gradle
100
app/build.gradle
@@ -2,16 +2,31 @@ plugins {
|
|||||||
id "com.android.application"
|
id "com.android.application"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
android {
|
||||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: 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 {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.2.0"
|
implementation "androidx.annotation:annotation:1.3.0"
|
||||||
implementation "androidx.core:core:1.6.0-rc01"
|
implementation "androidx.core:core:1.6.0"
|
||||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||||
implementation "androidx.preference:preference:1.1.1"
|
implementation "androidx.preference:preference:1.1.1"
|
||||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
|
implementation "com.google.android.material:material:1.4.0"
|
||||||
implementation "com.google.guava:guava:24.1-jre"
|
implementation "com.google.guava:guava:24.1-jre"
|
||||||
implementation "io.noties.markwon:core:$markwonVersion"
|
implementation "io.noties.markwon:core:$markwonVersion"
|
||||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||||
@@ -26,8 +41,13 @@ android {
|
|||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||||
versionCode 116
|
versionCode 1020
|
||||||
versionName "0.116"
|
versionName "0.119.0-beta.1"
|
||||||
|
|
||||||
|
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_PACKAGE_NAME = "com.termux"
|
||||||
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
||||||
@@ -44,15 +64,20 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ndk {
|
splits {
|
||||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
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 {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
storeFile file('dev_keystore.jks')
|
storeFile file('testkey_untrusted.jks')
|
||||||
keyAlias 'alias'
|
keyAlias 'alias'
|
||||||
storePassword 'xrj45yWGLbsO7W0v'
|
storePassword 'xrj45yWGLbsO7W0v'
|
||||||
keyPassword 'xrj45yWGLbsO7W0v'
|
keyPassword 'xrj45yWGLbsO7W0v'
|
||||||
@@ -62,7 +87,7 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources false // Reproducible builds
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +97,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
// Flag to enable support for the new language APIs
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@@ -91,11 +119,31 @@ android {
|
|||||||
includeAndroidResources = true
|
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 {
|
dependencies {
|
||||||
testImplementation "junit:junit:4.13.2"
|
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 {
|
task versionName {
|
||||||
@@ -104,6 +152,13 @@ task 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) {
|
def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
||||||
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||||
|
|
||||||
@@ -118,10 +173,11 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
|||||||
digest.update(buffer, 0, readBytes)
|
digest.update(buffer, 0, readBytes)
|
||||||
}
|
}
|
||||||
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
||||||
|
while (checksum.length() < 64) { checksum = "0" + checksum }
|
||||||
if (checksum == expectedChecksum) {
|
if (checksum == expectedChecksum) {
|
||||||
return
|
return
|
||||||
} else {
|
} 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()
|
file.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +195,7 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
|||||||
out.close()
|
out.close()
|
||||||
|
|
||||||
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
||||||
|
while (checksum.length() < 64) { checksum = "0" + checksum }
|
||||||
if (checksum != expectedChecksum) {
|
if (checksum != expectedChecksum) {
|
||||||
file.delete()
|
file.delete()
|
||||||
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
|
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
|
||||||
@@ -155,11 +212,22 @@ clean {
|
|||||||
|
|
||||||
task downloadBootstraps() {
|
task downloadBootstraps() {
|
||||||
doLast {
|
doLast {
|
||||||
def version = "2021.06.30-r1"
|
def packageVariant = project.ext.packageVariant
|
||||||
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version)
|
if (packageVariant == "apt-android-7") {
|
||||||
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version)
|
def version = "2024.06.17-r1" + "+" + packageVariant
|
||||||
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version)
|
downloadBootstrap("aarch64", "91a90661597fe14bb3c3563f5f65b243c0baaec42f2bc3d2243ff459e3942fb6", version)
|
||||||
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", version)
|
downloadBootstrap("arm", "d54b5eb2a305d72f267f9704deaca721b2bebbd3d4cca134aec31da719707997", version)
|
||||||
|
downloadBootstrap("i686", "06a51ac1c679d68d52045509f1a705622c8f41748ef753660e31e3b6a846eba2", version)
|
||||||
|
downloadBootstrap("x86_64", "4c8e43474c8d9543e01d4cbf3c4d7f59bbe4d696c38f6dece2b6ab3ba8881f2e", 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 + "\"")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@@ -10,3 +10,8 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
#-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.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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.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.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@@ -32,6 +34,9 @@
|
|||||||
<uses-permission android:name="android.permission.DUMP" />
|
<uses-permission android:name="android.permission.DUMP" />
|
||||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<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" />
|
||||||
|
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".app.TermuxApplication"
|
android:name=".app.TermuxApplication"
|
||||||
@@ -40,24 +45,21 @@
|
|||||||
android:extractNativeLibs="true"
|
android:extractNativeLibs="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="false"
|
android:supportsRtl="false"
|
||||||
android:theme="@style/Theme.Termux">
|
android:theme="@style/Theme.TermuxApp.DayNight.DarkActionBar"
|
||||||
|
tools:targetApi="m">
|
||||||
<!--
|
|
||||||
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
|
||||||
mark the app with "This app is optimized to run in full screen."
|
|
||||||
-->
|
|
||||||
<meta-data
|
|
||||||
android:name="android.max_aspect"
|
|
||||||
android:value="10.0" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".app.TermuxActivity"
|
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:label="@string/application_name"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true">
|
android:resizeableActivity="true"
|
||||||
|
android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar"
|
||||||
|
tools:targetApi="n">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@
|
|||||||
|
|
||||||
<activity-alias
|
<activity-alias
|
||||||
android:name=".HomeActivity"
|
android:name=".HomeActivity"
|
||||||
|
android:exported="true"
|
||||||
android:targetActivity=".app.TermuxActivity">
|
android:targetActivity=".app.TermuxActivity">
|
||||||
|
|
||||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||||
@@ -93,27 +96,33 @@
|
|||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:parentActivityName=".app.TermuxActivity"
|
android:parentActivityName=".app.TermuxActivity"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
|
tools:targetApi="n" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".app.activities.SettingsActivity"
|
android:name=".app.activities.SettingsActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/title_activity_termux_settings"
|
android:label="@string/title_activity_termux_settings"
|
||||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
android:theme="@style/Theme.TermuxApp.DayNight.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".shared.activities.ReportActivity"
|
android:name=".shared.activities.ReportActivity"
|
||||||
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
android:theme="@style/Theme.MarkdownViewActivity.DayNight"
|
||||||
android:documentLaunchMode="intoExisting"
|
android:documentLaunchMode="intoExisting" />
|
||||||
/>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".filepicker.TermuxFileReceiverActivity"
|
android:name=".app.api.file.FileReceiverActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:label="@string/application_name"
|
android:exported="false"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
android:resizeableActivity="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. -->
|
<!-- Accept multiple file types when sending. -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -129,6 +138,13 @@
|
|||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</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. -->
|
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
|
||||||
<intent-filter tools:ignore="AppLinkUrlError">
|
<intent-filter tools:ignore="AppLinkUrlError">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -141,7 +157,7 @@
|
|||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity-alias>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".filepicker.TermuxDocumentsProvider"
|
android:name=".filepicker.TermuxDocumentsProvider"
|
||||||
@@ -154,9 +170,35 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</provider>
|
</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
|
<service
|
||||||
android:name=".app.TermuxService"
|
android:name=".app.TermuxService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".app.RunCommandService"
|
android:name=".app.RunCommandService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -166,21 +208,30 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:name=".app.TermuxOpenReceiver" />
|
|
||||||
|
|
||||||
<provider
|
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8 mark the
|
||||||
android:name=".app.TermuxOpenReceiver$ContentProvider"
|
app with "This app is optimized to run in full screen." -->
|
||||||
android:authorities="${TERMUX_PACKAGE_NAME}.files"
|
<meta-data
|
||||||
android:exported="true"
|
android:name="android.max_aspect"
|
||||||
android:grantUriPermissions="true"
|
android:value="10.0" />
|
||||||
android:readPermission="android.permission.permRead" />
|
|
||||||
|
|
||||||
|
<!-- 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
|
<meta-data
|
||||||
android:name="com.sec.android.support.multiwindow"
|
android:name="com.sec.android.support.multiwindow"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
<meta-data
|
|
||||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
|
||||||
android:value="true" />
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -12,17 +12,19 @@ import android.os.IBinder;
|
|||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
import com.termux.shared.data.IntentUtils;
|
import com.termux.shared.data.IntentUtils;
|
||||||
import com.termux.shared.file.TermuxFileUtils;
|
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||||
import com.termux.shared.models.errors.Errno;
|
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||||
import com.termux.shared.models.errors.Error;
|
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;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
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.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import com.termux.shared.file.FileUtils;
|
import com.termux.shared.file.FileUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.notification.NotificationUtils;
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
import com.termux.app.utils.PluginUtils;
|
import com.termux.shared.shell.command.ExecutionCommand;
|
||||||
import com.termux.shared.models.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
|
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||||
@@ -61,6 +63,8 @@ public class RunCommandService extends Service {
|
|||||||
// Run again in case service is already started and onCreate() is not called
|
// Run again in case service is already started and onCreate() is not called
|
||||||
runStartForeground();
|
runStartForeground();
|
||||||
|
|
||||||
|
Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
|
||||||
|
|
||||||
ExecutionCommand executionCommand = new ExecutionCommand();
|
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||||
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||||
|
|
||||||
@@ -71,12 +75,11 @@ public class RunCommandService extends Service {
|
|||||||
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||||
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
return Service.START_NOT_STICKY;
|
return stopService();
|
||||||
}
|
}
|
||||||
|
|
||||||
executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
|
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);
|
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -100,8 +103,21 @@ public class RunCommandService extends Service {
|
|||||||
|
|
||||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
||||||
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
||||||
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
|
||||||
|
// 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.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
|
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.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.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||||
@@ -121,11 +137,11 @@ public class RunCommandService extends Service {
|
|||||||
// user knows someone tried to run a command in termux context, since it may be malicious
|
// 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
|
// 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.
|
// also sent, then its creator is also logged and shown.
|
||||||
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
|
||||||
if (errmsg != null) {
|
if (errmsg != null) {
|
||||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||||
return Service.START_NOT_STICKY;
|
return stopService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -134,8 +150,8 @@ public class RunCommandService extends Service {
|
|||||||
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
|
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);
|
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
return Service.START_NOT_STICKY;
|
return stopService();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get canonical path of executable
|
// Get canonical path of executable
|
||||||
@@ -147,10 +163,9 @@ public class RunCommandService extends Service {
|
|||||||
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||||
false);
|
false);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
|
|
||||||
executionCommand.setStateFailed(error);
|
executionCommand.setStateFailed(error);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
return Service.START_NOT_STICKY;
|
return stopService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -169,18 +184,25 @@ public class RunCommandService extends Service {
|
|||||||
true, true, true,
|
true, true, true,
|
||||||
false, true);
|
false, true);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
|
|
||||||
executionCommand.setStateFailed(error);
|
executionCommand.setStateFailed(error);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
return Service.START_NOT_STICKY;
|
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(TermuxFileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
|
||||||
|
|
||||||
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
// 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);
|
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
|
||||||
@@ -188,8 +210,11 @@ public class RunCommandService extends Service {
|
|||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
|
||||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
|
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_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_LABEL, executionCommand.commandLabel);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||||
@@ -211,8 +236,11 @@ public class RunCommandService extends Service {
|
|||||||
this.startService(execIntent);
|
this.startService(execIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
runStopForeground();
|
return stopService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int stopService() {
|
||||||
|
runStopForeground();
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +262,7 @@ public class RunCommandService extends Service {
|
|||||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
|
||||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
|
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;
|
if (builder == null) return null;
|
||||||
|
|
||||||
// No need to show a timestamp:
|
// No need to show a timestamp:
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
@@ -11,8 +9,6 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -29,37 +25,50 @@ import android.view.autofill.AutofillManager;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.app.api.file.FileReceiverActivity;
|
||||||
import com.termux.app.terminal.TermuxActivityRootView;
|
import com.termux.app.terminal.TermuxActivityRootView;
|
||||||
import com.termux.shared.packages.PermissionUtils;
|
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;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||||
import com.termux.app.activities.HelpActivity;
|
import com.termux.app.activities.HelpActivity;
|
||||||
import com.termux.app.activities.SettingsActivity;
|
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.TermuxSessionsListViewController;
|
||||||
import com.termux.app.terminal.io.TerminalToolbarViewPager;
|
import com.termux.app.terminal.io.TerminalToolbarViewPager;
|
||||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
|
||||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
import com.termux.shared.termux.extrakeys.ExtraKeysView;
|
||||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
import com.termux.shared.termux.interact.TextInputDialogUtils;
|
||||||
import com.termux.shared.interact.TextInputDialogUtils;
|
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
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.TerminalSession;
|
||||||
import com.termux.terminal.TerminalSessionClient;
|
import com.termux.terminal.TerminalSessionClient;
|
||||||
import com.termux.app.utils.CrashUtils;
|
|
||||||
import com.termux.view.TerminalView;
|
import com.termux.view.TerminalView;
|
||||||
import com.termux.view.TerminalViewClient;
|
import com.termux.view.TerminalViewClient;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A terminal emulator activity.
|
* A terminal emulator activity.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -70,7 +79,7 @@ import androidx.viewpager.widget.ViewPager;
|
|||||||
* </ul>
|
* </ul>
|
||||||
* about memory leaks.
|
* 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
|
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
|
||||||
@@ -94,7 +103,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
* The {@link TerminalSessionClient} interface implementation to allow for communication between
|
* The {@link TerminalSessionClient} interface implementation to allow for communication between
|
||||||
* {@link TerminalSession} and {@link TermuxActivity}.
|
* {@link TerminalSession} and {@link TermuxActivity}.
|
||||||
*/
|
*/
|
||||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Termux app shared preferences manager.
|
* Termux app shared preferences manager.
|
||||||
@@ -102,7 +111,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
private TermuxAppSharedPreferences mPreferences;
|
private TermuxAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Termux app shared properties manager, loaded from termux.properties
|
* Termux app SharedProperties loaded from termux.properties
|
||||||
*/
|
*/
|
||||||
private TermuxAppSharedProperties mProperties;
|
private TermuxAppSharedProperties mProperties;
|
||||||
|
|
||||||
@@ -121,6 +130,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
*/
|
*/
|
||||||
ExtraKeysView mExtraKeysView;
|
ExtraKeysView mExtraKeysView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client for the {@link #mExtraKeysView}.
|
||||||
|
*/
|
||||||
|
TermuxTerminalExtraKeys mTermuxTerminalExtraKeys;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The termux sessions list controller.
|
* The termux sessions list controller.
|
||||||
*/
|
*/
|
||||||
@@ -145,7 +159,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
/**
|
/**
|
||||||
* If onResume() was called after onCreate().
|
* If onResume() was called after onCreate().
|
||||||
*/
|
*/
|
||||||
private boolean isOnResumeAfterOnCreate = false;
|
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.
|
* The {@link TermuxActivity} is in an invalid state and must not be run.
|
||||||
@@ -154,11 +175,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
private int mNavBarHeight;
|
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_SELECT_URL_ID = 0;
|
||||||
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
|
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_AUTOFILL_ID = 2;
|
||||||
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
|
private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3;
|
||||||
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
|
private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4;
|
||||||
@@ -169,21 +191,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
private static final int CONTEXT_MENU_REPORT_ID = 9;
|
private static final int CONTEXT_MENU_REPORT_ID = 9;
|
||||||
|
|
||||||
private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input";
|
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";
|
private static final String LOG_TAG = "TermuxActivity";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "onCreate");
|
Logger.logDebug(LOG_TAG, "onCreate");
|
||||||
isOnResumeAfterOnCreate = true;
|
mIsOnResumeAfterOnCreate = true;
|
||||||
|
|
||||||
// Check if a crash happened on last run of the app and show a
|
if (savedInstanceState != null)
|
||||||
// notification with the crash details if it did
|
mIsActivityRecreated = savedInstanceState.getBoolean(ARG_ACTIVITY_RECREATED, false);
|
||||||
CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG);
|
|
||||||
|
|
||||||
// Load termux shared properties
|
// Delete ReportInfo serialized object files from cache older than 14 days
|
||||||
mProperties = new TermuxAppSharedProperties(this);
|
ReportActivity.deleteReportInfoFilesOlderThanXDays(this, 14, false);
|
||||||
|
|
||||||
|
// Load Termux app SharedProperties from disk
|
||||||
|
mProperties = TermuxAppSharedProperties.getProperties();
|
||||||
|
reloadProperties();
|
||||||
|
|
||||||
setActivityTheme();
|
setActivityTheme();
|
||||||
|
|
||||||
@@ -200,6 +225,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMargins();
|
||||||
|
|
||||||
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
|
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
|
||||||
mTermuxActivityRootView.setActivity(this);
|
mTermuxActivityRootView.setActivity(this);
|
||||||
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
|
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
|
||||||
@@ -215,8 +242,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDrawerTheme();
|
|
||||||
|
|
||||||
setTermuxTerminalViewAndClients();
|
setTermuxTerminalViewAndClients();
|
||||||
|
|
||||||
setTerminalToolbarView(savedInstanceState);
|
setTerminalToolbarView(savedInstanceState);
|
||||||
@@ -229,6 +254,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
registerForContextMenu(mTerminalView);
|
registerForContextMenu(mTerminalView);
|
||||||
|
|
||||||
|
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
|
||||||
|
|
||||||
|
try {
|
||||||
// Start the {@link TermuxService} and make it run regardless of who is bound to it
|
// Start the {@link TermuxService} and make it run regardless of who is bound to it
|
||||||
Intent serviceIntent = new Intent(this, TermuxService.class);
|
Intent serviceIntent = new Intent(this, TermuxService.class);
|
||||||
startService(serviceIntent);
|
startService(serviceIntent);
|
||||||
@@ -237,6 +265,15 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
// callback if it succeeds.
|
// callback if it succeeds.
|
||||||
if (!bindService(serviceIntent, this, 0))
|
if (!bindService(serviceIntent, this, 0))
|
||||||
throw new RuntimeException("bindService() failed");
|
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
|
// Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux
|
||||||
// app has been opened.
|
// app has been opened.
|
||||||
@@ -253,13 +290,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
mIsVisible = true;
|
mIsVisible = true;
|
||||||
|
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.onStart();
|
mTermuxTerminalSessionActivityClient.onStart();
|
||||||
|
|
||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onStart();
|
mTermuxTerminalViewClient.onStart();
|
||||||
|
|
||||||
if (!mProperties.isTerminalMarginAdjustmentDisabled())
|
if (mPreferences.isTerminalMarginAdjustmentEnabled())
|
||||||
addTermuxActivityRootViewGlobalLayoutListener();
|
addTermuxActivityRootViewGlobalLayoutListener();
|
||||||
|
|
||||||
registerTermuxActivityBroadcastReceiver();
|
registerTermuxActivityBroadcastReceiver();
|
||||||
@@ -273,13 +310,17 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
if (mIsInvalidState) return;
|
if (mIsInvalidState) return;
|
||||||
|
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.onResume();
|
mTermuxTerminalSessionActivityClient.onResume();
|
||||||
|
|
||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onResume();
|
mTermuxTerminalViewClient.onResume();
|
||||||
|
|
||||||
isOnResumeAfterOnCreate = false;
|
// 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
|
@Override
|
||||||
@@ -292,15 +333,15 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
mIsVisible = false;
|
mIsVisible = false;
|
||||||
|
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.onStop();
|
mTermuxTerminalSessionActivityClient.onStop();
|
||||||
|
|
||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onStop();
|
mTermuxTerminalViewClient.onStop();
|
||||||
|
|
||||||
removeTermuxActivityRootViewGlobalLayoutListener();
|
removeTermuxActivityRootViewGlobalLayoutListener();
|
||||||
|
|
||||||
unregisterTermuxActivityBroadcastReceiever();
|
unregisterTermuxActivityBroadcastReceiver();
|
||||||
getDrawer().closeDrawers();
|
getDrawer().closeDrawers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,8 +368,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
|
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "onSaveInstanceState");
|
||||||
|
|
||||||
super.onSaveInstanceState(savedInstanceState);
|
super.onSaveInstanceState(savedInstanceState);
|
||||||
saveTerminalToolbarTextInput(savedInstanceState);
|
saveTerminalToolbarTextInput(savedInstanceState);
|
||||||
|
savedInstanceState.putBoolean(ARG_ACTIVITY_RECREATED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -342,24 +386,25 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onServiceConnected(ComponentName componentName, IBinder service) {
|
public void onServiceConnected(ComponentName componentName, IBinder service) {
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "onServiceConnected");
|
Logger.logDebug(LOG_TAG, "onServiceConnected");
|
||||||
|
|
||||||
mTermuxService = ((TermuxService.LocalBinder) service).service;
|
mTermuxService = ((TermuxService.LocalBinder) service).service;
|
||||||
|
|
||||||
setTermuxSessionsListView();
|
setTermuxSessionsListView();
|
||||||
|
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
setIntent(null);
|
||||||
|
|
||||||
if (mTermuxService.isTermuxSessionsEmpty()) {
|
if (mTermuxService.isTermuxSessionsEmpty()) {
|
||||||
if (mIsVisible) {
|
if (mIsVisible) {
|
||||||
TermuxInstaller.setupBootstrapIfNeeded(TermuxActivity.this, () -> {
|
TermuxInstaller.setupBootstrapIfNeeded(TermuxActivity.this, () -> {
|
||||||
if (mTermuxService == null) return; // Activity might have been destroyed.
|
if (mTermuxService == null) return; // Activity might have been destroyed.
|
||||||
try {
|
try {
|
||||||
Bundle bundle = getIntent().getExtras();
|
|
||||||
boolean launchFailsafe = false;
|
boolean launchFailsafe = false;
|
||||||
if (bundle != null) {
|
if (intent != null && intent.getExtras() != null) {
|
||||||
launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
launchFailsafe = intent.getExtras().getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||||
}
|
}
|
||||||
mTermuxTerminalSessionClient.addNewSession(launchFailsafe, null);
|
mTermuxTerminalSessionActivityClient.addNewSession(launchFailsafe, null);
|
||||||
} catch (WindowManager.BadTokenException e) {
|
} catch (WindowManager.BadTokenException e) {
|
||||||
// Activity finished - ignore.
|
// Activity finished - ignore.
|
||||||
}
|
}
|
||||||
@@ -369,23 +414,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
finishActivityIfNotFinishing();
|
finishActivityIfNotFinishing();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Intent i = getIntent();
|
// If termux was started from launcher "New session" shortcut and activity is recreated,
|
||||||
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
|
// 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.
|
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
|
||||||
boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||||
mTermuxTerminalSessionClient.addNewSession(isFailSafe, null);
|
mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null);
|
||||||
} else {
|
} else {
|
||||||
mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast());
|
mTermuxTerminalSessionActivityClient.setCurrentSession(mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the {@link TerminalSession} and {@link TerminalEmulator} clients.
|
// Update the {@link TerminalSession} and {@link TerminalEmulator} clients.
|
||||||
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionClient);
|
mTermuxService.setTermuxTerminalSessionClient(mTermuxTerminalSessionActivityClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "onServiceDisconnected");
|
Logger.logDebug(LOG_TAG, "onServiceDisconnected");
|
||||||
|
|
||||||
// Respect being stopped from the {@link TermuxService} notification action.
|
// Respect being stopped from the {@link TermuxService} notification action.
|
||||||
@@ -396,20 +442,31 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void setActivityTheme() {
|
|
||||||
if (mProperties.isUsingBlackUI()) {
|
private void reloadProperties() {
|
||||||
this.setTheme(R.style.Theme_Termux_Black);
|
mProperties.loadTermuxPropertiesFromDisk();
|
||||||
} else {
|
|
||||||
this.setTheme(R.style.Theme_Termux);
|
if (mTermuxTerminalViewClient != null)
|
||||||
}
|
mTermuxTerminalViewClient.onReloadProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDrawerTheme() {
|
|
||||||
if (mProperties.isUsingBlackUI()) {
|
|
||||||
findViewById(R.id.left_drawer).setBackgroundColor(ContextCompat.getColor(this,
|
private void setActivityTheme() {
|
||||||
android.R.color.background_dark));
|
// Update NightMode.APP_NIGHT_MODE
|
||||||
((ImageButton) findViewById(R.id.settings_button)).setColorFilter(Color.WHITE);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -427,8 +484,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
private void setTermuxTerminalViewAndClients() {
|
private void setTermuxTerminalViewAndClients() {
|
||||||
// Set termux terminal view and session clients
|
// Set termux terminal view and session clients
|
||||||
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
|
mTermuxTerminalSessionActivityClient = new TermuxTerminalSessionActivityClient(this);
|
||||||
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionClient);
|
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionActivityClient);
|
||||||
|
|
||||||
// Set termux terminal view
|
// Set termux terminal view
|
||||||
mTerminalView = findViewById(R.id.terminal_view);
|
mTerminalView = findViewById(R.id.terminal_view);
|
||||||
@@ -437,8 +494,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onCreate();
|
mTermuxTerminalViewClient.onCreate();
|
||||||
|
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.onCreate();
|
mTermuxTerminalSessionActivityClient.onCreate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTermuxSessionsListView() {
|
private void setTermuxSessionsListView() {
|
||||||
@@ -452,7 +509,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
|
|
||||||
private void setTerminalToolbarView(Bundle savedInstanceState) {
|
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);
|
if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
||||||
@@ -469,23 +529,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setTerminalToolbarHeight() {
|
private void setTerminalToolbarHeight() {
|
||||||
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
|
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
||||||
if (terminalToolbarViewPager == null) return;
|
if (terminalToolbarViewPager == null) return;
|
||||||
|
|
||||||
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
||||||
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
|
layoutParams.height = Math.round(mTerminalToolbarDefaultHeight *
|
||||||
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
|
(mTermuxTerminalExtraKeys.getExtraKeysInfo() == null ? 0 : mTermuxTerminalExtraKeys.getExtraKeysInfo().getMatrix().length) *
|
||||||
mProperties.getTerminalToolbarHeightScaleFactor());
|
mProperties.getTerminalToolbarHeightScaleFactor());
|
||||||
terminalToolbarViewPager.setLayoutParams(layoutParams);
|
terminalToolbarViewPager.setLayoutParams(layoutParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleTerminalToolbar() {
|
public void toggleTerminalToolbar() {
|
||||||
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
|
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
|
||||||
if (terminalToolbarViewPager == null) return;
|
if (terminalToolbarViewPager == null) return;
|
||||||
|
|
||||||
final boolean showNow = mPreferences.toogleShowTerminalToolbar();
|
final boolean showNow = mPreferences.toogleShowTerminalToolbar();
|
||||||
Logger.showToast(this, (showNow ? getString(R.string.msg_enabling_terminal_toolbar) : getString(R.string.msg_disabling_terminal_toolbar)), true);
|
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);
|
terminalToolbarViewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
|
||||||
if (showNow && terminalToolbarViewPager.getCurrentItem() == 1) {
|
if (showNow && isTerminalToolbarTextInputViewSelected()) {
|
||||||
// Focus the text input view if just revealed.
|
// Focus the text input view if just revealed.
|
||||||
findViewById(R.id.terminal_toolbar_text_input).requestFocus();
|
findViewById(R.id.terminal_toolbar_text_input).requestFocus();
|
||||||
}
|
}
|
||||||
@@ -506,17 +567,17 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
private void setSettingsButtonView() {
|
private void setSettingsButtonView() {
|
||||||
ImageButton settingsButton = findViewById(R.id.settings_button);
|
ImageButton settingsButton = findViewById(R.id.settings_button);
|
||||||
settingsButton.setOnClickListener(v -> {
|
settingsButton.setOnClickListener(v -> {
|
||||||
startActivity(new Intent(this, SettingsActivity.class));
|
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setNewSessionButtonView() {
|
private void setNewSessionButtonView() {
|
||||||
View newSessionButton = findViewById(R.id.new_session_button);
|
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 -> {
|
newSessionButton.setOnLongClickListener(v -> {
|
||||||
TextInputDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
TextInputDialogUtils.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_create_named_session_confirm, text -> mTermuxTerminalSessionActivityClient.addNewSession(false, text),
|
||||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
|
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionActivityClient.addNewSession(true, text),
|
||||||
-1, null, null);
|
-1, null, null);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -581,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_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);
|
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_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_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);
|
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
|
||||||
@@ -609,6 +673,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
case CONTEXT_MENU_SHARE_TRANSCRIPT_ID:
|
case CONTEXT_MENU_SHARE_TRANSCRIPT_ID:
|
||||||
mTermuxTerminalViewClient.shareSessionTranscript();
|
mTermuxTerminalViewClient.shareSessionTranscript();
|
||||||
return true;
|
return true;
|
||||||
|
case CONTEXT_MENU_SHARE_SELECTED_TEXT:
|
||||||
|
mTermuxTerminalViewClient.shareSelectedText();
|
||||||
|
return true;
|
||||||
case CONTEXT_MENU_AUTOFILL_ID:
|
case CONTEXT_MENU_AUTOFILL_ID:
|
||||||
requestAutoFill();
|
requestAutoFill();
|
||||||
return true;
|
return true;
|
||||||
@@ -625,10 +692,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
toggleKeepScreenOn();
|
toggleKeepScreenOn();
|
||||||
return true;
|
return true;
|
||||||
case CONTEXT_MENU_HELP_ID:
|
case CONTEXT_MENU_HELP_ID:
|
||||||
startActivity(new Intent(this, HelpActivity.class));
|
ActivityUtils.startActivity(this, new Intent(this, HelpActivity.class));
|
||||||
return true;
|
return true;
|
||||||
case CONTEXT_MENU_SETTINGS_ID:
|
case CONTEXT_MENU_SETTINGS_ID:
|
||||||
startActivity(new Intent(this, SettingsActivity.class));
|
ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class));
|
||||||
return true;
|
return true;
|
||||||
case CONTEXT_MENU_REPORT_ID:
|
case CONTEXT_MENU_REPORT_ID:
|
||||||
mTermuxTerminalViewClient.reportIssueFromTranscript();
|
mTermuxTerminalViewClient.reportIssueFromTranscript();
|
||||||
@@ -638,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) {
|
private void showKillSessionDialog(TerminalSession session) {
|
||||||
if (session == null) return;
|
if (session == null) return;
|
||||||
|
|
||||||
@@ -657,8 +731,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
session.reset();
|
session.reset();
|
||||||
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
||||||
|
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.onResetTerminalSession();
|
mTermuxTerminalSessionActivityClient.onResetTerminalSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,7 +745,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
// The startActivity() call is not documented to throw IllegalArgumentException.
|
// The startActivity() call is not documented to throw IllegalArgumentException.
|
||||||
// However, crash reporting shows that it sometimes does, so catch it here.
|
// However, crash reporting shows that it sometimes does, so catch it here.
|
||||||
new AlertDialog.Builder(this).setMessage(getString(R.string.error_styling_not_installed))
|
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() {
|
private void toggleKeepScreenOn() {
|
||||||
@@ -696,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() {
|
public void requestStoragePermission(boolean isPermissionCallback) {
|
||||||
if (PermissionUtils.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
new Thread() {
|
||||||
return true;
|
@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, true, !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 {
|
} else {
|
||||||
Logger.logInfo(LOG_TAG, "Storage permission not granted, requesting permission.");
|
if (isPermissionCallback)
|
||||||
PermissionUtils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION);
|
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
|
||||||
return false;
|
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
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
|
Logger.logVerbose(LOG_TAG, "onRequestPermissionsResult: requestCode: " + requestCode + ", permissions: " + Arrays.toString(permissions) + ", grantResults: " + Arrays.toString(grantResults));
|
||||||
TermuxInstaller.setupStorageSymlinks(this);
|
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
|
||||||
} else {
|
requestStoragePermission(true);
|
||||||
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,6 +836,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
return mExtraKeysView;
|
return mExtraKeysView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TermuxTerminalExtraKeys getTermuxTerminalExtraKeys() {
|
||||||
|
return mTermuxTerminalExtraKeys;
|
||||||
|
}
|
||||||
|
|
||||||
public void setExtraKeysView(ExtraKeysView extraKeysView) {
|
public void setExtraKeysView(ExtraKeysView extraKeysView) {
|
||||||
mExtraKeysView = extraKeysView;
|
mExtraKeysView = extraKeysView;
|
||||||
}
|
}
|
||||||
@@ -744,6 +848,24 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
return (DrawerLayout) findViewById(R.id.drawer_layout);
|
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() {
|
public void termuxSessionListNotifyUpdated() {
|
||||||
mTermuxSessionListViewController.notifyDataSetChanged();
|
mTermuxSessionListViewController.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
@@ -753,7 +875,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOnResumeAfterOnCreate() {
|
public boolean isOnResumeAfterOnCreate() {
|
||||||
return isOnResumeAfterOnCreate;
|
return mIsOnResumeAfterOnCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActivityRecreated() {
|
||||||
|
return mIsActivityRecreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -770,8 +896,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
return mTermuxTerminalViewClient;
|
return mTermuxTerminalViewClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TermuxTerminalSessionClient getTermuxTerminalSessionClient() {
|
public TermuxTerminalSessionActivityClient getTermuxTerminalSessionClient() {
|
||||||
return mTermuxTerminalSessionClient;
|
return mTermuxTerminalSessionActivityClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -793,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.
|
// Make sure that terminal styling is always applied.
|
||||||
Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE);
|
||||||
|
stylingIntent.putExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, recreateActivity);
|
||||||
context.sendBroadcast(stylingIntent);
|
context.sendBroadcast(stylingIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerTermuxActivityBroadcastReceiver() {
|
private void registerTermuxActivityBroadcastReceiver() {
|
||||||
IntentFilter intentFilter = new IntentFilter();
|
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_RELOAD_STYLE);
|
||||||
|
intentFilter.addAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS);
|
||||||
|
|
||||||
registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter);
|
registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unregisterTermuxActivityBroadcastReceiever() {
|
private void unregisterTermuxActivityBroadcastReceiver() {
|
||||||
unregisterReceiver(mTermuxActivityBroadcastReceiver);
|
unregisterReceiver(mTermuxActivityBroadcastReceiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fixTermuxActivityBroadcastReceieverIntent(Intent intent) {
|
private void fixTermuxActivityBroadcastReceiverIntent(Intent intent) {
|
||||||
if (intent == null) return;
|
if (intent == null) return;
|
||||||
|
|
||||||
String extraReloadStyle = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE);
|
String extraReloadStyle = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE);
|
||||||
@@ -827,17 +955,20 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (intent == null) return;
|
if (intent == null) return;
|
||||||
|
|
||||||
if (mIsVisible) {
|
if (mIsVisible) {
|
||||||
fixTermuxActivityBroadcastReceieverIntent(intent);
|
fixTermuxActivityBroadcastReceiverIntent(intent);
|
||||||
|
|
||||||
switch (intent.getAction()) {
|
switch (intent.getAction()) {
|
||||||
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
case TERMUX_ACTIVITY.ACTION_NOTIFY_APP_CRASH:
|
||||||
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
Logger.logDebug(LOG_TAG, "Received intent to notify app crash");
|
||||||
if (ensureStoragePermissionGranted())
|
TermuxCrashUtils.notifyAppCrashFromCrashLogFile(context, LOG_TAG);
|
||||||
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
|
||||||
return;
|
return;
|
||||||
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
|
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
|
||||||
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
|
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;
|
return;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
@@ -845,39 +976,43 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reloadActivityStyling() {
|
private void reloadActivityStyling(boolean recreateActivity) {
|
||||||
if (mProperties!= null) {
|
if (mProperties != null) {
|
||||||
mProperties.loadTermuxPropertiesFromDisk();
|
reloadProperties();
|
||||||
|
|
||||||
if (mExtraKeysView != null) {
|
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();
|
setTerminalToolbarHeight();
|
||||||
|
|
||||||
if (mTermuxTerminalSessionClient != null)
|
FileReceiverActivity.updateFileReceiverActivityComponentsState(this);
|
||||||
mTermuxTerminalSessionClient.onReload();
|
|
||||||
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
|
mTermuxTerminalSessionActivityClient.onReloadActivityStyling();
|
||||||
|
|
||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onReload();
|
mTermuxTerminalViewClient.onReloadActivityStyling();
|
||||||
|
|
||||||
if (mTermuxService != null)
|
|
||||||
mTermuxService.setTerminalTranscriptRows();
|
|
||||||
|
|
||||||
// To change the activity and drawer theme, activity needs to be recreated.
|
// To change the activity and drawer theme, activity needs to be recreated.
|
||||||
// But this will destroy the activity, and will call the onCreate() again.
|
// It will destroy the activity, including all stored variables and views, and onCreate()
|
||||||
// We need to investigate if enabling this is wise, since all stored variables and
|
// will be called again. Extra keys input text, terminal sessions and transcripts will be preserved.
|
||||||
// views will be destroyed and bindService() will be called again. Extra keys input
|
if (recreateActivity) {
|
||||||
// text will we restored since that has already been implemented. Terminal sessions
|
Logger.logDebug(LOG_TAG, "Recreating activity");
|
||||||
// and transcripts are also already preserved. Theme does change properly too.
|
TermuxActivity.this.recreate();
|
||||||
// TermuxActivity.this.recreate();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void startTermuxActivity(@NonNull final Context context) {
|
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) {
|
public static Intent newInstance(@NonNull final Context context) {
|
||||||
|
|||||||
@@ -1,29 +1,86 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
import com.termux.shared.crash.TermuxCrashUtils;
|
import com.termux.BuildConfig;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
import com.termux.shared.errors.Error;
|
||||||
import com.termux.shared.logger.Logger;
|
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 {
|
public class TermuxApplication extends Application {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxApplication";
|
||||||
|
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
// Set crash handler for the app
|
Context context = getApplicationContext();
|
||||||
TermuxCrashUtils.setCrashHandler(this);
|
|
||||||
|
|
||||||
// Set log level for the app
|
// Set crash handler for the app
|
||||||
setLogLevel();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLogLevel() {
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
// 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;
|
if (preferences == null) return;
|
||||||
preferences.setLogLevel(null, preferences.getLogLevel());
|
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||||
Logger.logDebug("Starting Application");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,19 +4,26 @@ import android.app.Activity;
|
|||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.UserManager;
|
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.utils.CrashUtils;
|
|
||||||
import com.termux.shared.file.FileUtils;
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.shell.command.ExecutionCommand;
|
||||||
|
import com.termux.shared.shell.command.runner.app.AppShell;
|
||||||
|
import com.termux.shared.termux.crash.TermuxCrashUtils;
|
||||||
|
import com.termux.shared.termux.file.TermuxFileUtils;
|
||||||
import com.termux.shared.interact.MessageDialogUtils;
|
import com.termux.shared.interact.MessageDialogUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.models.errors.Error;
|
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.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
@@ -28,6 +35,11 @@ import java.util.List;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
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:
|
* Install the Termux bootstrap packages if necessary by following the below steps:
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -53,34 +65,55 @@ final class TermuxInstaller {
|
|||||||
|
|
||||||
/** Performs bootstrap setup if necessary. */
|
/** Performs bootstrap setup if necessary. */
|
||||||
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
|
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
|
// Termux can only be run as the primary user (device owner) since only that
|
||||||
// account has the expected file system paths. Verify that:
|
// account has the expected file system paths. Verify that:
|
||||||
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PackageUtils.isCurrentUserThePrimaryUser(activity)) {
|
||||||
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
|
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message,
|
||||||
if (!isPrimaryUser) {
|
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
|
||||||
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
|
||||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||||
|
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
||||||
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
||||||
activity.getString(R.string.bootstrap_error_title),
|
activity.getString(R.string.bootstrap_error_title),
|
||||||
bootstrapErrorMessage);
|
bootstrapErrorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
if (!isFilesDirectoryAccessible) {
|
||||||
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
|
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 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)) {
|
if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
|
||||||
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) {
|
||||||
// If prefix directory is empty or only contains the tmp directory
|
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains specific unimportant files.");
|
||||||
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 {
|
} else {
|
||||||
whenDone.run();
|
whenDone.run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
} else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) {
|
||||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
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);
|
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
||||||
@@ -92,24 +125,35 @@ final class TermuxInstaller {
|
|||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
|
|
||||||
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
|
||||||
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
|
||||||
|
|
||||||
// Delete prefix staging directory or any file at its destination
|
// Delete prefix staging directory or any file at its destination
|
||||||
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
|
error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete prefix directory or any file at its destination
|
// Delete prefix directory or any file at its destination
|
||||||
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||||
return;
|
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 byte[] buffer = new byte[8096];
|
||||||
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
||||||
@@ -126,23 +170,23 @@ final class TermuxInstaller {
|
|||||||
if (parts.length != 2)
|
if (parts.length != 2)
|
||||||
throw new RuntimeException("Malformed symlink line: " + line);
|
throw new RuntimeException("Malformed symlink line: " + line);
|
||||||
String oldPath = parts[0];
|
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));
|
symlinks.add(Pair.create(oldPath, newPath));
|
||||||
|
|
||||||
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String zipEntryName = zipEntry.getName();
|
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();
|
boolean isDirectory = zipEntry.isDirectory();
|
||||||
|
|
||||||
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +196,9 @@ final class TermuxInstaller {
|
|||||||
while ((readBytes = zipInput.read(buffer)) != -1)
|
while ((readBytes = zipInput.read(buffer)) != -1)
|
||||||
outStream.write(buffer, 0, readBytes);
|
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") ||
|
||||||
|
zipEntryName.equals("etc/termux/bootstrap/termux-bootstrap-second-stage.sh")) {
|
||||||
//noinspection OctalInteger
|
//noinspection OctalInteger
|
||||||
Os.chmod(targetFile.getAbsolutePath(), 0700);
|
Os.chmod(targetFile.getAbsolutePath(), 0700);
|
||||||
}
|
}
|
||||||
@@ -167,17 +213,43 @@ final class TermuxInstaller {
|
|||||||
Os.symlink(symlink.first, symlink.second);
|
Os.symlink(symlink.first, symlink.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
|
Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory.");
|
||||||
|
|
||||||
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
|
if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) {
|
||||||
throw new RuntimeException("Moving prefix staging to prefix directory failed");
|
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Termux bootstrap second stage
|
||||||
|
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
|
||||||
|
String termuxBootstrapSecondStageFile = TERMUX_PREFIX_DIR_PATH + "/etc/termux/bootstrap/termux-bootstrap-second-stage.sh";
|
||||||
|
if (FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
|
||||||
|
ExecutionCommand executionCommand = new ExecutionCommand(-1,
|
||||||
|
termuxBootstrapSecondStageFile, null, null,
|
||||||
|
null, ExecutionCommand.Runner.APP_SHELL.getName(), false);
|
||||||
|
executionCommand.commandLabel = "Termux Bootstrap Second Stage Command";
|
||||||
|
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_NORMAL;
|
||||||
|
AppShell appShell = AppShell.execute(activity, executionCommand, null, new TermuxShellEnvironment(), null, true);
|
||||||
|
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
|
||||||
|
if (appShell == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0 || stderrSet) {
|
||||||
|
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into
|
||||||
|
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
||||||
|
if (error != null)
|
||||||
|
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||||
|
|
||||||
|
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||||
|
|
||||||
|
// Recreate env file since termux prefix was wiped earlier
|
||||||
|
TermuxShellEnvironment.writeEnvironmentToFile(activity);
|
||||||
|
|
||||||
activity.runOnUiThread(whenDone);
|
activity.runOnUiThread(whenDone);
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
activity.runOnUiThread(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
@@ -192,11 +264,11 @@ final class TermuxInstaller {
|
|||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
|
public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
|
||||||
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
||||||
|
|
||||||
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
||||||
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true);
|
sendBootstrapCrashReportNotification(activity, message);
|
||||||
|
|
||||||
activity.runOnUiThread(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -207,7 +279,7 @@ final class TermuxInstaller {
|
|||||||
})
|
})
|
||||||
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
||||||
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||||
}).show();
|
}).show();
|
||||||
} catch (WindowManager.BadTokenException e1) {
|
} catch (WindowManager.BadTokenException e1) {
|
||||||
@@ -216,8 +288,20 @@ final class TermuxInstaller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
static void setupStorageSymlinks(final Context context) {
|
||||||
final String LOG_TAG = "termux-storage";
|
final String LOG_TAG = "termux-storage";
|
||||||
|
final String title = TermuxConstants.TERMUX_APP_NAME + " Setup Storage Error";
|
||||||
|
|
||||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
||||||
|
|
||||||
@@ -231,15 +315,21 @@ final class TermuxInstaller {
|
|||||||
if (error != null) {
|
if (error != null) {
|
||||||
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||||
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true);
|
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
||||||
|
"## " + title + "\n\n" + Error.getErrorMarkdownString(error),
|
||||||
|
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
|
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();
|
File sharedDir = Environment.getExternalStorageDirectory();
|
||||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
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);
|
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||||
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
|
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
|
||||||
|
|
||||||
@@ -255,9 +345,25 @@ final class TermuxInstaller {
|
|||||||
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||||
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
||||||
|
|
||||||
final File[] dirs = context.getExternalFilesDirs(null);
|
File podcastsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PODCASTS);
|
||||||
if (dirs != null && dirs.length > 1) {
|
Os.symlink(podcastsDir.getAbsolutePath(), new File(storageDir, "podcasts").getAbsolutePath());
|
||||||
for (int i = 1; i < dirs.length; i++) {
|
|
||||||
|
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];
|
File dir = dirs[i];
|
||||||
if (dir == null) continue;
|
if (dir == null) continue;
|
||||||
String symlinkName = "external-" + i;
|
String symlinkName = "external-" + i;
|
||||||
@@ -266,11 +372,25 @@ 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.");
|
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||||
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
||||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true);
|
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
||||||
|
"## " + title + "\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)),
|
||||||
|
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import android.os.ParcelFileDescriptor;
|
|||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.webkit.MimeTypeMap;
|
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.logger.Logger;
|
||||||
|
import com.termux.shared.net.uri.UriScheme;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -30,11 +35,13 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
final Uri data = intent.getData();
|
final Uri data = intent.getData();
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
Logger.logError(LOG_TAG, "termux-open: Called without intent data");
|
Logger.logError(LOG_TAG, "Called without intent data");
|
||||||
return;
|
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 String contentTypeExtra = intent.getStringExtra("content-type");
|
||||||
final boolean useChooser = intent.getBooleanExtra("chooser", false);
|
final boolean useChooser = intent.getBooleanExtra("chooser", false);
|
||||||
final String intentAction = intent.getAction() == null ? Intent.ACTION_VIEW : intent.getAction();
|
final String intentAction = intent.getAction() == null ? Intent.ACTION_VIEW : intent.getAction();
|
||||||
@@ -48,8 +55,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
|
String scheme = data.getScheme();
|
||||||
if (isExternalUrl) {
|
if (scheme != null && !UriScheme.SCHEME_FILE.equals(scheme)) {
|
||||||
Intent urlIntent = new Intent(intentAction, data);
|
Intent urlIntent = new Intent(intentAction, data);
|
||||||
if (intentAction.equals(Intent.ACTION_SEND)) {
|
if (intentAction.equals(Intent.ACTION_SEND)) {
|
||||||
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
|
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
|
||||||
@@ -61,14 +68,21 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
context.startActivity(urlIntent);
|
context.startActivity(urlIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} 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;
|
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);
|
final File fileToShare = new File(filePath);
|
||||||
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +103,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
contentTypeToUse = contentTypeExtra;
|
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)) {
|
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||||
@@ -105,12 +120,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
context.startActivity(sendIntent);
|
context.startActivity(sendIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} 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 {
|
public static class ContentProvider extends android.content.ContentProvider {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxContentProvider";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreate() {
|
public boolean onCreate() {
|
||||||
return true;
|
return true;
|
||||||
@@ -178,15 +195,33 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
File file = new File(uri.getPath());
|
File file = new File(uri.getPath());
|
||||||
try {
|
try {
|
||||||
String path = file.getCanonicalPath();
|
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();
|
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||||
// See https://support.google.com/faqs/answer/7496913:
|
// See https://support.google.com/faqs/answer/7496913:
|
||||||
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
|
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
|
||||||
throw new IllegalArgumentException("Invalid path: " + path);
|
throw new IllegalArgumentException("Invalid path: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} catch (IOException e) {
|
||||||
throw new IllegalArgumentException(e);
|
throw new IllegalArgumentException(e);
|
||||||
}
|
}
|
||||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
||||||
|
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,41 +5,46 @@ import android.app.Notification;
|
|||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.net.Uri;
|
|
||||||
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiManager;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.provider.Settings;
|
|
||||||
import android.widget.ArrayAdapter;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
import com.termux.app.event.SystemEventReceiver;
|
||||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
import com.termux.app.terminal.TermuxTerminalSessionActivityClient;
|
||||||
import com.termux.app.utils.PluginUtils;
|
import com.termux.app.terminal.TermuxTerminalSessionServiceClient;
|
||||||
|
import com.termux.shared.termux.plugins.TermuxPluginUtils;
|
||||||
import com.termux.shared.data.IntentUtils;
|
import com.termux.shared.data.IntentUtils;
|
||||||
import com.termux.shared.models.errors.Errno;
|
import com.termux.shared.net.uri.UriUtils;
|
||||||
|
import com.termux.shared.errors.Errno;
|
||||||
import com.termux.shared.shell.ShellUtils;
|
import com.termux.shared.shell.ShellUtils;
|
||||||
import com.termux.shared.shell.TermuxShellEnvironmentClient;
|
import com.termux.shared.shell.command.runner.app.AppShell;
|
||||||
import com.termux.shared.shell.TermuxShellUtils;
|
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;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||||
import com.termux.shared.shell.TermuxSession;
|
import com.termux.shared.termux.shell.TermuxShellManager;
|
||||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
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.logger.Logger;
|
||||||
import com.termux.shared.notification.NotificationUtils;
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
import com.termux.shared.packages.PermissionUtils;
|
import com.termux.shared.android.PermissionUtils;
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
import com.termux.shared.models.ExecutionCommand;
|
import com.termux.shared.shell.command.ExecutionCommand;
|
||||||
import com.termux.shared.shell.TermuxTask;
|
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.TerminalEmulator;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
import com.termux.terminal.TerminalSessionClient;
|
import com.termux.terminal.TerminalSessionClient;
|
||||||
@@ -47,11 +52,9 @@ import com.termux.terminal.TerminalSessionClient;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service holding a list of {@link TermuxSession} in {@link #mTermuxSessions} and background {@link TermuxTask}
|
* A service holding a list of {@link TermuxSession} in {@link TermuxShellManager#mTermuxSessions} and background {@link AppShell}
|
||||||
* in {@link #mTermuxTasks}, showing a foreground notification while running so that it is not terminated.
|
* 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 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
|
* 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.
|
* restart {@link TermuxActivity} later to yet again access the sessions.
|
||||||
@@ -62,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
|
* Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
|
||||||
* {@link #buildNotification()}.
|
* {@link #buildNotification()}.
|
||||||
*/
|
*/
|
||||||
public final class TermuxService extends Service implements TermuxTask.TermuxTaskClient, TermuxSession.TermuxSessionClient {
|
public final class TermuxService extends Service implements AppShell.AppShellClient, TermuxSession.TermuxSessionClient {
|
||||||
|
|
||||||
private static int EXECUTION_ID = 1000;
|
|
||||||
|
|
||||||
/** This service is only bound from inside the same process and never uses IPC. */
|
/** This service is only bound from inside the same process and never uses IPC. */
|
||||||
class LocalBinder extends Binder {
|
class LocalBinder extends Binder {
|
||||||
@@ -75,34 +76,27 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
|
|
||||||
private final Handler mHandler = new Handler();
|
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}
|
/** The full implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
||||||
* that holds activity references for activity related functions.
|
* that holds activity references for activity related functions.
|
||||||
* Note that the service may often outlive the activity, so need to clear this reference.
|
* Note that the service may often outlive the activity, so need to clear this reference.
|
||||||
*/
|
*/
|
||||||
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
private TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient;
|
||||||
|
|
||||||
/** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession}
|
/** 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. */
|
/** The wake lock and wifi lock are always acquired and released together. */
|
||||||
private PowerManager.WakeLock mWakeLock;
|
private PowerManager.WakeLock mWakeLock;
|
||||||
@@ -111,14 +105,21 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
||||||
boolean mWantsToStop = false;
|
boolean mWantsToStop = false;
|
||||||
|
|
||||||
public Integer mTerminalTranscriptRows;
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "TermuxService";
|
private static final String LOG_TAG = "TermuxService";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
Logger.logVerbose(LOG_TAG, "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();
|
runStartForeground();
|
||||||
|
|
||||||
|
SystemEventReceiver.registerPackageUpdateEvents(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Wakelock")
|
@SuppressLint("Wakelock")
|
||||||
@@ -129,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
|
// Run again in case service is already started and onCreate() is not called
|
||||||
runStartForeground();
|
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) {
|
if (action != null) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -169,6 +174,11 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
actionReleaseWakeLock(false);
|
actionReleaseWakeLock(false);
|
||||||
if (!mWantsToStop)
|
if (!mWantsToStop)
|
||||||
killAllTermuxExecutionCommands();
|
killAllTermuxExecutionCommands();
|
||||||
|
|
||||||
|
TermuxShellManager.onAppExit(this);
|
||||||
|
|
||||||
|
SystemEventReceiver.unregisterPackageUpdateEvents(this);
|
||||||
|
|
||||||
runStopForeground();
|
runStopForeground();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +195,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
// Since we cannot rely on {@link TermuxActivity.onDestroy()} to always complete,
|
// 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
|
// we unset clients here as well if it failed, so that we do not leave service and session
|
||||||
// clients with references to the activity.
|
// clients with references to the activity.
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
unsetTermuxTerminalSessionClient();
|
unsetTermuxTerminalSessionClient();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -253,28 +263,36 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
private synchronized void killAllTermuxExecutionCommands() {
|
private synchronized void killAllTermuxExecutionCommands() {
|
||||||
boolean processResult;
|
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++) {
|
for (int i = 0; i < termuxSessions.size(); i++) {
|
||||||
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
|
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
|
||||||
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
|
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||||
termuxSessions.get(i).killIfExecuting(this, processResult);
|
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++) {
|
for (int i = 0; i < termuxTasks.size(); i++) {
|
||||||
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
|
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
|
||||||
if (executionCommand.isPluginExecutionCommandWithPendingResult())
|
if (executionCommand.isPluginExecutionCommandWithPendingResult())
|
||||||
termuxTasks.get(i).killIfExecuting(this, true);
|
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++) {
|
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
|
||||||
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
|
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
|
||||||
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
|
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
|
||||||
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
|
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
|
||||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,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 = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase());
|
||||||
mWifiLock.acquire();
|
mWifiLock.acquire();
|
||||||
|
|
||||||
String packageName = getPackageName();
|
if (!PermissionUtils.checkIfBatteryOptimizationsDisabled(this)) {
|
||||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
PermissionUtils.requestDisableBatteryOptimizations(this);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNotification();
|
updateNotification();
|
||||||
@@ -354,26 +362,41 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecutionCommand executionCommand = new ExecutionCommand(getNextExecutionId());
|
ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId());
|
||||||
|
|
||||||
executionCommand.executableUri = intent.getData();
|
executionCommand.executableUri = intent.getData();
|
||||||
executionCommand.inBackground = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false);
|
executionCommand.isPluginExecutionCommand = true;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
if (executionCommand.executableUri != null) {
|
if (executionCommand.executableUri != null) {
|
||||||
executionCommand.executable = executionCommand.executableUri.getPath();
|
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);
|
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
|
||||||
if (executionCommand.inBackground)
|
if (Runner.APP_SHELL.equalsRunner(executionCommand.runner))
|
||||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
|
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.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
|
||||||
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
|
||||||
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
|
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
|
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.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
|
||||||
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, 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.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null);
|
||||||
executionCommand.isPluginExecutionCommand = true;
|
|
||||||
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
||||||
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||||
@@ -384,13 +407,20 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, 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
|
// 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);
|
executeTermuxTaskCommand(executionCommand);
|
||||||
} else {
|
else if (Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner))
|
||||||
executeTermuxSessionCommand(executionCommand);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,62 +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) {
|
private void executeTermuxTaskCommand(ExecutionCommand executionCommand) {
|
||||||
if (executionCommand == null) return;
|
if (executionCommand == null) return;
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "Executing background \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command");
|
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() + "\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a {@link TermuxTask}. */
|
if (newTermuxTask == null)
|
||||||
@Nullable
|
newTermuxTask = createTermuxTask(executionCommand);
|
||||||
public TermuxTask createTermuxTask(String executablePath, String[] arguments, String stdin, String workingDirectory) {
|
|
||||||
return createTermuxTask(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, true, false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a {@link TermuxTask}. */
|
/** Create a TermuxTask. */
|
||||||
@Nullable
|
@Nullable
|
||||||
public synchronized TermuxTask createTermuxTask(ExecutionCommand executionCommand) {
|
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 TermuxTask. */
|
||||||
|
@Nullable
|
||||||
|
public synchronized AppShell createTermuxTask(ExecutionCommand executionCommand) {
|
||||||
if (executionCommand == null) return null;
|
if (executionCommand == null) return null;
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
||||||
|
|
||||||
if (!executionCommand.inBackground) {
|
if (!Runner.APP_SHELL.equalsRunner(executionCommand.runner)) {
|
||||||
Logger.logDebug(LOG_TAG, "Ignoring a foreground execution command passed to createTermuxTask()");
|
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxTask()");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
executionCommand.setShellCommandShellEnvironment = true;
|
||||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
|
||||||
|
|
||||||
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false);
|
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
||||||
|
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
|
AppShell newTermuxTask = AppShell.execute(this, executionCommand, this,
|
||||||
|
new TermuxShellEnvironment(), null,false);
|
||||||
if (newTermuxTask == null) {
|
if (newTermuxTask == null) {
|
||||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
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 the execution command was started for a plugin, then process the error
|
||||||
if (executionCommand.isPluginExecutionCommand)
|
if (executionCommand.isPluginExecutionCommand)
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
else
|
else {
|
||||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs");
|
||||||
|
Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString());
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mTermuxTasks.add(newTermuxTask);
|
mShellManager.mTermuxTasks.add(newTermuxTask);
|
||||||
|
|
||||||
// Remove the execution command from the pending plugin execution commands list since it has
|
// Remove the execution command from the pending plugin execution commands list since it has
|
||||||
// now been processed
|
// now been processed
|
||||||
if (executionCommand.isPluginExecutionCommand)
|
if (executionCommand.isPluginExecutionCommand)
|
||||||
mPendingPluginExecutionCommands.remove(executionCommand);
|
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
|
||||||
|
|
||||||
updateNotification();
|
updateNotification();
|
||||||
|
|
||||||
return newTermuxTask;
|
return newTermuxTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Callback received when a {@link TermuxTask} finishes. */
|
/** Callback received when a TermuxTask finishes. */
|
||||||
@Override
|
@Override
|
||||||
public void onTermuxTaskExited(final TermuxTask termuxTask) {
|
public void onAppShellExited(final AppShell termuxTask) {
|
||||||
mHandler.post(() -> {
|
mHandler.post(() -> {
|
||||||
if (termuxTask != null) {
|
if (termuxTask != null) {
|
||||||
ExecutionCommand executionCommand = termuxTask.getExecutionCommand();
|
ExecutionCommand executionCommand = termuxTask.getExecutionCommand();
|
||||||
@@ -462,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 the execution command was started for a plugin, then process the results
|
||||||
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
|
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();
|
updateNotification();
|
||||||
@@ -481,14 +533,23 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "Executing foreground \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command");
|
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".
|
TermuxSession newTermuxSession = null;
|
||||||
if (executionCommand.executable != null) {
|
ShellCreateMode shellCreateMode = processShellCreateMode(executionCommand);
|
||||||
sessionName = ShellUtils.getExecutableBasename(executionCommand.executable).replace('-', ' ');
|
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;
|
if (newTermuxSession == null) return;
|
||||||
|
|
||||||
handleSessionAction(DataUtils.getIntFromString(executionCommand.sessionAction,
|
handleSessionAction(DataUtils.getIntFromString(executionCommand.sessionAction,
|
||||||
@@ -498,57 +559,68 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a {@link TermuxSession}.
|
* 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
|
@Nullable
|
||||||
public TermuxSession createTermuxSession(String executablePath, String[] arguments, String stdin, String workingDirectory, boolean isFailSafe, String sessionName) {
|
public TermuxSession createTermuxSession(String executablePath, String[] arguments, String stdin,
|
||||||
return createTermuxSession(new ExecutionCommand(getNextExecutionId(), executablePath, arguments, stdin, workingDirectory, false, isFailSafe), sessionName);
|
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}. */
|
/** Create a {@link TermuxSession}. */
|
||||||
@Nullable
|
@Nullable
|
||||||
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand, String sessionName) {
|
public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand) {
|
||||||
if (executionCommand == null) return null;
|
if (executionCommand == null) return null;
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||||
|
|
||||||
if (executionCommand.inBackground) {
|
if (!Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) {
|
||||||
Logger.logDebug(LOG_TAG, "Ignoring a background execution command passed to createTermuxSession()");
|
Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxSession()");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executionCommand.setShellCommandShellEnvironment = true;
|
||||||
|
executionCommand.terminalTranscriptRows = mProperties.getTerminalTranscriptRows();
|
||||||
|
|
||||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
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
|
// 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,
|
// Otherwise if command was manually started by the user like by adding a new terminal session,
|
||||||
// then no need to set stdout
|
// then no need to set stdout
|
||||||
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
|
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(),
|
||||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), sessionName, executionCommand.isPluginExecutionCommand);
|
this, new TermuxShellEnvironment(), null, executionCommand.isPluginExecutionCommand);
|
||||||
if (newTermuxSession == null) {
|
if (newTermuxSession == null) {
|
||||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
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 the execution command was started for a plugin, then process the error
|
||||||
if (executionCommand.isPluginExecutionCommand)
|
if (executionCommand.isPluginExecutionCommand)
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
else
|
else {
|
||||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs");
|
||||||
|
Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString());
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mTermuxSessions.add(newTermuxSession);
|
mShellManager.mTermuxSessions.add(newTermuxSession);
|
||||||
|
|
||||||
// Remove the execution command from the pending plugin execution commands list since it has
|
// Remove the execution command from the pending plugin execution commands list since it has
|
||||||
// now been processed
|
// now been processed
|
||||||
if (executionCommand.isPluginExecutionCommand)
|
if (executionCommand.isPluginExecutionCommand)
|
||||||
mPendingPluginExecutionCommands.remove(executionCommand);
|
mShellManager.mPendingPluginExecutionCommands.remove(executionCommand);
|
||||||
|
|
||||||
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
||||||
// activity in is foreground
|
// activity in is foreground
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
|
mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
|
||||||
|
|
||||||
updateNotification();
|
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;
|
return newTermuxSession;
|
||||||
}
|
}
|
||||||
@@ -558,7 +630,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
int index = getIndexOfSession(sessionToRemove);
|
int index = getIndexOfSession(sessionToRemove);
|
||||||
|
|
||||||
if (index >= 0)
|
if (index >= 0)
|
||||||
mTermuxSessions.get(index).finish();
|
mShellManager.mTermuxSessions.get(index).finish();
|
||||||
|
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
@@ -573,35 +645,40 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
|
|
||||||
// If the execution command was started for a plugin, then process the results
|
// If the execution command was started for a plugin, then process the results
|
||||||
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
|
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
|
// Notify {@link TermuxSessionsListViewController} that sessions list has been updated if
|
||||||
// activity in is foreground
|
// activity in is foreground
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated();
|
mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNotification();
|
updateNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the terminal transcript rows to be used for new {@link TermuxSession}. */
|
|
||||||
public Integer getTerminalTranscriptRows() {
|
|
||||||
if (mTerminalTranscriptRows == null)
|
|
||||||
setTerminalTranscriptRows();
|
|
||||||
return mTerminalTranscriptRows;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTerminalTranscriptRows() {
|
|
||||||
// TermuxService only uses this termux property currently, so no need to load them all into
|
|
||||||
// an internal values map like TermuxActivity does
|
|
||||||
mTerminalTranscriptRows = TermuxAppSharedProperties.getTerminalTranscriptRows(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Process session action for new session. */
|
/** Process session action for new session. */
|
||||||
private void handleSessionAction(int sessionAction, TerminalSession newTerminalSession) {
|
private void handleSessionAction(int sessionAction, TerminalSession newTerminalSession) {
|
||||||
@@ -610,8 +687,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
switch (sessionAction) {
|
switch (sessionAction) {
|
||||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY:
|
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY:
|
||||||
setCurrentStoredTerminalSession(newTerminalSession);
|
setCurrentStoredTerminalSession(newTerminalSession);
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
|
mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
|
||||||
startTermuxActivity();
|
startTermuxActivity();
|
||||||
break;
|
break;
|
||||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY:
|
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY:
|
||||||
@@ -621,8 +698,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
break;
|
break;
|
||||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY:
|
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY:
|
||||||
setCurrentStoredTerminalSession(newTerminalSession);
|
setCurrentStoredTerminalSession(newTerminalSession);
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
mTermuxTerminalSessionClient.setCurrentSession(newTerminalSession);
|
mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession);
|
||||||
break;
|
break;
|
||||||
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY:
|
case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY:
|
||||||
if (getTermuxSessionsSize() == 1)
|
if (getTermuxSessionsSize() == 1)
|
||||||
@@ -645,8 +722,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
} else {
|
} else {
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||||
if (preferences == null) return;
|
if (preferences == null) return;
|
||||||
if (preferences.arePluginErrorNotificationsEnabled())
|
if (preferences.arePluginErrorNotificationsEnabled(false))
|
||||||
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
|
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted_to_start_terminal), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,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
|
/** 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,
|
* 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
|
* callback is received, it should call {@link #setTermuxTerminalSessionClient} to set the
|
||||||
* {@link TermuxService#mTermuxTerminalSessionClient} so that further terminal sessions are directly
|
* {@link TermuxService#mTermuxTerminalSessionActivityClient} so that further terminal sessions are directly
|
||||||
* passed the {@link TermuxTerminalSessionClient} object which fully implements the
|
* passed the {@link TermuxTerminalSessionActivityClient} object which fully implements the
|
||||||
* {@link TerminalSessionClient} interface.
|
* {@link TerminalSessionClient} interface.
|
||||||
*
|
*
|
||||||
* @return Returns the {@link TermuxTerminalSessionClient} if {@link TermuxActivity} has bound with
|
* @return Returns the {@link TermuxTerminalSessionActivityClient} if {@link TermuxActivity} has bound with
|
||||||
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionClientBase}.
|
* {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}.
|
||||||
*/
|
*/
|
||||||
public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() {
|
public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() {
|
||||||
if (mTermuxTerminalSessionClient != null)
|
if (mTermuxTerminalSessionActivityClient != null)
|
||||||
return mTermuxTerminalSessionClient;
|
return mTermuxTerminalSessionActivityClient;
|
||||||
else
|
else
|
||||||
return mTermuxTerminalSessionClientBase;
|
return mTermuxTerminalSessionServiceClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
|
/** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the
|
||||||
* {@link TermuxService#mTermuxTerminalSessionClient} variable and update the {@link TerminalSession}
|
* {@link TermuxService#mTermuxTerminalSessionActivityClient} variable and update the {@link TerminalSession}
|
||||||
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionClientBase}
|
* and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionServiceClient}
|
||||||
* earlier.
|
* earlier.
|
||||||
*
|
*
|
||||||
* @param termuxTerminalSessionClient The {@link TermuxTerminalSessionClient} object that fully
|
* @param termuxTerminalSessionActivityClient The {@link TermuxTerminalSessionActivityClient} object that fully
|
||||||
* implements the {@link TerminalSessionClient} interface.
|
* implements the {@link TerminalSessionClient} interface.
|
||||||
*/
|
*/
|
||||||
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||||
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||||
|
|
||||||
for (int i = 0; i < mTermuxSessions.size(); i++)
|
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
|
||||||
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClient);
|
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
|
/** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)}
|
||||||
@@ -692,10 +769,10 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
* clients do not hold an activity references.
|
* clients do not hold an activity references.
|
||||||
*/
|
*/
|
||||||
public synchronized void unsetTermuxTerminalSessionClient() {
|
public synchronized void unsetTermuxTerminalSessionClient() {
|
||||||
for (int i = 0; i < mTermuxSessions.size(); i++)
|
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++)
|
||||||
mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionClientBase);
|
mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient);
|
||||||
|
|
||||||
mTermuxTerminalSessionClient = null;
|
mTermuxTerminalSessionActivityClient = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -707,12 +784,12 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
|
|
||||||
// Set pending intent to be launched when notification is clicked
|
// Set pending intent to be launched when notification is clicked
|
||||||
Intent notificationIntent = TermuxActivity.newInstance(this);
|
Intent notificationIntent = TermuxActivity.newInstance(this);
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
|
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
|
||||||
|
|
||||||
|
|
||||||
// Set notification text
|
// Set notification text
|
||||||
int sessionCount = getTermuxSessionsSize();
|
int sessionCount = getTermuxSessionsSize();
|
||||||
int taskCount = mTermuxTasks.size();
|
int taskCount = mShellManager.mTermuxTasks.size();
|
||||||
String notificationText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
|
String notificationText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
|
||||||
if (taskCount > 0) {
|
if (taskCount > 0) {
|
||||||
notificationText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
|
notificationText += ", " + taskCount + " task" + (taskCount == 1 ? "" : "s");
|
||||||
@@ -731,8 +808,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
// Build the notification
|
// Build the notification
|
||||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||||
TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority,
|
TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority,
|
||||||
getText(R.string.application_name), notificationText, null,
|
TermuxConstants.TERMUX_APP_NAME, notificationText, null,
|
||||||
pendingIntent, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||||
if (builder == null) return null;
|
if (builder == null) return null;
|
||||||
|
|
||||||
// No need to show a timestamp:
|
// No need to show a timestamp:
|
||||||
@@ -773,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. */
|
/** Update the shown foreground service notification after making any changes that affect it. */
|
||||||
private synchronized void updateNotification() {
|
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.
|
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
|
||||||
requestStopService();
|
requestStopService();
|
||||||
} else {
|
} else {
|
||||||
@@ -785,41 +862,55 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void setCurrentStoredTerminalSession(TerminalSession session) {
|
private void setCurrentStoredTerminalSession(TerminalSession terminalSession) {
|
||||||
if (session == null) return;
|
if (terminalSession == null) return;
|
||||||
// Make the newly created session the current one to be displayed
|
// Make the newly created session the current one to be displayed
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||||
if (preferences == null) return;
|
if (preferences == null) return;
|
||||||
preferences.setCurrentSession(session.mHandle);
|
preferences.setCurrentSession(terminalSession.mHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized boolean isTermuxSessionsEmpty() {
|
public synchronized boolean isTermuxSessionsEmpty() {
|
||||||
return mTermuxSessions.isEmpty();
|
return mShellManager.mTermuxSessions.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized int getTermuxSessionsSize() {
|
public synchronized int getTermuxSessionsSize() {
|
||||||
return mTermuxSessions.size();
|
return mShellManager.mTermuxSessions.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized List<TermuxSession> getTermuxSessions() {
|
public synchronized List<TermuxSession> getTermuxSessions() {
|
||||||
return mTermuxSessions;
|
return mShellManager.mTermuxSessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public synchronized TermuxSession getTermuxSession(int index) {
|
public synchronized TermuxSession getTermuxSession(int index) {
|
||||||
if (index >= 0 && index < mTermuxSessions.size())
|
if (index >= 0 && index < mShellManager.mTermuxSessions.size())
|
||||||
return mTermuxSessions.get(index);
|
return mShellManager.mTermuxSessions.get(index);
|
||||||
else
|
else
|
||||||
return null;
|
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() {
|
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) {
|
public synchronized int getIndexOfSession(TerminalSession terminalSession) {
|
||||||
for (int i = 0; i < mTermuxSessions.size(); i++) {
|
if (terminalSession == null) return -1;
|
||||||
if (mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
|
||||||
|
for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) {
|
||||||
|
if (mShellManager.mTermuxSessions.get(i).getTerminalSession().equals(terminalSession))
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
@@ -827,19 +918,39 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
|
|
||||||
public synchronized TerminalSession getTerminalSessionForHandle(String sessionHandle) {
|
public synchronized TerminalSession getTerminalSessionForHandle(String sessionHandle) {
|
||||||
TerminalSession terminalSession;
|
TerminalSession terminalSession;
|
||||||
for (int i = 0, len = mTermuxSessions.size(); i < len; i++) {
|
for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) {
|
||||||
terminalSession = mTermuxSessions.get(i).getTerminalSession();
|
terminalSession = mShellManager.mTermuxSessions.get(i).getTerminalSession();
|
||||||
if (terminalSession.mHandle.equals(sessionHandle))
|
if (terminalSession.mHandle.equals(sessionHandle))
|
||||||
return terminalSession;
|
return terminalSession;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized AppShell getTermuxTaskForShellName(String name) {
|
||||||
|
if (DataUtils.isNullOrEmpty(name)) return null;
|
||||||
public static synchronized int getNextExecutionId() {
|
AppShell appShell;
|
||||||
return EXECUTION_ID++;
|
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() {
|
public boolean wantsToStop() {
|
||||||
return mWantsToStop;
|
return mWantsToStop;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.termux.app.activities;
|
package com.termux.app.activities;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@@ -12,10 +11,12 @@ import android.webkit.WebViewClient;
|
|||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
/** Basic embedded browser for viewing help pages. */
|
/** Basic embedded browser for viewing help pages. */
|
||||||
public final class HelpActivity extends Activity {
|
public final class HelpActivity extends AppCompatActivity {
|
||||||
|
|
||||||
WebView mWebView;
|
WebView mWebView;
|
||||||
|
|
||||||
|
|||||||
@@ -2,29 +2,38 @@ package com.termux.app.activities;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.activities.ReportActivity;
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
import com.termux.shared.models.ReportInfo;
|
import com.termux.shared.models.ReportInfo;
|
||||||
import com.termux.app.models.UserAction;
|
import com.termux.app.models.UserAction;
|
||||||
import com.termux.shared.interact.ShareUtils;
|
import com.termux.shared.interact.ShareUtils;
|
||||||
import com.termux.shared.packages.PackageUtils;
|
import com.termux.shared.android.PackageUtils;
|
||||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences;
|
||||||
import com.termux.shared.termux.AndroidUtils;
|
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.TermuxConstants;
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
import com.termux.shared.activity.media.AppCompatActivityUtils;
|
||||||
|
import com.termux.shared.theme.NightMode;
|
||||||
|
|
||||||
public class SettingsActivity extends AppCompatActivity {
|
public class SettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true);
|
||||||
|
|
||||||
setContentView(R.layout.activity_settings);
|
setContentView(R.layout.activity_settings);
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
getSupportFragmentManager()
|
getSupportFragmentManager()
|
||||||
@@ -32,11 +41,9 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
.replace(R.id.settings, new RootPreferencesFragment())
|
.replace(R.id.settings, new RootPreferencesFragment())
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
AppCompatActivityUtils.setToolbar(this, com.termux.shared.R.id.toolbar);
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
AppCompatActivityUtils.setShowBackButtonInActionBar(this, true);
|
||||||
actionBar.setDisplayShowHomeEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -53,17 +60,52 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
configureTermuxAPIPreference(context);
|
||||||
|
configureTermuxFloatPreference(context);
|
||||||
configureTermuxTaskerPreference(context);
|
configureTermuxTaskerPreference(context);
|
||||||
|
configureTermuxWidgetPreference(context);
|
||||||
configureAboutPreference(context);
|
configureAboutPreference(context);
|
||||||
configureDonatePreference(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) {
|
private void configureTermuxTaskerPreference(@NonNull Context context) {
|
||||||
Preference termuxTaskerPrefernce = findPreference("termux_tasker");
|
Preference termuxTaskerPreference = findPreference("termux_tasker");
|
||||||
if (termuxTaskerPrefernce != null) {
|
if (termuxTaskerPreference != null) {
|
||||||
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
|
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
|
||||||
// If failed to get app preferences, then likely app is not installed, so do not show its preference
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,16 +119,20 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
String title = "About";
|
String title = "About";
|
||||||
|
|
||||||
StringBuilder aboutString = new StringBuilder();
|
StringBuilder aboutString = new StringBuilder();
|
||||||
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, false));
|
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES));
|
||||||
|
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context, true));
|
||||||
String termuxPluginAppsInfo = TermuxUtils.getTermuxPluginAppsInfoMarkdownString(context);
|
|
||||||
if (termuxPluginAppsInfo != null)
|
|
||||||
aboutString.append("\n\n").append(termuxPluginAppsInfo);
|
|
||||||
|
|
||||||
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
|
||||||
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||||
|
|
||||||
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
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();
|
}.start();
|
||||||
|
|
||||||
@@ -113,7 +159,7 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
donatePreference.setOnPreferenceClickListener(preference -> {
|
donatePreference.setOnPreferenceClickListener(preference -> {
|
||||||
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
|
ShareUtils.openUrl(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||||
return true;
|
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 androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
|
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat {
|
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 androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.termux.R;
|
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;
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@@ -144,9 +144,9 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
|||||||
case "terminal_view_key_logging_enabled":
|
case "terminal_view_key_logging_enabled":
|
||||||
return mPreferences.isTerminalViewKeyLoggingEnabled();
|
return mPreferences.isTerminalViewKeyLoggingEnabled();
|
||||||
case "plugin_error_notifications_enabled":
|
case "plugin_error_notifications_enabled":
|
||||||
return mPreferences.arePluginErrorNotificationsEnabled();
|
return mPreferences.arePluginErrorNotificationsEnabled(false);
|
||||||
case "crash_report_notifications_enabled":
|
case "crash_report_notifications_enabled":
|
||||||
return mPreferences.areCrashReportNotificationsEnabled();
|
return mPreferences.areCrashReportNotificationsEnabled(false);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import androidx.preference.PreferenceFragmentCompat;
|
|||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
|
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 androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
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 {
|
public enum UserAction {
|
||||||
|
|
||||||
ABOUT("about"),
|
ABOUT("about"),
|
||||||
CRASH_REPORT("crash report"),
|
|
||||||
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
|
|
||||||
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|||||||
@@ -1,107 +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.TermuxPropertyConstants;
|
|
||||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
public class TermuxAppSharedProperties extends TermuxSharedProperties {
|
|
||||||
|
|
||||||
private ExtraKeysInfo mExtraKeysInfo;
|
|
||||||
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "TermuxAppSharedProperties";
|
|
||||||
|
|
||||||
public TermuxAppSharedProperties(@Nonnull Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload the termux properties from disk into an in-memory cache.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void loadTermuxPropertiesFromDisk() {
|
|
||||||
super.loadTermuxPropertiesFromDisk();
|
|
||||||
|
|
||||||
setExtraKeys();
|
|
||||||
setSessionShortcuts();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the terminal extra keys and style.
|
|
||||||
*/
|
|
||||||
private void setExtraKeys() {
|
|
||||||
mExtraKeysInfo = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// The mMap stores the extra key and style string values while loading properties
|
|
||||||
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
|
|
||||||
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
|
||||||
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
|
||||||
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
|
||||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
|
||||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
|
||||||
|
|
||||||
try {
|
|
||||||
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
|
||||||
} catch (JSONException e2) {
|
|
||||||
Logger.showToast(mContext, "Can't create default extra keys",true);
|
|
||||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
|
||||||
mExtraKeysInfo = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the terminal sessions shortcuts.
|
|
||||||
*/
|
|
||||||
private void setSessionShortcuts() {
|
|
||||||
if (mSessionShortcuts == null)
|
|
||||||
mSessionShortcuts = new ArrayList<>();
|
|
||||||
else
|
|
||||||
mSessionShortcuts.clear();
|
|
||||||
|
|
||||||
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
|
|
||||||
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
|
|
||||||
// The mMap stores the code points for the session shortcuts while loading properties
|
|
||||||
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
|
|
||||||
// If codePoint is null, then session shortcut did not exist in properties or was invalid
|
|
||||||
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
|
|
||||||
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
|
|
||||||
// add the code point to sessionShortcuts
|
|
||||||
if (codePoint != null)
|
|
||||||
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<KeyboardShortcut> getSessionShortcuts() {
|
|
||||||
return mSessionShortcuts;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExtraKeysInfo getExtraKeysInfo() {
|
|
||||||
return mExtraKeysInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk.
|
|
||||||
*/
|
|
||||||
public static int getTerminalTranscriptRows(Context context) {
|
|
||||||
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,9 @@ import androidx.core.content.ContextCompat;
|
|||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxActivity;
|
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 com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -55,9 +57,9 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
|
|||||||
return sessionRowView;
|
return sessionRowView;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
boolean shouldEnableDarkTheme = ThemeUtils.shouldEnableDarkTheme(mActivity, NightMode.getAppNightMode().getName());
|
||||||
|
|
||||||
if (isUsingBlackUI) {
|
if (shouldEnableDarkTheme) {
|
||||||
sessionTitleView.setBackground(
|
sessionTitleView.setBackground(
|
||||||
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||||
);
|
);
|
||||||
@@ -84,7 +86,7 @@ public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession
|
|||||||
} else {
|
} else {
|
||||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
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;
|
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||||
sessionTitleView.setTextColor(color);
|
sessionTitleView.setTextColor(color);
|
||||||
return sessionRowView;
|
return sessionRowView;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.termux.app.terminal;
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
@@ -12,18 +13,23 @@ import android.media.SoundPool;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.shell.TermuxSession;
|
import com.termux.shared.interact.ShareUtils;
|
||||||
import com.termux.shared.interact.TextInputDialogUtils;
|
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.app.TermuxActivity;
|
||||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.app.TermuxService;
|
||||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||||
import com.termux.app.terminal.io.BellHandler;
|
import com.termux.shared.termux.terminal.io.BellHandler;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.terminal.TerminalColors;
|
import com.termux.terminal.TerminalColors;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
|
import com.termux.terminal.TerminalSessionClient;
|
||||||
import com.termux.terminal.TextStyle;
|
import com.termux.terminal.TextStyle;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -31,7 +37,8 @@ import java.io.FileInputStream;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Properties;
|
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;
|
private final TermuxActivity mActivity;
|
||||||
|
|
||||||
@@ -41,9 +48,9 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
|
|
||||||
private int mBellSoundId;
|
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;
|
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
|
// 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
|
// 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
|
// 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
|
* Should be called when mActivity.reloadActivityStyling() is called
|
||||||
*/
|
*/
|
||||||
public void onReload() {
|
public void onReloadActivityStyling() {
|
||||||
// Set terminal fonts and colors
|
// Set terminal fonts and colors
|
||||||
checkForFontAndColors();
|
checkForFontAndColors();
|
||||||
}
|
}
|
||||||
@@ -108,14 +115,14 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTextChanged(TerminalSession changedSession) {
|
public void onTextChanged(@NonNull TerminalSession changedSession) {
|
||||||
if (!mActivity.isVisible()) return;
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTitleChanged(TerminalSession updatedSession) {
|
public void onTitleChanged(@NonNull TerminalSession updatedSession) {
|
||||||
if (!mActivity.isVisible()) return;
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
if (updatedSession != mActivity.getCurrentSession()) {
|
if (updatedSession != mActivity.getCurrentSession()) {
|
||||||
@@ -129,7 +136,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
public void onSessionFinished(@NonNull TerminalSession finishedSession) {
|
||||||
TermuxService service = mActivity.getTermuxService();
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
|
||||||
if (service == null || service.wantsToStop()) {
|
if (service == null || service.wantsToStop()) {
|
||||||
@@ -138,39 +145,59 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
return;
|
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()) {
|
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
|
||||||
// Show toast for non-current sessions that exit.
|
// 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:
|
// 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);
|
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
// On Android TV devices we need to use older behaviour because we may
|
// On Android TV devices we need to use older behaviour because we may
|
||||||
// not be able to have multiple launcher icons.
|
// not be able to have multiple launcher icons.
|
||||||
if (service.getTermuxSessionsSize() > 1) {
|
if (service.getTermuxSessionsSize() > 1 || isPluginExecutionCommandWithPendingResult) {
|
||||||
removeFinishedSession(finishedSession);
|
removeFinishedSession(finishedSession);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Once we have a separate launcher icon for the failsafe session, it
|
// 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'.
|
// 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);
|
removeFinishedSession(finishedSession);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClipboardText(TerminalSession session, String text) {
|
public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) {
|
||||||
if (!mActivity.isVisible()) return;
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
ShareUtils.copyTextToClipboard(mActivity, text);
|
||||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
switch (mActivity.getProperties().getBellBehaviour()) {
|
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||||
@@ -178,7 +205,9 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
BellHandler.getInstance(mActivity).doBell();
|
BellHandler.getInstance(mActivity).doBell();
|
||||||
break;
|
break;
|
||||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
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;
|
break;
|
||||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||||
// Ignore the bell character.
|
// Ignore the bell character.
|
||||||
@@ -187,7 +216,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onColorsChanged(TerminalSession changedSession) {
|
public void onColorsChanged(@NonNull TerminalSession changedSession) {
|
||||||
if (mActivity.getCurrentSession() == changedSession)
|
if (mActivity.getCurrentSession() == changedSession)
|
||||||
updateBackgroundColor();
|
updateBackgroundColor();
|
||||||
}
|
}
|
||||||
@@ -205,6 +234,17 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be called when mActivity.onResetTerminalSession() is called
|
* Should be called when mActivity.onResetTerminalSession() is called
|
||||||
*/
|
*/
|
||||||
@@ -223,17 +263,20 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Initialize and get mBellSoundPool */
|
/** Load mBellSoundPool */
|
||||||
private synchronized SoundPool getBellSoundPool() {
|
private synchronized void loadBellSoundPool() {
|
||||||
if (mBellSoundPool == null) {
|
if (mBellSoundPool == null) {
|
||||||
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||||
|
|
||||||
|
try {
|
||||||
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
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 */
|
/** Release mBellSoundPool resources */
|
||||||
@@ -302,11 +345,22 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
if (sessionToRename == null) return;
|
if (sessionToRename == null) return;
|
||||||
|
|
||||||
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||||
sessionToRename.mSessionName = text;
|
renameSession(sessionToRename, text);
|
||||||
termuxSessionListNotifyUpdated();
|
termuxSessionListNotifyUpdated();
|
||||||
}, -1, null, -1, null, null);
|
}, -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) {
|
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||||
TermuxService service = mActivity.getTermuxService();
|
TermuxService service = mActivity.getTermuxService();
|
||||||
if (service == null) return;
|
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,13 +2,11 @@ package com.termux.app.terminal;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.Uri;
|
import android.os.Environment;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
@@ -21,31 +19,37 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxActivity;
|
import com.termux.app.TermuxActivity;
|
||||||
import com.termux.shared.data.UrlUtils;
|
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.shell.ShellUtils;
|
||||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
import com.termux.shared.termux.TermuxBootstrap;
|
||||||
import com.termux.shared.termux.AndroidUtils;
|
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.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.activities.ReportActivity;
|
import com.termux.shared.activities.ReportActivity;
|
||||||
import com.termux.shared.models.ReportInfo;
|
import com.termux.shared.models.ReportInfo;
|
||||||
import com.termux.app.models.UserAction;
|
import com.termux.app.models.UserAction;
|
||||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
import com.termux.shared.termux.settings.properties.TermuxPropertyConstants;
|
||||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.markdown.MarkdownUtils;
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
import com.termux.shared.termux.data.TermuxUrlUtils;
|
||||||
import com.termux.shared.view.KeyboardUtils;
|
import com.termux.shared.view.KeyboardUtils;
|
||||||
import com.termux.shared.view.ViewUtils;
|
import com.termux.shared.view.ViewUtils;
|
||||||
import com.termux.terminal.KeyHandler;
|
import com.termux.terminal.KeyHandler;
|
||||||
import com.termux.terminal.TerminalEmulator;
|
import com.termux.terminal.TerminalEmulator;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
|
||||||
@@ -53,7 +57,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
final TermuxActivity mActivity;
|
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. */
|
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||||
@@ -65,17 +69,25 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
||||||
|
|
||||||
|
private List<KeyboardShortcut> mSessionShortcuts;
|
||||||
|
|
||||||
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
||||||
|
|
||||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) {
|
||||||
this.mActivity = activity;
|
this.mActivity = activity;
|
||||||
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
this.mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TermuxActivity getActivity() {
|
||||||
|
return mActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be called when mActivity.onCreate() is called
|
* Should be called when mActivity.onCreate() is called
|
||||||
*/
|
*/
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
onReloadProperties();
|
||||||
|
|
||||||
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||||
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
|
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
|
||||||
}
|
}
|
||||||
@@ -99,7 +111,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
*/
|
*/
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
// Show the soft keyboard if required
|
// Show the soft keyboard if required
|
||||||
setSoftKeyboardState(true, false);
|
setSoftKeyboardState(true, mActivity.isActivityRecreated());
|
||||||
|
|
||||||
mTerminalCursorBlinkerStateAlreadySet = false;
|
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||||
|
|
||||||
@@ -121,10 +133,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
setTerminalCursorBlinkerState(false);
|
setTerminalCursorBlinkerState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.reloadProperties() is called
|
||||||
|
*/
|
||||||
|
public void onReloadProperties() {
|
||||||
|
setSessionShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be called when mActivity.reloadActivityStyling() is called
|
* Should be called when mActivity.reloadActivityStyling() is called
|
||||||
*/
|
*/
|
||||||
public void onReload() {
|
public void onReloadActivityStyling() {
|
||||||
// Show the soft keyboard if required
|
// Show the soft keyboard if required
|
||||||
setSoftKeyboardState(false, true);
|
setSoftKeyboardState(false, true);
|
||||||
|
|
||||||
@@ -133,7 +152,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be called when {@link com.termux.view.TerminalView#mEmulator}
|
* Should be called when {@link com.termux.view.TerminalView#mEmulator} is set
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onEmulatorSet() {
|
public void onEmulatorSet() {
|
||||||
@@ -165,11 +184,27 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSingleTapUp(MotionEvent e) {
|
public void onSingleTapUp(MotionEvent e) {
|
||||||
|
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))
|
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
|
||||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
else
|
else
|
||||||
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
|
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldBackButtonBeMappedToEscape() {
|
public boolean shouldBackButtonBeMappedToEscape() {
|
||||||
@@ -186,6 +221,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
|
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminalViewSelected() {
|
||||||
|
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -202,16 +242,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||||
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||||
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
mTermuxTerminalSessionActivityClient.removeFinishedSession(currentSession);
|
||||||
return true;
|
return true;
|
||||||
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
} else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
|
||||||
|
e.isCtrlPressed() && e.isAltPressed()) {
|
||||||
// Get the unmodified code point:
|
// Get the unmodified code point:
|
||||||
int unicodeChar = e.getUnicodeChar(0);
|
int unicodeChar = e.getUnicodeChar(0);
|
||||||
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
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 */) {
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||||
mTermuxTerminalSessionClient.switchToSession(false);
|
mTermuxTerminalSessionActivityClient.switchToSession(false);
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||||
@@ -221,9 +262,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
} else if (unicodeChar == 'm'/* menu */) {
|
} else if (unicodeChar == 'm'/* menu */) {
|
||||||
mActivity.getTerminalView().showContextMenu();
|
mActivity.getTerminalView().showContextMenu();
|
||||||
} else if (unicodeChar == 'r'/* rename */) {
|
} else if (unicodeChar == 'r'/* rename */) {
|
||||||
mTermuxTerminalSessionClient.renameSession(currentSession);
|
mTermuxTerminalSessionActivityClient.renameSession(currentSession);
|
||||||
} else if (unicodeChar == 'c'/* create */) {
|
} else if (unicodeChar == 'c'/* create */) {
|
||||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
|
||||||
} else if (unicodeChar == 'u' /* urls */) {
|
} else if (unicodeChar == 'u' /* urls */) {
|
||||||
showUrlSelection();
|
showUrlSelection();
|
||||||
} else if (unicodeChar == 'v') {
|
} else if (unicodeChar == 'v') {
|
||||||
@@ -236,7 +277,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
changeFontSize(false);
|
changeFontSize(false);
|
||||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||||
int index = unicodeChar - '1';
|
int index = unicodeChar - '1';
|
||||||
mTermuxTerminalSessionClient.switchToSession(index);
|
mTermuxTerminalSessionActivityClient.switchToSession(index);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -281,12 +322,32 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean readControlKey() {
|
public boolean readControlKey() {
|
||||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean readAltKey() {
|
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
|
@Override
|
||||||
@@ -400,11 +461,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
return true;
|
return true;
|
||||||
} else if (ctrlDown) {
|
} else if (ctrlDown) {
|
||||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||||
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
mTermuxTerminalSessionActivityClient.removeFinishedSession(session);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
List<KeyboardShortcut> shortcuts = mSessionShortcuts;
|
||||||
if (shortcuts != null && !shortcuts.isEmpty()) {
|
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||||
int codePointLowerCase = Character.toLowerCase(codePoint);
|
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||||
@@ -412,16 +473,16 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
if (codePointLowerCase == shortcut.codePoint) {
|
if (codePointLowerCase == shortcut.codePoint) {
|
||||||
switch (shortcut.shortcutAction) {
|
switch (shortcut.shortcutAction) {
|
||||||
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
mTermuxTerminalSessionActivityClient.addNewSession(false, null);
|
||||||
return true;
|
return true;
|
||||||
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||||
mTermuxTerminalSessionClient.switchToSession(true);
|
mTermuxTerminalSessionActivityClient.switchToSession(true);
|
||||||
return true;
|
return true;
|
||||||
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||||
mTermuxTerminalSessionClient.switchToSession(false);
|
mTermuxTerminalSessionActivityClient.switchToSession(false);
|
||||||
return true;
|
return true;
|
||||||
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||||
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
mTermuxTerminalSessionActivityClient.renameSession(mActivity.getCurrentSession());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,6 +493,27 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
return false;
|
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) {
|
public void changeFontSize(boolean increase) {
|
||||||
@@ -484,7 +566,14 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
||||||
boolean noRequestFocus = false;
|
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 soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
||||||
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
||||||
@@ -492,7 +581,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
||||||
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
||||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
noRequestFocus = true;
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
noShowKeyboard = true;
|
||||||
// Delay is only required if onCreate() is called like when Termux app is exited with
|
// 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
|
// double back press, not when Termux app is switched back from another app and keyboard
|
||||||
// toggle is pressed to enable keyboard
|
// toggle is pressed to enable keyboard
|
||||||
@@ -508,10 +598,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
// If soft keyboard is to be hidden on startup
|
// If soft keyboard is to be hidden on startup
|
||||||
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
|
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
|
||||||
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
|
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
|
// Required to keep keyboard hidden when Termux app is switched back from another app
|
||||||
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
||||||
noRequestFocus = true;
|
|
||||||
|
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
noShowKeyboard = true;
|
||||||
// Required to keep keyboard hidden on app startup
|
// Required to keep keyboard hidden on app startup
|
||||||
mShowSoftKeyboardIgnoreOnce = true;
|
mShowSoftKeyboardIgnoreOnce = true;
|
||||||
}
|
}
|
||||||
@@ -541,7 +633,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
// 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
|
// or soft keyboard is to be hidden or is disabled
|
||||||
if (!isReloadTermuxProperties && !noRequestFocus) {
|
if (!isReloadTermuxProperties && !noShowKeyboard) {
|
||||||
// Request focus for TerminalView
|
// Request focus for TerminalView
|
||||||
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
|
// 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
|
// had focus on startup to show the keyboard, like when opening url with context menu
|
||||||
@@ -586,17 +678,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||||
if (transcriptText == null) return;
|
if (transcriptText == null) return;
|
||||||
|
|
||||||
try {
|
|
||||||
// See https://github.com/termux/termux-app/issues/1166.
|
// 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();
|
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript),
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
transcriptText, mActivity.getString(R.string.title_share_transcript_with));
|
||||||
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void 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() {
|
public void showUrlSelection() {
|
||||||
@@ -605,7 +697,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||||
|
|
||||||
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
|
LinkedHashSet<CharSequence> urlSet = TermuxUrlUtils.extractUrls(text);
|
||||||
if (urlSet.isEmpty()) {
|
if (urlSet.isEmpty()) {
|
||||||
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||||
return;
|
return;
|
||||||
@@ -617,9 +709,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
// Click to copy url to clipboard:
|
// Click to copy url to clipboard:
|
||||||
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||||
String url = (String) urls[which];
|
String url = (String) urls[which];
|
||||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard));
|
||||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
|
||||||
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
|
||||||
}).setTitle(R.string.title_select_url_dialog).create();
|
}).setTitle(R.string.title_select_url_dialog).create();
|
||||||
|
|
||||||
// Long press to open URL:
|
// Long press to open URL:
|
||||||
@@ -628,13 +718,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
String url = (String) urls[position];
|
String url = (String) urls[position];
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
ShareUtils.openUrl(mActivity, url);
|
||||||
try {
|
|
||||||
mActivity.startActivity(i, null);
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
// If no applications match, Android displays a system message.
|
|
||||||
mActivity.startActivity(Intent.createChooser(i, null));
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -649,29 +733,58 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||||
if (transcriptText == null) return;
|
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);
|
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
|
||||||
|
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
String transcriptTextTruncated = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
|
||||||
|
|
||||||
StringBuilder reportString = new StringBuilder();
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
||||||
|
|
||||||
reportString.append("## Transcript\n");
|
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));
|
if (addTermuxDebugInfo) {
|
||||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity, true));
|
||||||
|
|
||||||
|
if (TermuxBootstrap.isAppPackageManagerAPT()) {
|
||||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||||
if (termuxAptInfo != null)
|
if (termuxAptInfo != null)
|
||||||
reportString.append("\n\n").append(termuxAptInfo);
|
reportString.append("\n\n").append(termuxAptInfo);
|
||||||
|
}
|
||||||
|
|
||||||
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(), TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
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();
|
}.start();
|
||||||
}
|
}
|
||||||
@@ -681,12 +794,9 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
if (session == null) return;
|
if (session == null) return;
|
||||||
if (!session.isRunning()) return;
|
if (!session.isRunning()) return;
|
||||||
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true);
|
||||||
ClipData clipData = clipboard.getPrimaryClip();
|
if (text != null)
|
||||||
if (clipData == null) return;
|
session.getEmulator().paste(text);
|
||||||
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
|
||||||
if (!TextUtils.isEmpty(paste))
|
|
||||||
session.getEmulator().paste(paste.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import androidx.viewpager.widget.ViewPager;
|
|||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxActivity;
|
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;
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
public class TerminalToolbarViewPager {
|
public class TerminalToolbarViewPager {
|
||||||
@@ -44,9 +44,11 @@ public class TerminalToolbarViewPager {
|
|||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
||||||
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||||
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
|
extraKeysView.setExtraKeysViewClient(mActivity.getTermuxTerminalExtraKeys());
|
||||||
|
extraKeysView.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps());
|
||||||
mActivity.setExtraKeysView(extraKeysView);
|
mActivity.setExtraKeysView(extraKeysView);
|
||||||
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
extraKeysView.reload(mActivity.getTermuxTerminalExtraKeys().getExtraKeysInfo(),
|
||||||
|
mActivity.getTerminalToolbarDefaultHeight());
|
||||||
|
|
||||||
// apply extra keys fix if enabled in prefs
|
// apply extra keys fix if enabled in prefs
|
||||||
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
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,188 +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.shared.activities.ReportActivity;
|
|
||||||
import com.termux.shared.models.errors.Error;
|
|
||||||
import com.termux.shared.notification.NotificationUtils;
|
|
||||||
import com.termux.shared.file.FileUtils;
|
|
||||||
import com.termux.shared.models.ReportInfo;
|
|
||||||
import com.termux.app.models.UserAction;
|
|
||||||
import com.termux.shared.notification.TermuxNotificationUtils;
|
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
|
||||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
|
||||||
import com.termux.shared.data.DataUtils;
|
|
||||||
import com.termux.shared.logger.Logger;
|
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
|
||||||
|
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
public class CrashUtils {
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "CrashUtils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify the user of an app crash at last run by reading the crash info from the crash log file
|
|
||||||
* at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
|
||||||
* created by {@link com.termux.shared.crash.CrashHandler}.
|
|
||||||
*
|
|
||||||
* If the crash log file exists and is not empty and
|
|
||||||
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
|
|
||||||
* enabled, then a notification will be shown for the crash on the
|
|
||||||
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
|
|
||||||
*
|
|
||||||
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
* @param logTagParam The log tag to use for logging.
|
|
||||||
*/
|
|
||||||
public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
|
|
||||||
if (context == null) return;
|
|
||||||
|
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
|
||||||
if (preferences == null) return;
|
|
||||||
|
|
||||||
// If user has disabled notifications for crashes
|
|
||||||
if (!preferences.areCrashReportNotificationsEnabled())
|
|
||||||
return;
|
|
||||||
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
|
|
||||||
|
|
||||||
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Error error;
|
|
||||||
StringBuilder reportStringBuilder = new StringBuilder();
|
|
||||||
|
|
||||||
// Read report string from crash log file
|
|
||||||
error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
|
||||||
if (error != null) {
|
|
||||||
Logger.logErrorExtended(logTag, error.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move crash log file to backup location if it exists
|
|
||||||
error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
|
||||||
if (error != null) {
|
|
||||||
Logger.logErrorExtended(logTag, error.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
String reportString = reportStringBuilder.toString();
|
|
||||||
|
|
||||||
if (reportString.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
|
|
||||||
|
|
||||||
sendCrashReportNotification(context, logTag, reportString, false);
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
|
||||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
* @param logTag The log tag to use for logging.
|
|
||||||
* @param reportString The text for the crash report.
|
|
||||||
* @param forceNotification If set to {@code true}, then a notification will be shown
|
|
||||||
* regardless of if pending intent is {@code null} or
|
|
||||||
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
|
|
||||||
* is {@code false}.
|
|
||||||
*/
|
|
||||||
public static void sendCrashReportNotification(final Context context, String logTag, String reportString, boolean forceNotification) {
|
|
||||||
if (context == null) return;
|
|
||||||
|
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
|
||||||
if (preferences == null) return;
|
|
||||||
|
|
||||||
// If user has disabled notifications for crashes
|
|
||||||
if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification)
|
|
||||||
return;
|
|
||||||
|
|
||||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
|
||||||
|
|
||||||
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
|
||||||
// to show the details of the crash
|
|
||||||
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
|
||||||
|
|
||||||
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
|
|
||||||
|
|
||||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
// Setup the notification channel if not already set up
|
|
||||||
setupCrashReportsNotificationChannel(context);
|
|
||||||
|
|
||||||
// Build the notification
|
|
||||||
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
|
||||||
if (builder == null) return;
|
|
||||||
|
|
||||||
// Send the notification
|
|
||||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
|
||||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
|
||||||
if (notificationManager != null)
|
|
||||||
notificationManager.notify(nextNotificationId, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
|
||||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
* @param title The title for the notification.
|
|
||||||
* @param notificationText The second line text of the notification.
|
|
||||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
|
||||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
|
||||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
|
||||||
* @return Returns the {@link Notification.Builder}.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
|
||||||
|
|
||||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
|
||||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
|
||||||
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
|
||||||
|
|
||||||
if (builder == null) return null;
|
|
||||||
|
|
||||||
// Enable timestamp
|
|
||||||
builder.setShowWhen(true);
|
|
||||||
|
|
||||||
// Set notification icon
|
|
||||||
builder.setSmallIcon(R.drawable.ic_error_notification);
|
|
||||||
|
|
||||||
// Set background color for small notification icon
|
|
||||||
builder.setColor(0xFF607D8B);
|
|
||||||
|
|
||||||
// Dismiss on click
|
|
||||||
builder.setAutoCancel(true);
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup the notification channel for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} and
|
|
||||||
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
*/
|
|
||||||
public static void setupCrashReportsNotificationChannel(final Context context) {
|
|
||||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
|
|
||||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,307 +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.shared.activities.ReportActivity;
|
|
||||||
import com.termux.shared.file.TermuxFileUtils;
|
|
||||||
import com.termux.shared.models.ResultConfig;
|
|
||||||
import com.termux.shared.models.ResultData;
|
|
||||||
import com.termux.shared.models.errors.Errno;
|
|
||||||
import com.termux.shared.models.errors.Error;
|
|
||||||
import com.termux.shared.notification.NotificationUtils;
|
|
||||||
import com.termux.shared.notification.TermuxNotificationUtils;
|
|
||||||
import com.termux.shared.shell.ResultSender;
|
|
||||||
import com.termux.shared.shell.ShellUtils;
|
|
||||||
import com.termux.shared.termux.AndroidUtils;
|
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
|
||||||
import com.termux.shared.logger.Logger;
|
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
|
||||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
|
||||||
import com.termux.shared.settings.properties.SharedProperties;
|
|
||||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
|
||||||
import com.termux.shared.models.ReportInfo;
|
|
||||||
import com.termux.shared.models.ExecutionCommand;
|
|
||||||
import com.termux.app.models.UserAction;
|
|
||||||
import com.termux.shared.data.DataUtils;
|
|
||||||
import com.termux.shared.markdown.MarkdownUtils;
|
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
|
||||||
|
|
||||||
public class PluginUtils {
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "PluginUtils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process {@link ExecutionCommand} result.
|
|
||||||
*
|
|
||||||
* The ExecutionCommand currentState must be greater or equal to
|
|
||||||
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
|
||||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
|
||||||
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
|
||||||
* is not {@code null}, then the result of commands are sent back to the command caller.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
|
||||||
* @param logTag The log tag to use for logging.
|
|
||||||
* @param executionCommand The {@link ExecutionCommand} to process.
|
|
||||||
*/
|
|
||||||
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
|
||||||
if (executionCommand == null) return;
|
|
||||||
|
|
||||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
|
||||||
Error error = null;
|
|
||||||
ResultData resultData = executionCommand.resultData;
|
|
||||||
|
|
||||||
if (!executionCommand.hasExecuted()) {
|
|
||||||
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
|
||||||
|
|
||||||
// Log the output. ResultData should not be logged if pending result since ResultSender will do it
|
|
||||||
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
|
||||||
|
|
||||||
// If execution command was started by a plugin which expects the result back
|
|
||||||
if (isPluginExecutionCommandWithPendingResult) {
|
|
||||||
// Set variables which will be used by sendCommandResultData to send back the result
|
|
||||||
if (executionCommand.resultConfig.resultPendingIntent != null)
|
|
||||||
setPluginResultPendingIntentVariables(executionCommand);
|
|
||||||
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
|
||||||
setPluginResultDirectoryVariables(executionCommand);
|
|
||||||
|
|
||||||
// Send result to caller
|
|
||||||
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
|
||||||
if (error != null) {
|
|
||||||
// error will be added to existing Errors
|
|
||||||
resultData.setStateFailed(error);
|
|
||||||
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
|
||||||
|
|
||||||
// Flash and send notification for the error
|
|
||||||
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
|
||||||
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!executionCommand.isStateFailed() && error == null)
|
|
||||||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process {@link ExecutionCommand} error.
|
|
||||||
*
|
|
||||||
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
|
||||||
* The {@link ResultData#getErrCode()} must have been set to a value greater than
|
|
||||||
* {@link Errno#ERRNO_SUCCESS}.
|
|
||||||
* The {@link ResultData#errorsList} must also be set with appropriate error info.
|
|
||||||
*
|
|
||||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
|
||||||
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
|
||||||
* is not {@code null}, then the errors of commands are sent back to the command caller.
|
|
||||||
*
|
|
||||||
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
|
||||||
* enabled, then a flash and a notification will be shown for the error as well
|
|
||||||
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
|
|
||||||
* the error.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
* @param logTag The log tag to use for logging.
|
|
||||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
|
||||||
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
|
||||||
* regardless of if pending intent is {@code null} or
|
|
||||||
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
|
||||||
* is {@code false}.
|
|
||||||
*/
|
|
||||||
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
|
||||||
if (context == null || executionCommand == null) return;
|
|
||||||
|
|
||||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
|
||||||
Error error;
|
|
||||||
ResultData resultData = executionCommand.resultData;
|
|
||||||
|
|
||||||
if (!executionCommand.isStateFailed()) {
|
|
||||||
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
|
||||||
|
|
||||||
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
|
|
||||||
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
|
||||||
|
|
||||||
// If execution command was started by a plugin which expects the result back
|
|
||||||
if (isPluginExecutionCommandWithPendingResult) {
|
|
||||||
// Set variables which will be used by sendCommandResultData to send back the result
|
|
||||||
if (executionCommand.resultConfig.resultPendingIntent != null)
|
|
||||||
setPluginResultPendingIntentVariables(executionCommand);
|
|
||||||
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
|
||||||
setPluginResultDirectoryVariables(executionCommand);
|
|
||||||
|
|
||||||
// Send result to caller
|
|
||||||
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
|
||||||
if (error != null) {
|
|
||||||
// error will be added to existing Errors
|
|
||||||
resultData.setStateFailed(error);
|
|
||||||
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
|
||||||
forceNotification = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
|
||||||
if (!forceNotification) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
|
||||||
if (preferences == null) return;
|
|
||||||
|
|
||||||
// If user has disabled notifications for plugin commands, then just return
|
|
||||||
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Flash and send notification for the error
|
|
||||||
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
|
||||||
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
|
||||||
* to send back the result via {@link ResultConfig#resultPendingIntent}. */
|
|
||||||
public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) {
|
|
||||||
ResultConfig resultConfig = executionCommand.resultConfig;
|
|
||||||
|
|
||||||
resultConfig.resultBundleKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE;
|
|
||||||
resultConfig.resultStdoutKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT;
|
|
||||||
resultConfig.resultStdoutOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH;
|
|
||||||
resultConfig.resultStderrKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR;
|
|
||||||
resultConfig.resultStderrOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH;
|
|
||||||
resultConfig.resultExitCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE;
|
|
||||||
resultConfig.resultErrCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR;
|
|
||||||
resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
|
||||||
* to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */
|
|
||||||
public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) {
|
|
||||||
ResultConfig resultConfig = executionCommand.resultConfig;
|
|
||||||
|
|
||||||
resultConfig.resultDirectoryPath = TermuxFileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null, true);
|
|
||||||
resultConfig.resultDirectoryAllowedParentPath = TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(resultConfig.resultDirectoryPath);
|
|
||||||
|
|
||||||
// Set default resultFileBasename if resultSingleFile is true to `<executable_basename>-<timestamp>.log`
|
|
||||||
if (resultConfig.resultSingleFile && resultConfig.resultFileBasename == null)
|
|
||||||
resultConfig.resultFileBasename = ShellUtils.getExecutableBasename(executionCommand.executable) + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp() + ".log";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
|
||||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
|
||||||
* @param notificationTextString The text of the notification.
|
|
||||||
*/
|
|
||||||
public static void sendPluginCommandErrorNotification(Context context, String logTag, ExecutionCommand executionCommand, String notificationTextString) {
|
|
||||||
// Send a notification to show the error which when clicked will open the ReportActivity
|
|
||||||
// to show the details of the error
|
|
||||||
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
|
||||||
|
|
||||||
StringBuilder reportString = new StringBuilder();
|
|
||||||
|
|
||||||
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
|
||||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
|
||||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
|
||||||
|
|
||||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND.getName(), logTag, title, null, reportString.toString(), null,true));
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
// Setup the notification channel if not already set up
|
|
||||||
setupPluginCommandErrorsNotificationChannel(context);
|
|
||||||
|
|
||||||
// Use markdown in notification
|
|
||||||
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
|
|
||||||
//CharSequence notificationTextCharSequence = notificationTextString;
|
|
||||||
|
|
||||||
// Build the notification
|
|
||||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
|
||||||
if (builder == null) return;
|
|
||||||
|
|
||||||
// Send the notification
|
|
||||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
|
||||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
|
||||||
if (notificationManager != null)
|
|
||||||
notificationManager.notify(nextNotificationId, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
|
||||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
* @param title The title for the notification.
|
|
||||||
* @param notificationText The second line text of the notification.
|
|
||||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
|
||||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
|
||||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
|
||||||
* @return Returns the {@link Notification.Builder}.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
|
||||||
|
|
||||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
|
||||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
|
||||||
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
|
||||||
|
|
||||||
if (builder == null) return null;
|
|
||||||
|
|
||||||
// Enable timestamp
|
|
||||||
builder.setShowWhen(true);
|
|
||||||
|
|
||||||
// Set notification icon
|
|
||||||
builder.setSmallIcon(R.drawable.ic_error_notification);
|
|
||||||
|
|
||||||
// Set background color for small notification icon
|
|
||||||
builder.setColor(0xFF607D8B);
|
|
||||||
|
|
||||||
// Dismiss on click
|
|
||||||
builder.setAutoCancel(true);
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup the notification channel for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
|
|
||||||
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
*/
|
|
||||||
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
|
||||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
|
||||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} to get error string.
|
|
||||||
* @return Returns the {@code error} if policy is violated, otherwise {@code null}.
|
|
||||||
*/
|
|
||||||
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
|
||||||
String errmsg = null;
|
|
||||||
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
|
|
||||||
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errmsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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.TextInputDialogUtils;
|
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
|
||||||
import com.termux.app.TermuxService;
|
|
||||||
import com.termux.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) {
|
|
||||||
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 = 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"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/partial_primary_toolbar"
|
||||||
|
android:id="@+id/partial_primary_toolbar"/>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/settings"
|
android:id="@+id/settings"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<com.termux.app.terminal.TermuxActivityRootView 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:id="@+id/activity_termux_root_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -6,9 +8,12 @@
|
|||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
android:id="@+id/activity_termux_root_relative_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginHorizontal="3dp"
|
||||||
|
android:layout_marginVertical="0dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.drawerlayout.widget.DrawerLayout
|
<androidx.drawerlayout.widget.DrawerLayout
|
||||||
@@ -22,25 +27,25 @@
|
|||||||
android:id="@+id/terminal_view"
|
android:id="@+id/terminal_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginRight="3dp"
|
android:defaultFocusHighlightEnabled="false"
|
||||||
android:layout_marginLeft="3dp"
|
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:autofillHints="password" />
|
android:autofillHints="password"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/left_drawer"
|
android:id="@+id/left_drawer"
|
||||||
android:layout_width="240dp"
|
android:layout_width="240dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="start"
|
android:layout_gravity="start"
|
||||||
android:background="@android:color/white"
|
|
||||||
android:choiceMode="singleChoice"
|
android:choiceMode="singleChoice"
|
||||||
android:divider="@android:color/transparent"
|
android:divider="@android:color/transparent"
|
||||||
android:dividerHeight="0dp"
|
android:dividerHeight="0dp"
|
||||||
android:descendantFocusability="blocksDescendants"
|
android:descendantFocusability="blocksDescendants"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:background="?attr/termuxActivityDrawerBackground">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -52,7 +57,8 @@
|
|||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:src="@drawable/ic_settings"
|
android:src="@drawable/ic_settings"
|
||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:contentDescription="@string/action_open_settings" />
|
android:contentDescription="@string/action_open_settings"
|
||||||
|
app:tint="?attr/termuxActivityDrawerImageTint" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
@@ -70,7 +76,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/toggle_keyboard_button"
|
android:id="@+id/toggle_keyboard_button"
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -78,7 +84,7 @@
|
|||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/action_toggle_soft_keyboard" />
|
android:text="@string/action_toggle_soft_keyboard" />
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/new_session_button"
|
android:id="@+id/new_session_button"
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -95,7 +101,7 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="37.5dp"
|
android:layout_height="37.5dp"
|
||||||
android:background="@android:drawable/screen_background_dark_transparent"
|
android:background="@color/black"
|
||||||
android:layout_alignParentBottom="true" />
|
android:layout_alignParentBottom="true" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|||||||
@@ -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:id="@+id/session_title"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
android:id="@+id/terminal_toolbar_extra_keys"
|
||||||
style="?android:attr/buttonBarStyle"
|
style="?android:attr/buttonBarStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@android:color/black"/>
|
<background android:drawable="@android:color/black"/>
|
||||||
<foreground android:drawable="@drawable/ic_foreground"/>
|
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@android:color/black"/>
|
<background android:drawable="@android:color/black"/>
|
||||||
<foreground android:drawable="@drawable/ic_foreground"/>
|
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_foreground"/>
|
||||||
</adaptive-icon>
|
</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_STYLING_APP_NAME "Termux:Styling">
|
||||||
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
|
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
|
||||||
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
|
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
|
||||||
<!ENTITY TERMUX_PROPERTIES_PRIMARY_PATH_SHORT "~/.termux/termux.properties">
|
|
||||||
]>
|
]>
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="application_name">&TERMUX_APP_NAME;</string>
|
<string name="application_name">&TERMUX_APP_NAME;</string>
|
||||||
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
|
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<!-- Termux RUN_COMMAND permission -->
|
<!-- Termux RUN_COMMAND permission -->
|
||||||
<string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string>
|
<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;
|
<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_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
|
||||||
<string name="bootstrap_error_abort">Abort</string>
|
<string name="bootstrap_error_abort">Abort</string>
|
||||||
<string name="bootstrap_error_try_again">Try again</string>
|
<string name="bootstrap_error_try_again">Try again</string>
|
||||||
<string name="bootstrap_error_not_primary_user_message">&TERMUX_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">Terminal transcript</string>
|
||||||
<string name="title_share_transcript_with">Send transcript to:</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_autofill_password">Autofill password</string>
|
||||||
|
|
||||||
<string name="action_reset_terminal">Reset</string>
|
<string name="action_reset_terminal">Reset</string>
|
||||||
@@ -78,6 +88,7 @@
|
|||||||
|
|
||||||
<string name="action_report_issue">Report Issue</string>
|
<string name="action_report_issue">Report Issue</string>
|
||||||
<string name="msg_generating_report">Generating Report</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="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
|
||||||
<string name="action_styling_install">Install</string>
|
<string name="action_styling_install">Install</string>
|
||||||
@@ -92,24 +103,24 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- TermuxService -->
|
<!-- TermuxService -->
|
||||||
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
<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 -->
|
<!-- 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_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_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>
|
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Termux Execution Commands -->
|
|
||||||
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
|
|
||||||
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Termux File Receiver -->
|
<!-- Termux File Receiver -->
|
||||||
<string name="title_file_received">Save file in ~/downloads/</string>
|
<string name="title_file_received">Save file in ~/downloads/</string>
|
||||||
<string name="action_file_received_edit">Edit</string>
|
<string name="action_file_received_edit">Edit</string>
|
||||||
@@ -117,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 -->
|
<!-- Termux Settings -->
|
||||||
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
|
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
|
||||||
|
|
||||||
@@ -137,7 +154,8 @@
|
|||||||
<!-- Terminal View Key Logging -->
|
<!-- 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_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_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 -->
|
<!-- Plugin Error Notifications -->
|
||||||
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
|
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
|
||||||
@@ -164,15 +182,52 @@
|
|||||||
|
|
||||||
<!-- Soft Keyboard Only If No Hardware-->
|
<!-- 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_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_off">Soft keyboard will be enabled even if
|
||||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if no hardware keyboard is connected.</string>
|
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_title">&TERMUX_TASKER_APP_NAME;</string>
|
||||||
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</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 -->
|
<!-- About Preference -->
|
||||||
<string name="about_preference_title">About</string>
|
<string name="about_preference_title">About</string>
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<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="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||||
<!-- Seen in buttons on alert dialog: -->
|
<!-- Seen in buttons on alert dialog: -->
|
||||||
<item name="android:colorAccent">#212121</item>
|
<item name="android:colorAccent">#212121</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<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>
|
</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:summary="@string/termux_preferences_summary"
|
||||||
app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/>
|
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
|
<Preference
|
||||||
app:key="termux_tasker"
|
app:key="termux_tasker"
|
||||||
app:title="@string/termux_tasker_preferences_title"
|
app:title="@string/termux_tasker_preferences_title"
|
||||||
@@ -13,6 +27,13 @@
|
|||||||
app:isPreferenceVisible="false"
|
app:isPreferenceVisible="false"
|
||||||
app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/>
|
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
|
<Preference
|
||||||
app:key="about"
|
app:key="about"
|
||||||
app:title="@string/about_preference_title"
|
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:summary="@string/termux_terminal_io_preferences_summary"
|
||||||
app:fragment="com.termux.app.fragments.settings.termux.TerminalIOPreferencesFragment"/>
|
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>
|
</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;
|
package com.termux.app;
|
||||||
|
|
||||||
import com.termux.shared.data.UrlUtils;
|
import com.termux.shared.termux.data.TermuxUrlUtils;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -13,7 +13,7 @@ public class TermuxActivityTest {
|
|||||||
private void assertUrlsAre(String text, String... urls) {
|
private void assertUrlsAre(String text, String... urls) {
|
||||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||||
Collections.addAll(expected, urls);
|
Collections.addAll(expected, urls);
|
||||||
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
|
Assert.assertEquals(expected, TermuxUrlUtils.extractUrls(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -9,7 +11,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
public class TermuxFileReceiverActivityTest {
|
public class FileReceiverActivityTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIsSharedTextAnUrl() {
|
public void testIsSharedTextAnUrl() {
|
||||||
@@ -19,13 +21,13 @@ public class TermuxFileReceiverActivityTest {
|
|||||||
validUrls.add("https://example.com/path/parameter=foo");
|
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");
|
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) {
|
for (String url : validUrls) {
|
||||||
Assert.assertTrue(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
Assert.assertTrue(FileReceiverActivity.isSharedTextAnUrl(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> invalidUrls = new ArrayList<>();
|
List<String> invalidUrls = new ArrayList<>();
|
||||||
invalidUrls.add("a test with example.com");
|
invalidUrls.add("a test with example.com");
|
||||||
for (String url : invalidUrls) {
|
for (String url : invalidUrls) {
|
||||||
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
|
Assert.assertFalse(FileReceiverActivity.isSharedTextAnUrl(url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
classpath "com.android.tools.build:gradle:4.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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,7 +15,7 @@
|
|||||||
org.gradle.jvmargs=-Xmx2048M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
minSdkVersion=24
|
minSdkVersion=21
|
||||||
targetSdkVersion=28
|
targetSdkVersion=28
|
||||||
ndkVersion=22.1.7171670
|
ndkVersion=22.1.7171670
|
||||||
compileSdkVersion=30
|
compileSdkVersion=30
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ tasks.withType(Test) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13.2'
|
implementation "androidx.annotation:annotation:1.3.0"
|
||||||
|
testImplementation "junit:junit:4.13.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
task sourceJar(type: Jar) {
|
task sourceJar(type: Jar) {
|
||||||
@@ -66,7 +67,7 @@ afterEvaluate {
|
|||||||
from components.release
|
from components.release
|
||||||
groupId = 'com.termux'
|
groupId = 'com.termux'
|
||||||
artifactId = 'terminal-emulator'
|
artifactId = 'terminal-emulator'
|
||||||
version = '0.116'
|
version = '0.118.0'
|
||||||
artifact(sourceJar)
|
artifact(sourceJar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,9 +227,9 @@ public final class KeyHandler {
|
|||||||
return transformForModifiers("\033[3", keyMode, '~');
|
return transformForModifiers("\033[3", keyMode, '~');
|
||||||
|
|
||||||
case KEYCODE_PAGE_UP:
|
case KEYCODE_PAGE_UP:
|
||||||
return "\033[5~";
|
return transformForModifiers("\033[5", keyMode, '~');
|
||||||
case KEYCODE_PAGE_DOWN:
|
case KEYCODE_PAGE_DOWN:
|
||||||
return "\033[6~";
|
return transformForModifiers("\033[6", keyMode, '~');
|
||||||
case KEYCODE_DEL:
|
case KEYCODE_DEL:
|
||||||
String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
|
String prefix = ((keyMode & KEYMOD_ALT) == 0) ? "" : "\033";
|
||||||
// Just do what xterm and gnome-terminal does:
|
// 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) {
|
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) {
|
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 (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;
|
boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1;
|
||||||
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||||
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||||
@@ -102,6 +105,45 @@ public final class TerminalBuffer {
|
|||||||
return builder.toString();
|
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() {
|
public int getActiveTranscriptRows() {
|
||||||
return mActiveTranscriptRows;
|
return mActiveTranscriptRows;
|
||||||
}
|
}
|
||||||
@@ -407,8 +449,8 @@ public final class TerminalBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setChar(int column, int row, int codePoint, long style) {
|
public void setChar(int column, int row, int codePoint, long style) {
|
||||||
if (row >= mScreenRows || column >= mColumns)
|
if (row < 0 || row >= mScreenRows || column < 0 || column >= mColumns)
|
||||||
throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
throw new IllegalArgumentException("TerminalBuffer.setChar(): row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
|
||||||
row = externalToInternalRow(row);
|
row = externalToInternalRow(row);
|
||||||
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
|
allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public final class TerminalColorScheme {
|
|||||||
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
|
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:
|
// 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];
|
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
|
||||||
|
|
||||||
@@ -71,6 +71,7 @@ public final class TerminalColorScheme {
|
|||||||
|
|
||||||
public void updateWith(Properties props) {
|
public void updateWith(Properties props) {
|
||||||
reset();
|
reset();
|
||||||
|
boolean cursorPropExists = false;
|
||||||
for (Map.Entry<Object, Object> entries : props.entrySet()) {
|
for (Map.Entry<Object, Object> entries : props.entrySet()) {
|
||||||
String key = (String) entries.getKey();
|
String key = (String) entries.getKey();
|
||||||
String value = (String) entries.getValue();
|
String value = (String) entries.getValue();
|
||||||
@@ -82,6 +83,7 @@ public final class TerminalColorScheme {
|
|||||||
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
|
colorIndex = TextStyle.COLOR_INDEX_BACKGROUND;
|
||||||
} else if (key.equals("cursor")) {
|
} else if (key.equals("cursor")) {
|
||||||
colorIndex = TextStyle.COLOR_INDEX_CURSOR;
|
colorIndex = TextStyle.COLOR_INDEX_CURSOR;
|
||||||
|
cursorPropExists = true;
|
||||||
} else if (key.startsWith("color")) {
|
} else if (key.startsWith("color")) {
|
||||||
try {
|
try {
|
||||||
colorIndex = Integer.parseInt(key.substring(5));
|
colorIndex = Integer.parseInt(key.substring(5));
|
||||||
@@ -98,6 +100,27 @@ public final class TerminalColorScheme {
|
|||||||
|
|
||||||
mDefaultColors[colorIndex] = colorValue;
|
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;
|
package com.termux.terminal;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
|
||||||
/** Current terminal colors (if different from default). */
|
/** Current terminal colors (if different from default). */
|
||||||
public final class TerminalColors {
|
public final class TerminalColors {
|
||||||
|
|
||||||
@@ -73,4 +75,22 @@ public final class TerminalColors {
|
|||||||
if (c != 0) mCurrentColors[intoIndex] = c;
|
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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ public final class TerminalEmulator {
|
|||||||
private String mTitle;
|
private String mTitle;
|
||||||
private final Stack<String> mTitleStack = new Stack<>();
|
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). */
|
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
|
||||||
private int mCursorRow, mCursorCol;
|
private int mCursorRow, mCursorCol;
|
||||||
@@ -248,6 +252,9 @@ public final class TerminalEmulator {
|
|||||||
*/
|
*/
|
||||||
private int mScrollCounter = 0;
|
private int mScrollCounter = 0;
|
||||||
|
|
||||||
|
/** If automatic scrolling of terminal is disabled */
|
||||||
|
private boolean mAutoScrollDisabled;
|
||||||
|
|
||||||
private byte mUtf8ToFollow, mUtf8Index;
|
private byte mUtf8ToFollow, mUtf8Index;
|
||||||
private final byte[] mUtf8InputBuffer = new byte[4];
|
private final byte[] mUtf8InputBuffer = new byte[4];
|
||||||
private int mLastEmittedCodePoint = -1;
|
private int mLastEmittedCodePoint = -1;
|
||||||
@@ -796,7 +803,6 @@ public final class TerminalEmulator {
|
|||||||
int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor);
|
int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor);
|
||||||
int columnsToMove = columnsAfterCursor - columnsToDelete;
|
int columnsToMove = columnsAfterCursor - columnsToDelete;
|
||||||
mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0);
|
mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0);
|
||||||
blockClear(mCursorRow + columnsToMove, 0, columnsToDelete, mRows);
|
|
||||||
} else {
|
} else {
|
||||||
unknownSequence(b);
|
unknownSequence(b);
|
||||||
}
|
}
|
||||||
@@ -825,7 +831,7 @@ public final class TerminalEmulator {
|
|||||||
if (internalBit != -1) {
|
if (internalBit != -1) {
|
||||||
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
||||||
} else {
|
} else {
|
||||||
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
|
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -936,10 +942,17 @@ public final class TerminalEmulator {
|
|||||||
for (String part : dcs.substring(2).split(";")) {
|
for (String part : dcs.substring(2).split(";")) {
|
||||||
if (part.length() % 2 == 0) {
|
if (part.length() % 2 == 0) {
|
||||||
StringBuilder transBuffer = new StringBuilder();
|
StringBuilder transBuffer = new StringBuilder();
|
||||||
|
char c;
|
||||||
for (int i = 0; i < part.length(); i += 2) {
|
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);
|
transBuffer.append(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
String trans = transBuffer.toString();
|
String trans = transBuffer.toString();
|
||||||
String responseValue;
|
String responseValue;
|
||||||
switch (trans) {
|
switch (trans) {
|
||||||
@@ -962,7 +975,7 @@ public final class TerminalEmulator {
|
|||||||
case "&8": // Undo key - ignore.
|
case "&8": // Undo key - ignore.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
Logger.logWarn(mClient, LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||||
}
|
}
|
||||||
// Respond with invalid request:
|
// Respond with invalid request:
|
||||||
mSession.write("\033P0+r" + part + "\033\\");
|
mSession.write("\033P0+r" + part + "\033\\");
|
||||||
@@ -974,12 +987,12 @@ public final class TerminalEmulator {
|
|||||||
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
if (LOG_ESCAPE_SEQUENCES)
|
if (LOG_ESCAPE_SEQUENCES)
|
||||||
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
|
Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||||
}
|
}
|
||||||
finishSequence();
|
finishSequence();
|
||||||
}
|
}
|
||||||
@@ -1069,7 +1082,7 @@ public final class TerminalEmulator {
|
|||||||
int externalBit = mArgs[i];
|
int externalBit = mArgs[i];
|
||||||
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
||||||
if (internalBit == -1) {
|
if (internalBit == -1) {
|
||||||
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 {
|
} else {
|
||||||
if (b == 's') {
|
if (b == 's') {
|
||||||
mSavedDecSetFlags |= internalBit;
|
mSavedDecSetFlags |= internalBit;
|
||||||
@@ -1259,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
|
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
|
||||||
// some special control character cases, e.g., Control-Space to make a NUL.
|
// some special control character cases, e.g., Control-Space to make a NUL.
|
||||||
// (2) enables this feature for keys including the exceptions listed.
|
// (2) enables this feature for keys including the exceptions listed.
|
||||||
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;
|
break;
|
||||||
default:
|
default:
|
||||||
parseArg(b);
|
parseArg(b);
|
||||||
@@ -1380,6 +1393,8 @@ public final class TerminalEmulator {
|
|||||||
break;
|
break;
|
||||||
case '[':
|
case '[':
|
||||||
continueSequence(ESC_CSI);
|
continueSequence(ESC_CSI);
|
||||||
|
mIsCSIStart = true;
|
||||||
|
mLastCSIArg = null;
|
||||||
break;
|
break;
|
||||||
case '=': // DECKPAM
|
case '=': // DECKPAM
|
||||||
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true);
|
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true);
|
||||||
@@ -1806,7 +1821,7 @@ public final class TerminalEmulator {
|
|||||||
int firstArg = mArgs[i + 1];
|
int firstArg = mArgs[i + 1];
|
||||||
if (firstArg == 2) {
|
if (firstArg == 2) {
|
||||||
if (i + 4 > mArgIndex) {
|
if (i + 4 > mArgIndex) {
|
||||||
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
Logger.logWarn(mClient, LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||||
} else {
|
} else {
|
||||||
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
||||||
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
||||||
@@ -1831,7 +1846,7 @@ public final class TerminalEmulator {
|
|||||||
mBackColor = color;
|
mBackColor = color;
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
||||||
@@ -1848,7 +1863,7 @@ public final class TerminalEmulator {
|
|||||||
mBackColor = code - 100 + 8;
|
mBackColor = code - 100 + 8;
|
||||||
} else {
|
} else {
|
||||||
if (LOG_ESCAPE_SEQUENCES)
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1980,9 +1995,9 @@ public final class TerminalEmulator {
|
|||||||
int startIndex = textParameter.indexOf(";") + 1;
|
int startIndex = textParameter.indexOf(";") + 1;
|
||||||
try {
|
try {
|
||||||
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
||||||
mSession.clipboardText(clipboardText);
|
mSession.onCopyTextToClipboard(clipboardText);
|
||||||
} catch (Exception e) {
|
} 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;
|
break;
|
||||||
case 104:
|
case 104:
|
||||||
@@ -2087,8 +2102,33 @@ public final class TerminalEmulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Process the next ASCII character of a parameter. */
|
/**
|
||||||
private void parseArg(int b) {
|
* 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 (b >= '0' && b <= '9') {
|
||||||
if (mArgIndex < mArgs.length) {
|
if (mArgIndex < mArgs.length) {
|
||||||
int oldValue = mArgs[mArgIndex];
|
int oldValue = mArgs[mArgIndex];
|
||||||
@@ -2099,6 +2139,8 @@ public final class TerminalEmulator {
|
|||||||
} else {
|
} else {
|
||||||
value = thisDigit;
|
value = thisDigit;
|
||||||
}
|
}
|
||||||
|
if (value > 9999)
|
||||||
|
value = 9999;
|
||||||
mArgs[mArgIndex] = value;
|
mArgs[mArgIndex] = value;
|
||||||
}
|
}
|
||||||
continueSequence(mEscapeState);
|
continueSequence(mEscapeState);
|
||||||
@@ -2110,6 +2152,8 @@ public final class TerminalEmulator {
|
|||||||
} else {
|
} else {
|
||||||
unknownSequence(b);
|
unknownSequence(b);
|
||||||
}
|
}
|
||||||
|
mLastCSIArg = b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getArg0(int defaultValue) {
|
private int getArg0(int defaultValue) {
|
||||||
@@ -2178,7 +2222,7 @@ public final class TerminalEmulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void finishSequenceAndLogError(String error) {
|
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();
|
finishSequence();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2326,7 +2370,14 @@ public final class TerminalEmulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int offsetDueToCombiningChar = ((displayWidth <= 0 && mCursorCol > 0 && !mAboutToAutoWrap) ? 1 : 0);
|
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)
|
if (autoWrap && displayWidth > 0)
|
||||||
mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth);
|
mAboutToAutoWrap = (mCursorCol == mRightMargin - displayWidth);
|
||||||
@@ -2364,6 +2415,15 @@ public final class TerminalEmulator {
|
|||||||
mScrollCounter = 0;
|
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. */
|
/** Reset terminal state so user can interact with it regardless of present state. */
|
||||||
public void reset() {
|
public void reset() {
|
||||||
setCursorStyle();
|
setCursorStyle();
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ public abstract class TerminalOutput {
|
|||||||
/** Notify the terminal client that the terminal title has changed. */
|
/** Notify the terminal client that the terminal title has changed. */
|
||||||
public abstract void titleChanged(String oldTitle, String newTitle);
|
public abstract void titleChanged(String oldTitle, String newTitle);
|
||||||
|
|
||||||
/** Notify the terminal client that the terminal title has changed. */
|
/** Notify the terminal client that text should be copied to clipboard. */
|
||||||
public abstract void clipboardText(String text);
|
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. */
|
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
|
||||||
public abstract void onBell();
|
public abstract void onBell();
|
||||||
|
|||||||
@@ -11,11 +11,37 @@ public final class TerminalRow {
|
|||||||
|
|
||||||
private static final float SPARE_CAPACITY_FACTOR = 1.5f;
|
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. */
|
/** The number of columns in this terminal row. */
|
||||||
private final int mColumns;
|
private final int mColumns;
|
||||||
/** The text filling this terminal row. */
|
/** The text filling this terminal row. */
|
||||||
public char[] mText;
|
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;
|
private short mSpaceUsed;
|
||||||
/** If this row has been line wrapped due to text output at the end of line. */
|
/** If this row has been line wrapped due to text output at the end of line. */
|
||||||
boolean mLineWrap;
|
boolean mLineWrap;
|
||||||
@@ -124,6 +150,9 @@ public final class TerminalRow {
|
|||||||
|
|
||||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||||
public void setChar(int columnToSet, int codePoint, long style) {
|
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;
|
mStyle[columnToSet] = style;
|
||||||
|
|
||||||
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
|
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
|
// Get the number of elements in the mText array this column uses now
|
||||||
int oldCharactersUsedForColumn;
|
int oldCharactersUsedForColumn;
|
||||||
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
|
if (columnToSet + oldCodePointDisplayWidth < mColumns) {
|
||||||
oldCharactersUsedForColumn = findStartOfColumn(columnToSet + oldCodePointDisplayWidth) - oldStartOfColumnIndex;
|
int oldEndOfColumnIndex = findStartOfColumn(columnToSet + oldCodePointDisplayWidth);
|
||||||
|
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex;
|
||||||
} else {
|
} else {
|
||||||
// Last character.
|
// Last character.
|
||||||
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex;
|
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
|
// Find how many chars this column will need
|
||||||
int newCharactersUsedForColumn = Character.charCount(codePoint);
|
int newCharactersUsedForColumn = Character.charCount(codePoint);
|
||||||
if (newIsCombining) {
|
if (newIsCombining) {
|
||||||
// Combining characters are added to the contents of the column instead of overwriting them, so that they
|
// Combining characters are added to the contents of the column instead of overwriting them, so that they
|
||||||
// modify the existing contents.
|
// modify the existing contents.
|
||||||
// FIXME: Put a limit of combining characters.
|
|
||||||
// FIXME: Unassigned characters also get width=0.
|
// FIXME: Unassigned characters also get width=0.
|
||||||
newCharactersUsedForColumn += oldCharactersUsedForColumn;
|
newCharactersUsedForColumn += oldCharactersUsedForColumn;
|
||||||
}
|
}
|
||||||
@@ -186,7 +222,7 @@ public final class TerminalRow {
|
|||||||
if (mSpaceUsed + javaCharDifference > text.length) {
|
if (mSpaceUsed + javaCharDifference > text.length) {
|
||||||
// We need to grow the array
|
// We need to grow the array
|
||||||
char[] newText = new char[text.length + mColumns];
|
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);
|
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn);
|
||||||
mText = text = newText;
|
mText = text = newText;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
int[] processId = new int[1];
|
int[] processId = new int[1];
|
||||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||||
mShellPid = processId[0];
|
mShellPid = processId[0];
|
||||||
|
mClient.setTerminalShellPid(this, mShellPid);
|
||||||
|
|
||||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
||||||
|
|
||||||
@@ -236,7 +237,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
try {
|
try {
|
||||||
Os.kill(mShellPid, OsConstants.SIGKILL);
|
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||||
} catch (ErrnoException e) {
|
} catch (ErrnoException e) {
|
||||||
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
Logger.logWarn(mClient, LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,8 +270,13 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clipboardText(String text) {
|
public void onCopyTextToClipboard(String text) {
|
||||||
mClient.onClipboardText(this, text);
|
mClient.onCopyTextToClipboard(this, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPasteTextFromClipboard() {
|
||||||
|
mClient.onPasteTextFromClipboard(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -303,7 +309,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
return outputPath;
|
return outputPath;
|
||||||
}
|
}
|
||||||
} catch (IOException | SecurityException e) {
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -321,7 +327,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
descriptorField.setAccessible(true);
|
descriptorField.setAccessible(true);
|
||||||
descriptorField.set(result, fileDescriptor);
|
descriptorField.set(result, fileDescriptor);
|
||||||
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
} 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);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.termux.terminal;
|
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
|
* 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
|
* send callbacks to the client when {@link TerminalSession} changes or for sending other
|
||||||
@@ -7,20 +10,24 @@ package com.termux.terminal;
|
|||||||
*/
|
*/
|
||||||
public interface TerminalSessionClient {
|
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 onTerminalCursorStateChange(boolean state);
|
||||||
|
|
||||||
|
void setTerminalShellPid(@NonNull TerminalSession session, int pid);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Integer getTerminalCursorStyle();
|
Integer getTerminalCursorStyle();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.termux.terminal;
|
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.
|
* 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:
|
* Must be kept in sync with the following:
|
||||||
* https://github.com/termux/wcwidth
|
* https://github.com/termux/wcwidth
|
||||||
* https://github.com/termux/libandroid-support
|
* 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 {
|
public final class WcWidth {
|
||||||
|
|
||||||
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
// 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 = {
|
private static final int[][] ZERO_WIDTH = {
|
||||||
{0x00300, 0x0036f}, // Combining Grave Accent ..Combining Latin Small Le
|
{0x00300, 0x0036f}, // Combining Grave Accent ..Combining Latin Small Le
|
||||||
{0x00483, 0x00489}, // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
{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
|
{0x00825, 0x00827}, // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||||
{0x00829, 0x0082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
{0x00829, 0x0082d}, // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||||
{0x00859, 0x0085b}, // Mandaic Affrication Mark..Mandaic Gemination Mark
|
{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
|
{0x008e3, 0x00902}, // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||||
{0x0093a, 0x0093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
{0x0093a, 0x0093a}, // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||||
{0x0093c, 0x0093c}, // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
{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
|
{0x00b3f, 0x00b3f}, // Oriya Vowel Sign I ..Oriya Vowel Sign I
|
||||||
{0x00b41, 0x00b44}, // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
|
{0x00b41, 0x00b44}, // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
|
||||||
{0x00b4d, 0x00b4d}, // Oriya Sign Virama ..Oriya Sign Virama
|
{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
|
{0x00b62, 0x00b63}, // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
|
||||||
{0x00b82, 0x00b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
{0x00b82, 0x00b82}, // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||||
{0x00bc0, 0x00bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
{0x00bc0, 0x00bc0}, // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||||
{0x00bcd, 0x00bcd}, // Tamil Sign Virama ..Tamil Sign Virama
|
{0x00bcd, 0x00bcd}, // Tamil Sign Virama ..Tamil Sign Virama
|
||||||
{0x00c00, 0x00c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
{0x00c00, 0x00c00}, // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||||
{0x00c04, 0x00c04}, // Telugu Sign Combining An..Telugu Sign Combining An
|
{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
|
{0x00c3e, 0x00c40}, // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||||
{0x00c46, 0x00c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
{0x00c46, 0x00c48}, // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||||
{0x00c4a, 0x00c4d}, // Telugu Vowel Sign O ..Telugu Sign Virama
|
{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
|
{0x00d41, 0x00d44}, // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||||
{0x00d4d, 0x00d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
|
{0x00d4d, 0x00d4d}, // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||||
{0x00d62, 0x00d63}, // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
{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
|
{0x00dca, 0x00dca}, // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||||
{0x00dd2, 0x00dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
{0x00dd2, 0x00dd4}, // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||||
{0x00dd6, 0x00dd6}, // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
{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
|
{0x00e47, 0x00e4e}, // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||||
{0x00eb1, 0x00eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
{0x00eb1, 0x00eb1}, // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||||
{0x00eb4, 0x00ebc}, // Lao Vowel Sign I ..Lao Semivowel Sign Lo
|
{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
|
{0x00f18, 0x00f19}, // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||||
{0x00f35, 0x00f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
{0x00f35, 0x00f35}, // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||||
{0x00f37, 0x00f37}, // 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
|
{0x0109d, 0x0109d}, // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||||
{0x0135d, 0x0135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
{0x0135d, 0x0135f}, // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||||
{0x01712, 0x01714}, // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
{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
|
{0x01752, 0x01753}, // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||||
{0x01772, 0x01773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
{0x01772, 0x01773}, // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||||
{0x017b4, 0x017b5}, // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
{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
|
{0x017c9, 0x017d3}, // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||||
{0x017dd, 0x017dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
{0x017dd, 0x017dd}, // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||||
{0x0180b, 0x0180d}, // Mongolian Free Variation..Mongolian Free Variation
|
{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
|
{0x01885, 0x01886}, // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||||
{0x018a9, 0x018a9}, // 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
|
{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
|
{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
|
{0x01a73, 0x01a7c}, // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||||
{0x01a7f, 0x01a7f}, // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
{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
|
{0x01b00, 0x01b03}, // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||||
{0x01b34, 0x01b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
{0x01b34, 0x01b34}, // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||||
{0x01b36, 0x01b3a}, // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
{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
|
{0x01ced, 0x01ced}, // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||||
{0x01cf4, 0x01cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
{0x01cf4, 0x01cf4}, // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||||
{0x01cf8, 0x01cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
{0x01cf8, 0x01cf9}, // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||||
{0x01dc0, 0x01df9}, // Combining Dotted Grave A..Combining Wide Inverted
|
{0x01dc0, 0x01dff}, // Combining Dotted Grave A..Combining Right Arrowhea
|
||||||
{0x01dfb, 0x01dff}, // Combining Deletion Mark ..Combining Right Arrowhea
|
|
||||||
{0x020d0, 0x020f0}, // Combining Left Harpoon A..Combining Asterisk Above
|
{0x020d0, 0x020f0}, // Combining Left Harpoon A..Combining Asterisk Above
|
||||||
{0x02cef, 0x02cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
{0x02cef, 0x02cf1}, // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||||
{0x02d7f, 0x02d7f}, // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
{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
|
{0x0a806, 0x0a806}, // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||||
{0x0a80b, 0x0a80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
{0x0a80b, 0x0a80b}, // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||||
{0x0a825, 0x0a826}, // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
{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
|
{0x0a8c4, 0x0a8c5}, // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
|
||||||
{0x0a8e0, 0x0a8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
|
{0x0a8e0, 0x0a8f1}, // Combining Devanagari Dig..Combining Devanagari Sig
|
||||||
{0x0a8ff, 0x0a8ff}, // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
|
{0x0a8ff, 0x0a8ff}, // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
|
||||||
@@ -233,13 +236,18 @@ public final class WcWidth {
|
|||||||
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
|
{0x10a3f, 0x10a3f}, // Kharoshthi Virama ..Kharoshthi Virama
|
||||||
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
|
{0x10ae5, 0x10ae6}, // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||||
{0x10d24, 0x10d27}, // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
|
{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
|
{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
|
{0x11001, 0x11001}, // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||||
{0x11038, 0x11046}, // Brahmi Vowel Sign Aa ..Brahmi Virama
|
{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
|
{0x1107f, 0x11081}, // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||||
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
{0x110b3, 0x110b6}, // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||||
{0x110b9, 0x110ba}, // Kaithi Sign Virama ..Kaithi Sign Nukta
|
{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
|
{0x11100, 0x11102}, // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||||
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
{0x11127, 0x1112b}, // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||||
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
{0x1112d, 0x11134}, // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||||
@@ -247,11 +255,12 @@ public final class WcWidth {
|
|||||||
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
{0x11180, 0x11181}, // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||||
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
{0x111b6, 0x111be}, // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||||
{0x111c9, 0x111cc}, // Sharada Sandhi Mark ..Sharada Extra Short Vowe
|
{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
|
{0x1122f, 0x11231}, // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||||
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
{0x11234, 0x11234}, // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||||
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
|
{0x11236, 0x11237}, // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||||
{0x1123e, 0x1123e}, // Khojki Sign Sukun ..Khojki Sign Sukun
|
{0x1123e, 0x1123e}, // Khojki Sign Sukun ..Khojki Sign Sukun
|
||||||
|
{0x11241, 0x11241}, // (nil) ..(nil)
|
||||||
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
{0x112df, 0x112df}, // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||||
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
{0x112e3, 0x112ea}, // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||||
{0x11300, 0x11301}, // Grantha Sign Combining A..Grantha Sign Candrabindu
|
{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
|
{0x11727, 0x1172b}, // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||||
{0x1182f, 0x11837}, // Dogra Vowel Sign U ..Dogra Sign Anusvara
|
{0x1182f, 0x11837}, // Dogra Vowel Sign U ..Dogra Sign Anusvara
|
||||||
{0x11839, 0x1183a}, // Dogra Sign Virama ..Dogra Sign Nukta
|
{0x11839, 0x1183a}, // Dogra Sign Virama ..Dogra Sign Nukta
|
||||||
{0x1193b, 0x1193c}, // (nil) ..(nil)
|
{0x1193b, 0x1193c}, // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
|
||||||
{0x1193e, 0x1193e}, // (nil) ..(nil)
|
{0x1193e, 0x1193e}, // Dives Akuru Virama ..Dives Akuru Virama
|
||||||
{0x11943, 0x11943}, // (nil) ..(nil)
|
{0x11943, 0x11943}, // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
|
||||||
{0x119d4, 0x119d7}, // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
|
{0x119d4, 0x119d7}, // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
|
||||||
{0x119da, 0x119db}, // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
|
{0x119da, 0x119db}, // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
|
||||||
{0x119e0, 0x119e0}, // Nandinagari Sign Virama ..Nandinagari Sign Virama
|
{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
|
{0x11d95, 0x11d95}, // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
|
||||||
{0x11d97, 0x11d97}, // Gunjala Gondi Virama ..Gunjala Gondi Virama
|
{0x11d97, 0x11d97}, // Gunjala Gondi Virama ..Gunjala Gondi Virama
|
||||||
{0x11ef3, 0x11ef4}, // Makasar Vowel Sign I ..Makasar Vowel Sign U
|
{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
|
{0x16af0, 0x16af4}, // Bassa Vah Combining High..Bassa Vah Combining High
|
||||||
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
{0x16b30, 0x16b36}, // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||||
{0x16f4f, 0x16f4f}, // Miao Sign Consonant Modi..Miao Sign Consonant Modi
|
{0x16f4f, 0x16f4f}, // Miao Sign Consonant Modi..Miao Sign Consonant Modi
|
||||||
{0x16f8f, 0x16f92}, // Miao Tone Right ..Miao Tone Below
|
{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
|
{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
|
{0x1d167, 0x1d169}, // Musical Symbol Combining..Musical Symbol Combining
|
||||||
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
|
{0x1d17b, 0x1d182}, // Musical Symbol Combining..Musical Symbol Combining
|
||||||
{0x1d185, 0x1d18b}, // 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
|
{0x1e01b, 0x1e021}, // Combining Glagolitic Let..Combining Glagolitic Let
|
||||||
{0x1e023, 0x1e024}, // Combining Glagolitic Let..Combining Glagolitic Let
|
{0x1e023, 0x1e024}, // Combining Glagolitic Let..Combining Glagolitic Let
|
||||||
{0x1e026, 0x1e02a}, // 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
|
{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
|
{0x1e2ec, 0x1e2ef}, // Wancho Tone Tup ..Wancho Tone Koini
|
||||||
|
{0x1e4ec, 0x1e4ef}, // (nil) ..(nil)
|
||||||
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
|
{0x1e8d0, 0x1e8d6}, // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||||
{0x1e944, 0x1e94a}, // Adlam Alif Lengthener ..Adlam Nukta
|
{0x1e944, 0x1e94a}, // Adlam Alif Lengthener ..Adlam Nukta
|
||||||
{0xe0100, 0xe01ef}, // Variation Selector-17 ..Variation Selector-256
|
{0xe0100, 0xe01ef}, // Variation Selector-17 ..Variation Selector-256
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
// 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 = {
|
private static final int[][] WIDE_EASTASIAN = {
|
||||||
{0x01100, 0x0115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
{0x01100, 0x0115f}, // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||||
{0x0231a, 0x0231b}, // Watch ..Hourglass
|
{0x0231a, 0x0231b}, // Watch ..Hourglass
|
||||||
@@ -392,7 +413,7 @@ public final class WcWidth {
|
|||||||
{0x03190, 0x031e3}, // Ideographic Annotation L..Cjk Stroke Q
|
{0x03190, 0x031e3}, // Ideographic Annotation L..Cjk Stroke Q
|
||||||
{0x031f0, 0x0321e}, // Katakana Letter Small Ku..Parenthesized Korean Cha
|
{0x031f0, 0x0321e}, // Katakana Letter Small Ku..Parenthesized Korean Cha
|
||||||
{0x03220, 0x03247}, // Parenthesized Ideograph ..Circled Ideograph Koto
|
{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
|
{0x04e00, 0x0a48c}, // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||||
{0x0a490, 0x0a4c6}, // Yi Radical Qot ..Yi Radical Ke
|
{0x0a490, 0x0a4c6}, // Yi Radical Qot ..Yi Radical Ke
|
||||||
{0x0a960, 0x0a97c}, // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
{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
|
{0x0fe68, 0x0fe6b}, // Small Reverse Solidus ..Small Commercial At
|
||||||
{0x0ff01, 0x0ff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
{0x0ff01, 0x0ff60}, // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||||
{0x0ffe0, 0x0ffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
|
{0x0ffe0, 0x0ffe6}, // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||||
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..(nil)
|
{0x16fe0, 0x16fe4}, // Tangut Iteration Mark ..Khitan Small Script Fill
|
||||||
{0x16ff0, 0x16ff1}, // (nil) ..(nil)
|
{0x16ff0, 0x16ff1}, // Vietnamese Alternate Rea..Vietnamese Alternate Rea
|
||||||
{0x17000, 0x187f7}, // (nil) ..(nil)
|
{0x17000, 0x187f7}, // (nil) ..(nil)
|
||||||
{0x18800, 0x18cd5}, // Tangut Component-001 ..(nil)
|
{0x18800, 0x18cd5}, // Tangut Component-001 ..Khitan Small Script Char
|
||||||
{0x18d00, 0x18d08}, // (nil) ..(nil)
|
{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
|
{0x1b150, 0x1b152}, // Hiragana Letter Small Wi..Hiragana Letter Small Wo
|
||||||
|
{0x1b155, 0x1b155}, // (nil) ..(nil)
|
||||||
{0x1b164, 0x1b167}, // Katakana Letter Small Wi..Katakana Letter Small N
|
{0x1b164, 0x1b167}, // Katakana Letter Small Wi..Katakana Letter Small N
|
||||||
{0x1b170, 0x1b2fb}, // Nushu Character-1b170 ..Nushu Character-1b2fb
|
{0x1b170, 0x1b2fb}, // Nushu Character-1b170 ..Nushu Character-1b2fb
|
||||||
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
{0x1f004, 0x1f004}, // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||||
@@ -443,24 +469,24 @@ public final class WcWidth {
|
|||||||
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
|
{0x1f680, 0x1f6c5}, // Rocket ..Left Luggage
|
||||||
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
|
{0x1f6cc, 0x1f6cc}, // Sleeping Accommodation ..Sleeping Accommodation
|
||||||
{0x1f6d0, 0x1f6d2}, // Place Of Worship ..Shopping Trolley
|
{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
|
{0x1f6eb, 0x1f6ec}, // Airplane Departure ..Airplane Arriving
|
||||||
{0x1f6f4, 0x1f6fc}, // Scooter ..(nil)
|
{0x1f6f4, 0x1f6fc}, // Scooter ..Roller Skate
|
||||||
{0x1f7e0, 0x1f7eb}, // Large Orange Circle ..Large Brown Square
|
{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
|
{0x1f93c, 0x1f945}, // Wrestlers ..Goal Net
|
||||||
{0x1f947, 0x1f978}, // First Place Medal ..(nil)
|
{0x1f947, 0x1f9ff}, // First Place Medal ..Nazar Amulet
|
||||||
{0x1f97a, 0x1f9cb}, // Face With Pleading Eyes ..(nil)
|
{0x1fa70, 0x1fa7c}, // Ballet Shoes ..Crutch
|
||||||
{0x1f9cd, 0x1f9ff}, // Standing Person ..Nazar Amulet
|
{0x1fa80, 0x1fa88}, // Yo-yo ..(nil)
|
||||||
{0x1fa70, 0x1fa74}, // Ballet Shoes ..(nil)
|
{0x1fa90, 0x1fabd}, // Ringed Planet ..(nil)
|
||||||
{0x1fa78, 0x1fa7a}, // Drop Of Blood ..Stethoscope
|
{0x1fabf, 0x1fac5}, // (nil) ..Person With Crown
|
||||||
{0x1fa80, 0x1fa86}, // Yo-yo ..(nil)
|
{0x1face, 0x1fadb}, // (nil) ..(nil)
|
||||||
{0x1fa90, 0x1faa8}, // Ringed Planet ..(nil)
|
{0x1fae0, 0x1fae8}, // Melting Face ..(nil)
|
||||||
{0x1fab0, 0x1fab6}, // (nil) ..(nil)
|
{0x1faf0, 0x1faf8}, // Hand With Index Finger A..(nil)
|
||||||
{0x1fac0, 0x1fac2}, // (nil) ..(nil)
|
|
||||||
{0x1fad0, 0x1fad6}, // (nil) ..(nil)
|
|
||||||
{0x20000, 0x2fffd}, // Cjk Unified Ideograph-20..(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);
|
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");
|
withTerminalSized(5, 3).enterString("ABC\r\nFG");
|
||||||
assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,19 @@ public class TerminalTest extends TerminalTestCase {
|
|||||||
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
assertEquals(TextStyle.COLOR_INDEX_FOREGROUND, mTerminal.mForeColor);
|
||||||
assertEquals(TextStyle.COLOR_INDEX_BACKGROUND, mTerminal.mBackColor);
|
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:
|
// 256 colors:
|
||||||
enterString("\033[38;5;119m");
|
enterString("\033[38;5;119m");
|
||||||
assertEquals(119, mTerminal.mForeColor);
|
assertEquals(119, mTerminal.mForeColor);
|
||||||
|
|||||||
@@ -37,10 +37,14 @@ public abstract class TerminalTestCase extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clipboardText(String text) {
|
public void onCopyTextToClipboard(String text) {
|
||||||
clipboardPuts.add(text);
|
clipboardPuts.add(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPasteTextFromClipboard() {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBell() {
|
public void onBell() {
|
||||||
bellsRung++;
|
bellsRung++;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ android {
|
|||||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.2.0"
|
implementation "androidx.annotation:annotation:1.3.0"
|
||||||
api project(":terminal-emulator")
|
api project(":terminal-emulator")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation "junit:junit:4.13.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
task sourceJar(type: Jar) {
|
task sourceJar(type: Jar) {
|
||||||
@@ -45,7 +45,7 @@ afterEvaluate {
|
|||||||
from components.release
|
from components.release
|
||||||
groupId = 'com.termux'
|
groupId = 'com.termux'
|
||||||
artifactId = 'terminal-view'
|
artifactId = 'terminal-view'
|
||||||
version = '0.116'
|
version = '0.118.0'
|
||||||
artifact(sourceJar)
|
artifact(sourceJar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,9 +118,13 @@ public final class TerminalRenderer {
|
|||||||
final int columnWidthSinceLastRun = column - lastRunStartColumn;
|
final int columnWidthSinceLastRun = column - lastRunStartColumn;
|
||||||
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
||||||
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
||||||
|
boolean invertCursorTextColor = false;
|
||||||
|
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||||
|
invertCursorTextColor = true;
|
||||||
|
}
|
||||||
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||||
cursorColor, cursorShape, lastRunStyle, reverseVideo || lastRunInsideSelection);
|
cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
|
||||||
}
|
}
|
||||||
measuredWidthForRun = 0.f;
|
measuredWidthForRun = 0.f;
|
||||||
lastRunStyle = style;
|
lastRunStyle = style;
|
||||||
@@ -143,8 +147,12 @@ public final class TerminalRenderer {
|
|||||||
final int columnWidthSinceLastRun = columns - lastRunStartColumn;
|
final int columnWidthSinceLastRun = columns - lastRunStartColumn;
|
||||||
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
|
||||||
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
|
||||||
|
boolean invertCursorTextColor = false;
|
||||||
|
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||||
|
invertCursorTextColor = true;
|
||||||
|
}
|
||||||
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
||||||
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || lastRunInsideSelection);
|
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.termux.view;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -10,6 +11,7 @@ import android.graphics.Typeface;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@@ -19,6 +21,7 @@ import android.view.HapticFeedbackConstants;
|
|||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyCharacterMap;
|
import android.view.KeyCharacterMap;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.Menu;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewConfiguration;
|
import android.view.ViewConfiguration;
|
||||||
@@ -30,6 +33,7 @@ import android.view.inputmethod.EditorInfo;
|
|||||||
import android.view.inputmethod.InputConnection;
|
import android.view.inputmethod.InputConnection;
|
||||||
import android.widget.Scroller;
|
import android.widget.Scroller;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.termux.terminal.KeyHandler;
|
import com.termux.terminal.KeyHandler;
|
||||||
@@ -83,6 +87,12 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
private final boolean mAccessibilityEnabled;
|
private final boolean mAccessibilityEnabled;
|
||||||
|
|
||||||
|
/** The {@link KeyEvent} is generated from a virtual keyboard, like manually with the {@link KeyEvent#KeyEvent(int, int)} constructor. */
|
||||||
|
public final static int KEY_EVENT_SOURCE_VIRTUAL_KEYBOARD = KeyCharacterMap.VIRTUAL_KEYBOARD; // -1
|
||||||
|
|
||||||
|
/** The {@link KeyEvent} is generated from a non-physical device, like if 0 value is returned by {@link KeyEvent#getDeviceId()}. */
|
||||||
|
public final static int KEY_EVENT_SOURCE_SOFT_KEYBOARD = 0;
|
||||||
|
|
||||||
private static final String LOG_TAG = "TerminalView";
|
private static final String LOG_TAG = "TerminalView";
|
||||||
|
|
||||||
public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
|
public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
|
||||||
@@ -94,7 +104,7 @@ public final class TerminalView extends View {
|
|||||||
@Override
|
@Override
|
||||||
public boolean onUp(MotionEvent event) {
|
public boolean onUp(MotionEvent event) {
|
||||||
mScrollRemainder = 0.0f;
|
mScrollRemainder = 0.0f;
|
||||||
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !isSelectingText() && !scrolledWithFinger) {
|
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !event.isFromSource(InputDevice.SOURCE_MOUSE) && !isSelectingText() && !scrolledWithFinger) {
|
||||||
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
|
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
|
||||||
// for zooming.
|
// for zooming.
|
||||||
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
||||||
@@ -114,14 +124,9 @@ public final class TerminalView extends View {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
requestFocus();
|
requestFocus();
|
||||||
if (!mEmulator.isMouseTrackingActive()) {
|
|
||||||
if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
|
||||||
mClient.onSingleTapUp(event);
|
mClient.onSingleTapUp(event);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
|
public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
|
||||||
@@ -262,10 +267,19 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||||
|
// Ensure that inputType is only set if TerminalView is selected view with the keyboard and
|
||||||
|
// an alternate view is not selected, like an EditText. This is necessary if an activity is
|
||||||
|
// initially started with the alternate view or if activity is returned to from another app
|
||||||
|
// and the alternate view was the one selected the last time.
|
||||||
|
if (mClient.isTerminalViewSelected()) {
|
||||||
if (mClient.shouldEnforceCharBasedInput()) {
|
if (mClient.shouldEnforceCharBasedInput()) {
|
||||||
// Some keyboards seems do not reset the internal state on TYPE_NULL.
|
// Some keyboards seems do not reset the internal state on TYPE_NULL.
|
||||||
// Affects mostly Samsung stock keyboards.
|
// Affects mostly Samsung stock keyboards.
|
||||||
// https://github.com/termux/termux-app/issues/686
|
// https://github.com/termux/termux-app/issues/686
|
||||||
|
// However, this is not a valid value as per AOSP since `InputType.TYPE_CLASS_*` is
|
||||||
|
// not set and it logs a warning:
|
||||||
|
// W/InputAttributes: Unexpected input class: inputType=0x00080090 imeOptions=0x02000000
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/InputAttributes.java;l=79
|
||||||
outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
|
outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
|
||||||
} else {
|
} else {
|
||||||
// Using InputType.NULL is the most correct input type and avoids issues with other hacks.
|
// Using InputType.NULL is the most correct input type and avoids issues with other hacks.
|
||||||
@@ -277,6 +291,10 @@ public final class TerminalView extends View {
|
|||||||
// https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
|
// https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
|
||||||
outAttrs.inputType = InputType.TYPE_NULL;
|
outAttrs.inputType = InputType.TYPE_NULL;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Corresponds to android:inputType="text"
|
||||||
|
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
// Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen
|
// Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen
|
||||||
// keyboard on Android TV (see https://github.com/termux/termux-app/issues/221).
|
// keyboard on Android TV (see https://github.com/termux/termux-app/issues/221).
|
||||||
@@ -337,6 +355,10 @@ public final class TerminalView extends View {
|
|||||||
codePoint = firstChar;
|
codePoint = firstChar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check onKeyDown() for details.
|
||||||
|
if (mClient.readShiftKey())
|
||||||
|
codePoint = Character.toUpperCase(codePoint);
|
||||||
|
|
||||||
boolean ctrlHeld = false;
|
boolean ctrlHeld = false;
|
||||||
if (codePoint <= 31 && codePoint != 27) {
|
if (codePoint <= 31 && codePoint != 27) {
|
||||||
if (codePoint == '\n') {
|
if (codePoint == '\n') {
|
||||||
@@ -368,7 +390,7 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputCodePoint(codePoint, ctrlHeld, false);
|
inputCodePoint(KEY_EVENT_SOURCE_SOFT_KEYBOARD, codePoint, ctrlHeld, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,19 +413,29 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onScreenUpdated() {
|
public void onScreenUpdated() {
|
||||||
|
onScreenUpdated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onScreenUpdated(boolean skipScrolling) {
|
||||||
if (mEmulator == null) return;
|
if (mEmulator == null) return;
|
||||||
|
|
||||||
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
||||||
if (mTopRow < -rowsInHistory) mTopRow = -rowsInHistory;
|
if (mTopRow < -rowsInHistory) mTopRow = -rowsInHistory;
|
||||||
|
|
||||||
boolean skipScrolling = false;
|
if (isSelectingText() || mEmulator.isAutoScrollDisabled()) {
|
||||||
if (isSelectingText()) {
|
|
||||||
// Do not scroll when selecting text.
|
// Do not scroll when selecting text.
|
||||||
int rowShift = mEmulator.getScrollCounter();
|
int rowShift = mEmulator.getScrollCounter();
|
||||||
if (-mTopRow + rowShift > rowsInHistory) {
|
if (-mTopRow + rowShift > rowsInHistory) {
|
||||||
// .. unless we're hitting the end of history transcript, in which
|
// .. unless we're hitting the end of history transcript, in which
|
||||||
// case we abort text selection and scroll to end.
|
// case we abort text selection and scroll to end.
|
||||||
|
if (isSelectingText())
|
||||||
stopTextSelectionMode();
|
stopTextSelectionMode();
|
||||||
|
|
||||||
|
if (mEmulator.isAutoScrollDisabled()) {
|
||||||
|
mTopRow = -rowsInHistory;
|
||||||
|
skipScrolling = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
skipScrolling = true;
|
skipScrolling = true;
|
||||||
mTopRow -= rowShift;
|
mTopRow -= rowShift;
|
||||||
@@ -428,6 +460,14 @@ public final class TerminalView extends View {
|
|||||||
if (mAccessibilityEnabled) setContentDescription(getText());
|
if (mAccessibilityEnabled) setContentDescription(getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** This must be called by the hosting activity in {@link Activity#onContextMenuClosed(Menu)}
|
||||||
|
* when context menu for the {@link TerminalView} is started by
|
||||||
|
* {@link TextSelectionCursorController#ACTION_MORE} is closed. */
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
// Unset the stored text since it shouldn't be used anymore and should be cleared from memory
|
||||||
|
unsetStoredSelectedText();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the text size, which in turn sets the number of rows and columns.
|
* Sets the text size, which in turn sets the number of rows and columns.
|
||||||
*
|
*
|
||||||
@@ -454,10 +494,31 @@ public final class TerminalView extends View {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the zero indexed column and row of the terminal view for the
|
||||||
|
* position of the event.
|
||||||
|
*
|
||||||
|
* @param event The event with the position to get the column and row for.
|
||||||
|
* @param relativeToScroll If true the column number will take the scroll
|
||||||
|
* position into account. E.g. if scrolled 3 lines up and the event
|
||||||
|
* position is in the top left, column will be -3 if relativeToScroll is
|
||||||
|
* true and 0 if relativeToScroll is false.
|
||||||
|
* @return Array with the column and row.
|
||||||
|
*/
|
||||||
|
public int[] getColumnAndRow(MotionEvent event, boolean relativeToScroll) {
|
||||||
|
int column = (int) (event.getX() / mRenderer.mFontWidth);
|
||||||
|
int row = (int) ((event.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
|
||||||
|
if (relativeToScroll) {
|
||||||
|
row += mTopRow;
|
||||||
|
}
|
||||||
|
return new int[] { column, row };
|
||||||
|
}
|
||||||
|
|
||||||
/** Send a single mouse event code to the terminal. */
|
/** Send a single mouse event code to the terminal. */
|
||||||
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
|
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
|
||||||
int x = (int) (e.getX() / mRenderer.mFontWidth) + 1;
|
int[] columnAndRow = getColumnAndRow(e, false);
|
||||||
int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1;
|
int x = columnAndRow[0] + 1;
|
||||||
|
int y = columnAndRow[1] + 1;
|
||||||
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
|
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
|
||||||
if (mMouseStartDownTime == e.getDownTime()) {
|
if (mMouseStartDownTime == e.getDownTime()) {
|
||||||
x = mMouseScrollStartX;
|
x = mMouseScrollStartX;
|
||||||
@@ -517,11 +578,14 @@ public final class TerminalView extends View {
|
|||||||
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
||||||
return true;
|
return true;
|
||||||
} else if (event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
|
} else if (event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
|
||||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
ClipboardManager clipboardManager = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
ClipData clipData = clipboard.getPrimaryClip();
|
ClipData clipData = clipboardManager.getPrimaryClip();
|
||||||
if (clipData != null) {
|
if (clipData != null) {
|
||||||
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
ClipData.Item clipItem = clipData.getItemAt(0);
|
||||||
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
if (clipItem != null) {
|
||||||
|
CharSequence text = clipItem.coerceToText(getContext());
|
||||||
|
if (!TextUtils.isEmpty(text)) mEmulator.paste(text.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
|
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
|
||||||
switch (event.getAction()) {
|
switch (event.getAction()) {
|
||||||
@@ -533,7 +597,6 @@ public final class TerminalView extends View {
|
|||||||
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,6 +630,102 @@ public final class TerminalView extends View {
|
|||||||
return super.onKeyPreIme(keyCode, event);
|
return super.onKeyPreIme(keyCode, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key presses in software keyboards will generally NOT trigger this listener, although some
|
||||||
|
* may elect to do so in some situations. Do not rely on this to catch software key presses.
|
||||||
|
* Gboard calls this when shouldEnforceCharBasedInput() is disabled (InputType.TYPE_NULL) instead
|
||||||
|
* of calling commitText(), with deviceId=-1. However, Hacker's Keyboard, OpenBoard, LG Keyboard
|
||||||
|
* call commitText().
|
||||||
|
*
|
||||||
|
* This function may also be called directly without android calling it, like by
|
||||||
|
* `TerminalExtraKeys` which generates a KeyEvent manually which uses {@link KeyCharacterMap#VIRTUAL_KEYBOARD}
|
||||||
|
* as the device (deviceId=-1), as does Gboard. That would normally use mappings defined in
|
||||||
|
* `/system/usr/keychars/Virtual.kcm`. You can run `dumpsys input` to find the `KeyCharacterMapFile`
|
||||||
|
* used by virtual keyboard or hardware keyboard. Note that virtual keyboard device is not the
|
||||||
|
* same as software keyboard, like Gboard, etc. Its a fake device used for generating events and
|
||||||
|
* for testing.
|
||||||
|
*
|
||||||
|
* We handle shift key in `commitText()` to convert codepoint to uppercase case there with a
|
||||||
|
* call to {@link Character#toUpperCase(int)}, but here we instead rely on getUnicodeChar() for
|
||||||
|
* conversion of keyCode, for both hardware keyboard shift key (via effectiveMetaState) and
|
||||||
|
* `mClient.readShiftKey()`, based on value in kcm files.
|
||||||
|
* This may result in different behaviour depending on keyboard and android kcm files set for the
|
||||||
|
* InputDevice for the event passed to this function. This will likely be an issue for non-english
|
||||||
|
* languages since `Virtual.kcm` in english only by default or at least in AOSP. For both hardware
|
||||||
|
* shift key (via effectiveMetaState) and `mClient.readShiftKey()`, `getUnicodeChar()` is used
|
||||||
|
* for shift specific behaviour which usually is to uppercase.
|
||||||
|
*
|
||||||
|
* For fn key on hardware keyboard, android checks kcm files for hardware keyboards, which is
|
||||||
|
* `Generic.kcm` by default, unless a vendor specific one is defined. The event passed will have
|
||||||
|
* {@link KeyEvent#META_FUNCTION_ON} set. If the kcm file only defines a single character or unicode
|
||||||
|
* code point `\\uxxxx`, then only one event is passed with that value. However, if kcm defines
|
||||||
|
* a `fallback` key for fn or others, like `key DPAD_UP { ... fn: fallback PAGE_UP }`, then
|
||||||
|
* android will first pass an event with original key `DPAD_UP` and {@link KeyEvent#META_FUNCTION_ON}
|
||||||
|
* set. But this function will not consume it and android will pass another event with `PAGE_UP`
|
||||||
|
* and {@link KeyEvent#META_FUNCTION_ON} not set, which will be consumed.
|
||||||
|
*
|
||||||
|
* Now there are some other issues as well, firstly ctrl and alt flags are not passed to
|
||||||
|
* `getUnicodeChar()`, so modified key values in kcm are not used. Secondly, if the kcm file
|
||||||
|
* for other modifiers like shift or fn define a non-alphabet, like { fn: '\u0015' } to act as
|
||||||
|
* DPAD_LEFT, the `getUnicodeChar()` will correctly return `21` as the code point but action will
|
||||||
|
* not happen because the `handleKeyCode()` function that transforms DPAD_LEFT to `\033[D`
|
||||||
|
* escape sequence for the terminal to perform the left action would not be called since its
|
||||||
|
* called before `getUnicodeChar()` and terminal will instead get `21 0x15 Negative Acknowledgement`.
|
||||||
|
* The solution to such issues is calling `getUnicodeChar()` before the call to `handleKeyCode()`
|
||||||
|
* if user has defined a custom kcm file, like done in POC mentioned in #2237. Note that
|
||||||
|
* Hacker's Keyboard calls `commitText()` so don't test fn/shift with it for this function.
|
||||||
|
* https://github.com/termux/termux-app/pull/2237
|
||||||
|
* https://github.com/agnostic-apollo/termux-app/blob/terminal-code-point-custom-mapping/terminal-view/src/main/java/com/termux/view/TerminalView.java
|
||||||
|
*
|
||||||
|
* Key Character Map (kcm) and Key Layout (kl) files info:
|
||||||
|
* https://source.android.com/devices/input/key-character-map-files
|
||||||
|
* https://source.android.com/devices/input/key-layout-files
|
||||||
|
* https://source.android.com/devices/input/keyboard-devices
|
||||||
|
* AOSP kcm and kl files:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/data/keyboards
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/packages/InputDevices/res/raw
|
||||||
|
*
|
||||||
|
* KeyCodes:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/view/KeyEvent.java
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/master:frameworks/native/include/android/keycodes.h
|
||||||
|
*
|
||||||
|
* `dumpsys input`:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/services/inputflinger/reader/EventHub.cpp;l=1917
|
||||||
|
*
|
||||||
|
* Loading of keymap:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/services/inputflinger/reader/EventHub.cpp;l=1644
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/Keyboard.cpp;l=41
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/InputDevice.cpp
|
||||||
|
* OVERLAY keymaps for hardware keyboards may be combined as well:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=165
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=831
|
||||||
|
*
|
||||||
|
* Parse kcm file:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=727
|
||||||
|
* Parse key value:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=981
|
||||||
|
*
|
||||||
|
* `KeyEvent.getUnicodeChar()`
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/view/KeyEvent.java;l=2716
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/KeyCharacterMap.java;l=368
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/jni/android_view_KeyCharacterMap.cpp;l=117
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=231
|
||||||
|
*
|
||||||
|
* Keyboard layouts advertised by applications, like for hardware keyboards via #ACTION_QUERY_KEYBOARD_LAYOUTS
|
||||||
|
* Config is stored in `/data/system/input-manager-state.xml`
|
||||||
|
* https://github.com/ris58h/custom-keyboard-layout
|
||||||
|
* Loading from apps:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=1221
|
||||||
|
* Set:
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/hardware/input/InputManager.java;l=89
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/hardware/input/InputManager.java;l=543
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/apps/Settings/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java;l=167
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=1385
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/PersistentDataStore.java
|
||||||
|
* Get overlay keyboard layout
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=2158
|
||||||
|
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp;l=616
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
||||||
@@ -589,13 +748,15 @@ public final class TerminalView extends View {
|
|||||||
final int metaState = event.getMetaState();
|
final int metaState = event.getMetaState();
|
||||||
final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey();
|
final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey();
|
||||||
final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey();
|
final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey();
|
||||||
|
final boolean shiftDown = event.isShiftPressed() || mClient.readShiftKey();
|
||||||
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
||||||
|
|
||||||
int keyMod = 0;
|
int keyMod = 0;
|
||||||
if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL;
|
if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL;
|
||||||
if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT;
|
if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT;
|
||||||
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
if (shiftDown) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
||||||
if (event.isNumLockOn()) keyMod |= KeyHandler.KEYMOD_NUM_LOCK;
|
if (event.isNumLockOn()) keyMod |= KeyHandler.KEYMOD_NUM_LOCK;
|
||||||
|
// https://github.com/termux/termux-app/issues/731
|
||||||
if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) {
|
if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) {
|
||||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "handleKeyCode() took key event");
|
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "handleKeyCode() took key event");
|
||||||
return true;
|
return true;
|
||||||
@@ -611,6 +772,9 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
|
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
|
||||||
|
|
||||||
|
if (shiftDown) effectiveMetaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
|
||||||
|
if (mClient.readFnKey()) effectiveMetaState |= KeyEvent.META_FUNCTION_ON;
|
||||||
|
|
||||||
int result = event.getUnicodeChar(effectiveMetaState);
|
int result = event.getUnicodeChar(effectiveMetaState);
|
||||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
||||||
mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
|
mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
|
||||||
@@ -622,7 +786,7 @@ public final class TerminalView extends View {
|
|||||||
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
||||||
// If entered combining accent previously, write it out:
|
// If entered combining accent previously, write it out:
|
||||||
if (mCombiningAccent != 0)
|
if (mCombiningAccent != 0)
|
||||||
inputCodePoint(mCombiningAccent, controlDown, leftAltDown);
|
inputCodePoint(event.getDeviceId(), mCombiningAccent, controlDown, leftAltDown);
|
||||||
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
||||||
} else {
|
} else {
|
||||||
if (mCombiningAccent != 0) {
|
if (mCombiningAccent != 0) {
|
||||||
@@ -630,7 +794,7 @@ public final class TerminalView extends View {
|
|||||||
if (combinedChar > 0) result = combinedChar;
|
if (combinedChar > 0) result = combinedChar;
|
||||||
mCombiningAccent = 0;
|
mCombiningAccent = 0;
|
||||||
}
|
}
|
||||||
inputCodePoint(result, controlDown, leftAltDown);
|
inputCodePoint(event.getDeviceId(), result, controlDown, leftAltDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
||||||
@@ -638,14 +802,18 @@ public final class TerminalView extends View {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
public void inputCodePoint(int eventSource, int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
||||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) {
|
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) {
|
||||||
mClient.logInfo(LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
mClient.logInfo(LOG_TAG, "inputCodePoint(eventSource=" + eventSource + ", codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
||||||
+ leftAltDownFromEvent + ")");
|
+ leftAltDownFromEvent + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mTermSession == null) return;
|
if (mTermSession == null) return;
|
||||||
|
|
||||||
|
// Ensure cursor is shown when a key is pressed down like long hold on (arrow) keys
|
||||||
|
if (mEmulator != null)
|
||||||
|
mEmulator.setCursorBlinkState(true);
|
||||||
|
|
||||||
final boolean controlDown = controlDownFromEvent || mClient.readControlKey();
|
final boolean controlDown = controlDownFromEvent || mClient.readControlKey();
|
||||||
final boolean altDown = leftAltDownFromEvent || mClient.readAltKey();
|
final boolean altDown = leftAltDownFromEvent || mClient.readAltKey();
|
||||||
|
|
||||||
@@ -676,6 +844,8 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (codePoint > -1) {
|
if (codePoint > -1) {
|
||||||
|
// If not virtual or soft keyboard.
|
||||||
|
if (eventSource > KEY_EVENT_SOURCE_SOFT_KEYBOARD) {
|
||||||
// Work around bluetooth keyboards sending funny unicode characters instead
|
// Work around bluetooth keyboards sending funny unicode characters instead
|
||||||
// of the more normal ones from ASCII that terminal programs expect - the
|
// of the more normal ones from ASCII that terminal programs expect - the
|
||||||
// desire to input the original characters should be low.
|
// desire to input the original characters should be low.
|
||||||
@@ -690,6 +860,7 @@ public final class TerminalView extends View {
|
|||||||
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
|
// If left alt, send escape before the code point to make e.g. Alt+B and Alt+F work in readline:
|
||||||
mTermSession.writeCodePoint(altDown, codePoint);
|
mTermSession.writeCodePoint(altDown, codePoint);
|
||||||
@@ -702,6 +873,9 @@ public final class TerminalView extends View {
|
|||||||
if (mEmulator != null)
|
if (mEmulator != null)
|
||||||
mEmulator.setCursorBlinkState(true);
|
mEmulator.setCursorBlinkState(true);
|
||||||
|
|
||||||
|
if (handleKeyCodeAction(keyCode, keyMod))
|
||||||
|
return true;
|
||||||
|
|
||||||
TerminalEmulator term = mTermSession.getEmulator();
|
TerminalEmulator term = mTermSession.getEmulator();
|
||||||
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
|
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
|
||||||
if (code == null) return false;
|
if (code == null) return false;
|
||||||
@@ -709,6 +883,26 @@ public final class TerminalView extends View {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean handleKeyCodeAction(int keyCode, int keyMod) {
|
||||||
|
boolean shiftDown = (keyMod & KeyHandler.KEYMOD_SHIFT) != 0;
|
||||||
|
|
||||||
|
switch (keyCode) {
|
||||||
|
case KeyEvent.KEYCODE_PAGE_UP:
|
||||||
|
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||||
|
// shift+page_up and shift+page_down should scroll scrollback history instead of
|
||||||
|
// scrolling command history or changing pages
|
||||||
|
if (shiftDown) {
|
||||||
|
long time = SystemClock.uptimeMillis();
|
||||||
|
MotionEvent motionEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
|
||||||
|
doScroll(motionEvent, keyCode == KeyEvent.KEYCODE_PAGE_UP ? -1 : 1);
|
||||||
|
motionEvent.recycle();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a key is released in the view.
|
* Called when a key is released in the view.
|
||||||
*
|
*
|
||||||
@@ -1047,6 +1241,25 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the currently selected text if selecting. */
|
||||||
|
public String getSelectedText() {
|
||||||
|
if (isSelectingText() && mTextSelectionCursorController != null)
|
||||||
|
return mTextSelectionCursorController.getSelectedText();
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
|
||||||
|
@Nullable
|
||||||
|
public String getStoredSelectedText() {
|
||||||
|
return mTextSelectionCursorController != null ? mTextSelectionCursorController.getStoredSelectedText() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
|
||||||
|
public void unsetStoredSelectedText() {
|
||||||
|
if (mTextSelectionCursorController != null) mTextSelectionCursorController.unsetStoredSelectedText();
|
||||||
|
}
|
||||||
|
|
||||||
private ActionMode getTextSelectionActionMode() {
|
private ActionMode getTextSelectionActionMode() {
|
||||||
if (mTextSelectionCursorController != null) {
|
if (mTextSelectionCursorController != null) {
|
||||||
return mTextSelectionCursorController.getActionMode();
|
return mTextSelectionCursorController.getActionMode();
|
||||||
@@ -1108,6 +1321,7 @@ public final class TerminalView extends View {
|
|||||||
* Define functions required for long hold toolbar.
|
* Define functions required for long hold toolbar.
|
||||||
*/
|
*/
|
||||||
private final Runnable mShowFloatingToolbar = new Runnable() {
|
private final Runnable mShowFloatingToolbar = new Runnable() {
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (getTextSelectionActionMode() != null) {
|
if (getTextSelectionActionMode() != null) {
|
||||||
@@ -1116,6 +1330,7 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
private void showFloatingToolbar() {
|
private void showFloatingToolbar() {
|
||||||
if (getTextSelectionActionMode() != null) {
|
if (getTextSelectionActionMode() != null) {
|
||||||
int delay = ViewConfiguration.getDoubleTapTimeout();
|
int delay = ViewConfiguration.getDoubleTapTimeout();
|
||||||
@@ -1123,6 +1338,7 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
void hideFloatingToolbar() {
|
void hideFloatingToolbar() {
|
||||||
if (getTextSelectionActionMode() != null) {
|
if (getTextSelectionActionMode() != null) {
|
||||||
removeCallbacks(mShowFloatingToolbar);
|
removeCallbacks(mShowFloatingToolbar);
|
||||||
@@ -1131,7 +1347,7 @@ public final class TerminalView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateFloatingToolbarVisibility(MotionEvent event) {
|
public void updateFloatingToolbarVisibility(MotionEvent event) {
|
||||||
if (getTextSelectionActionMode() != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && getTextSelectionActionMode() != null) {
|
||||||
switch (event.getActionMasked()) {
|
switch (event.getActionMasked()) {
|
||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
hideFloatingToolbar();
|
hideFloatingToolbar();
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ public interface TerminalViewClient {
|
|||||||
|
|
||||||
boolean shouldUseCtrlSpaceWorkaround();
|
boolean shouldUseCtrlSpaceWorkaround();
|
||||||
|
|
||||||
|
boolean isTerminalViewSelected();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void copyModeChanged(boolean copyMode);
|
void copyModeChanged(boolean copyMode);
|
||||||
@@ -52,6 +54,11 @@ public interface TerminalViewClient {
|
|||||||
|
|
||||||
boolean readAltKey();
|
boolean readAltKey();
|
||||||
|
|
||||||
|
boolean readShiftKey();
|
||||||
|
|
||||||
|
boolean readFnKey();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user