Compare commits
857 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78be0e793e | ||
|
|
e547c15481 | ||
|
|
c621c35827 | ||
|
|
886e52dcff | ||
|
|
8e4da6cbcd | ||
|
|
bde9d01f76 | ||
|
|
5a511a2ba3 | ||
|
|
5c50964b1f | ||
|
|
dea8c9879e | ||
|
|
2034121798 | ||
|
|
23a900c433 | ||
|
|
93a7525d9b | ||
|
|
5670128236 | ||
|
|
dfd32435af | ||
|
|
49265160f8 | ||
|
|
70e1accafe | ||
|
|
1c7f9166f2 | ||
|
|
553913cde1 | ||
|
|
6bca378cec | ||
|
|
12f910c32d | ||
|
|
94c5f3674a | ||
|
|
28b9f93d13 | ||
|
|
69bebb5916 | ||
|
|
321350256e | ||
|
|
e5a9b99afe | ||
|
|
00f805f7ec | ||
|
|
d3c34ad1f5 | ||
|
|
59877a08d1 | ||
|
|
9c92251595 | ||
|
|
e408fdcc08 | ||
|
|
53c1a49b5b | ||
|
|
2aafcf8435 | ||
|
|
1c1af34374 | ||
|
|
52f18a73fb | ||
|
|
28f81f2cc7 | ||
|
|
4494bc66e4 | ||
|
|
679e0de044 | ||
|
|
80b495e50b | ||
|
|
69e5deedc7 | ||
|
|
7f36d7bbd0 | ||
|
|
b7b12ebe84 | ||
|
|
f77c88633e | ||
|
|
5f2ccca423 | ||
|
|
f0f6927273 | ||
|
|
0fb18c0c8b | ||
|
|
4dfed3320e | ||
|
|
7ac62c9840 | ||
|
|
fd80cdaf23 | ||
|
|
19c690d02b | ||
|
|
e119d34bca | ||
|
|
f545ebf0bd | ||
|
|
0b4bbaf23d | ||
|
|
e7dd0eeebe | ||
|
|
7ef9255437 | ||
|
|
7225e2b379 | ||
|
|
1ad038ece5 | ||
|
|
cb8b0225ca | ||
|
|
7620800cd5 | ||
|
|
6837db0015 | ||
|
|
e08e3b536e | ||
|
|
b711a467c1 | ||
|
|
d736b1eba5 | ||
|
|
58d577066a | ||
|
|
89a1e02713 | ||
|
|
6524a619f6 | ||
|
|
f8ccbb4953 | ||
|
|
31298b8857 | ||
|
|
11f5c0afd1 | ||
|
|
27dc211e2d | ||
|
|
2f828255ee | ||
|
|
339b2a24a2 | ||
|
|
6de3713049 | ||
|
|
79df863b75 | ||
|
|
af115c9966 | ||
|
|
1e30022ce7 | ||
|
|
4629276500 | ||
|
|
d42514d8c9 | ||
|
|
90c9a7b3bc | ||
|
|
e6dac93352 | ||
|
|
e4e638bd31 | ||
|
|
fe8c3ba216 | ||
|
|
4ecea144bb | ||
|
|
116b9b42d8 | ||
|
|
39c69db820 | ||
|
|
4d1851e6be | ||
|
|
596aa56b38 | ||
|
|
4850678d55 | ||
|
|
bc52a4e90c | ||
|
|
3e7b3604a4 | ||
|
|
f3f58c8fc7 | ||
|
|
4711094614 | ||
|
|
42ad3723fd | ||
|
|
b268b6edf7 | ||
|
|
b84854af92 | ||
|
|
cfebb3358d | ||
|
|
93e1b13278 | ||
|
|
0d4bfb7bd5 | ||
|
|
0aa5a123b7 | ||
|
|
2e156d4621 | ||
|
|
fdcf6cb6e1 | ||
|
|
01f2ed0892 | ||
|
|
c9abfe5438 | ||
|
|
8f9771adce | ||
|
|
b34f60b1b0 | ||
|
|
0fe608f91e | ||
|
|
5d911ef93f | ||
|
|
1d06ff9bf0 | ||
|
|
107927f5a1 | ||
|
|
d6eb5e3511 | ||
|
|
a6ae656c9f | ||
|
|
3af5730354 | ||
|
|
3306c3c2a2 | ||
|
|
cde0bd2246 | ||
|
|
354fe1948e | ||
|
|
ae1c9bacd6 | ||
|
|
d19cf435be | ||
|
|
1132028bd2 | ||
|
|
b7b4a0a8a2 | ||
|
|
ffd8687b4c | ||
|
|
bbb6f4471f | ||
|
|
b33b906784 | ||
|
|
567eeca782 | ||
|
|
3d46849673 | ||
|
|
6293f5f170 | ||
|
|
e5c5174f6f | ||
|
|
f1034c2e79 | ||
|
|
bbf03a0507 | ||
|
|
153818f7fb | ||
|
|
192b208883 | ||
|
|
824b3e657f | ||
|
|
a95e187b25 | ||
|
|
f888f35e35 | ||
|
|
24a5493ea5 | ||
|
|
0cd7cb8162 | ||
|
|
df4d8ac7e5 | ||
|
|
64fb2ce49b | ||
|
|
0c9b85a4f9 | ||
|
|
62a2104adc | ||
|
|
71dfefd4b7 | ||
|
|
682ce08314 | ||
|
|
c9a476caf7 | ||
|
|
9cee71004f | ||
|
|
81d97c3584 | ||
|
|
939338aaac | ||
|
|
067709bf4b | ||
|
|
69e4b575a8 | ||
|
|
07e6ecd3c3 | ||
|
|
325a6f7d66 | ||
|
|
cf5bb69fc8 | ||
|
|
18b004a2ba | ||
|
|
38323b1c2a | ||
|
|
8a5442f80d | ||
|
|
8598b92dea | ||
|
|
b2cd20c035 | ||
|
|
d4fc34ca2d | ||
|
|
c0323fe816 | ||
|
|
a32309827f | ||
|
|
ada678dfe2 | ||
|
|
cdbd38faaa | ||
|
|
d4653d0590 | ||
|
|
49f53f55f3 | ||
|
|
15eb56d4dd | ||
|
|
d7ea770d47 | ||
|
|
2a8d5e292d | ||
|
|
04da4b2268 | ||
|
|
34bacfd5b1 | ||
|
|
006d5abb78 | ||
|
|
480bad181b | ||
|
|
2afa4b4351 | ||
|
|
a2209ddd5e | ||
|
|
2cc6285a81 | ||
|
|
5dee839230 | ||
|
|
1ef8eb9219 | ||
|
|
d3ddb21716 | ||
|
|
8e80e889f0 | ||
|
|
4e5d14e4a2 | ||
|
|
0230698494 | ||
|
|
977cb34fc7 | ||
|
|
f62febbfb7 | ||
|
|
7ca20fdeb3 | ||
|
|
922f4f4ae5 | ||
|
|
df03f0b7d6 | ||
|
|
249f7c6b7c | ||
|
|
78a99fddfd | ||
|
|
dff2794501 | ||
|
|
3e0f74a894 | ||
|
|
2b3f681723 | ||
|
|
d3d6731d6f | ||
|
|
9bbcc08ff3 | ||
|
|
7aa957c684 | ||
|
|
eeb8554535 | ||
|
|
4eced52c5f | ||
|
|
b856e16998 | ||
|
|
1bdf9bf2e3 | ||
|
|
92b804dc9c | ||
|
|
1b5e5b56cb | ||
|
|
31371b5e3d | ||
|
|
ef1ab197b6 | ||
|
|
bccc35bc3f | ||
|
|
20d20f42c0 | ||
|
|
9d36e9adde | ||
|
|
3491956385 | ||
|
|
134e21765c | ||
|
|
c28990a176 | ||
|
|
131f481750 | ||
|
|
f393e9b2cf | ||
|
|
8612a1d0f8 | ||
|
|
7d53a147d8 | ||
|
|
5d6a98452a | ||
|
|
b4995ef9a7 | ||
|
|
ec7568d28e | ||
|
|
607ba3ee56 | ||
|
|
ae260fad9c | ||
|
|
d9b5344b24 | ||
|
|
bc02508102 | ||
|
|
f969c01f7e | ||
|
|
fb6e9b69ab | ||
|
|
c1a377d15d | ||
|
|
8c1c7ce727 | ||
|
|
2a940dfca6 | ||
|
|
2e7fd480f4 | ||
|
|
9e82561804 | ||
|
|
4427c1675d | ||
|
|
6f6d11fbb8 | ||
|
|
ff0440d7d2 | ||
|
|
c9e18e5b93 | ||
|
|
5e0b29bb6d | ||
|
|
73af5c2326 | ||
|
|
7da847a485 | ||
|
|
11a236a172 | ||
|
|
3b5d3114a6 | ||
|
|
66f15d2a08 | ||
|
|
0225a8b1fc | ||
|
|
d39972b3bf | ||
|
|
93b506a001 | ||
|
|
ebf2e472b3 | ||
|
|
e72841ba27 | ||
|
|
5fd91b4f92 | ||
|
|
10d6eaa5d1 | ||
|
|
319446fc15 | ||
|
|
36be41d0de | ||
|
|
ef9e406300 | ||
|
|
7b4acb53c9 | ||
|
|
dbf84773d4 | ||
|
|
324a69f7fa | ||
|
|
14c49867f7 | ||
|
|
395759cc0a | ||
|
|
ada5087f67 | ||
|
|
93a5bf8d29 | ||
|
|
356a442c6f | ||
|
|
9fd2cf9834 | ||
|
|
5a96075025 | ||
|
|
85b2c44ac7 | ||
|
|
108e4cb391 | ||
|
|
80858bab6b | ||
|
|
00194ebb90 | ||
|
|
f50d15d353 | ||
|
|
edcebf1336 | ||
|
|
fcfd131ccd | ||
|
|
ce5e9a9042 | ||
|
|
7884cb3bea | ||
|
|
d8fcc1f221 | ||
|
|
397a78f248 | ||
|
|
8baf53b09a | ||
|
|
4139bf9424 | ||
|
|
94deecb7b1 | ||
|
|
a4381b7827 | ||
|
|
866da75fa9 | ||
|
|
2b6e9ade07 | ||
|
|
6d1b0efd3b | ||
|
|
e03858f065 | ||
|
|
01929397cf | ||
|
|
496da3f877 | ||
|
|
407e4e003a | ||
|
|
fec61d315f | ||
|
|
f3a3a89f93 | ||
|
|
5b084f0851 | ||
|
|
a7eb173178 | ||
|
|
fa5117a098 | ||
|
|
2ef45c85b2 | ||
|
|
58b7a26b33 | ||
|
|
6e2a2ed946 | ||
|
|
7be1fe5555 | ||
|
|
0e94d52094 | ||
|
|
096dedffb1 | ||
|
|
b5d491a54c | ||
|
|
f6822d6c24 | ||
|
|
32c3ffd57b | ||
|
|
92570bee06 | ||
|
|
05bb399893 | ||
|
|
fe584940e1 | ||
|
|
78cdaef6d2 | ||
|
|
7b4a69f839 | ||
|
|
831aa69da8 | ||
|
|
a56cba6843 | ||
|
|
9228982632 | ||
|
|
38114784f1 | ||
|
|
b805f1486c | ||
|
|
7d31b7f480 | ||
|
|
a0298285e3 | ||
|
|
538a1d5cdf | ||
|
|
f1e973f0d2 | ||
|
|
b467b68f7b | ||
|
|
b895cbbb1e | ||
|
|
fc30eba247 | ||
|
|
b1d4c0c7fe | ||
|
|
7fe5bd32c8 | ||
|
|
43bbef9a11 | ||
|
|
eaea0f74a5 | ||
|
|
cb13a5a531 | ||
|
|
d267843e36 | ||
|
|
5ca67dd885 | ||
|
|
6b0d531758 | ||
|
|
1b3283bd69 | ||
|
|
2820f6a7b8 | ||
|
|
8925ae67bb | ||
|
|
4066c5df42 | ||
|
|
20aac6aa72 | ||
|
|
e1f799f9a1 | ||
|
|
4479de4b9b | ||
|
|
3a8f53a54c | ||
|
|
2f04a6186b | ||
|
|
ad64dd7c3d | ||
|
|
dfc4595ec5 | ||
|
|
8c80efb904 | ||
|
|
564079c7e9 | ||
|
|
5b6fd9b88c | ||
|
|
63bfe95848 | ||
|
|
af95a99854 | ||
|
|
25523ae224 | ||
|
|
665adb6895 | ||
|
|
eb7fda28df | ||
|
|
7063a3a9da | ||
|
|
6e02b99ff6 | ||
|
|
9aae665bfc | ||
|
|
52ce6cc94b | ||
|
|
f91168eff4 | ||
|
|
8faa5b2151 | ||
|
|
216cc10f3c | ||
|
|
cba80b6c0b | ||
|
|
850faa25dd | ||
|
|
a108b2bd6b | ||
|
|
b95823d7a8 | ||
|
|
382da7e8f7 | ||
|
|
ba9c118b50 | ||
|
|
531c32f3c9 | ||
|
|
db2f50c76e | ||
|
|
784affe39c | ||
|
|
b486d29d23 | ||
|
|
332f1104a3 | ||
|
|
5a70be1523 | ||
|
|
619552ec5c | ||
|
|
70580abd50 | ||
|
|
f191c35851 | ||
|
|
bd7ed28981 | ||
|
|
b68bd107c1 | ||
|
|
5075273362 | ||
|
|
04268f4c20 | ||
|
|
6f24628fd2 | ||
|
|
debbe44809 | ||
|
|
b2ff0e4051 | ||
|
|
9e7029b76a | ||
|
|
51370799c7 | ||
|
|
0910844896 | ||
|
|
f33ebf810f | ||
|
|
930029b5d2 | ||
|
|
33def928cf | ||
|
|
fc04a93990 | ||
|
|
8d302aa9fe | ||
|
|
2224d917a3 | ||
|
|
664ec43f94 | ||
|
|
6cf742460c | ||
|
|
72981fb981 | ||
|
|
2c5534e2c1 | ||
|
|
5b32540635 | ||
|
|
db3ff7b24a | ||
|
|
5f71e3e73a | ||
|
|
3e04ea4cb0 | ||
|
|
0af823607a | ||
|
|
4d9c0c315e | ||
|
|
9c32935ca2 | ||
|
|
669c336e2f | ||
|
|
35842cf4a6 | ||
|
|
b086270a5a | ||
|
|
f39f06a540 | ||
|
|
58440bc88d | ||
|
|
9f438e2912 | ||
|
|
f794bfcadc | ||
|
|
b6d7831646 | ||
|
|
d212198e30 | ||
|
|
c2843897ac | ||
|
|
c4c4912a7e | ||
|
|
38a3319ca2 | ||
|
|
7e13b8aa2e | ||
|
|
6dca19ae00 | ||
|
|
2659c06c5d | ||
|
|
9b7c7102b2 | ||
|
|
1819087ca0 | ||
|
|
366a61f052 | ||
|
|
6e224cabcf | ||
|
|
8c8fa96133 | ||
|
|
537f2ed97e | ||
|
|
0e23315c41 | ||
|
|
2cde986419 | ||
|
|
d28939810c | ||
|
|
93724b7aa6 | ||
|
|
5d06f040e8 | ||
|
|
ed9afa082a | ||
|
|
9703bd31ad | ||
|
|
9749f25eba | ||
|
|
453b838b24 | ||
|
|
9fdf2a49fd | ||
|
|
3270506bff | ||
|
|
a240f4cf45 | ||
|
|
4647beb0d2 | ||
|
|
d92e806461 | ||
|
|
b75cf0bb84 | ||
|
|
f928efed4e | ||
|
|
36db64d585 | ||
|
|
b8f0430699 | ||
|
|
90e6260d5e | ||
|
|
566d656c16 | ||
|
|
b729085d52 | ||
|
|
a94bcb02f1 | ||
|
|
e28be01dc2 | ||
|
|
7f7c1efac1 | ||
|
|
76df44e6bb | ||
|
|
fd13f3f98d | ||
|
|
269c3cafb0 | ||
|
|
662b5cace6 | ||
|
|
3c01b09fff | ||
|
|
1ce2db9929 | ||
|
|
c987ff718a | ||
|
|
9e295b105c | ||
|
|
37b3a9f8db | ||
|
|
490853e427 | ||
|
|
9ebedc4740 | ||
|
|
82730d9e08 | ||
|
|
3d2756f376 | ||
|
|
6db51bdec0 | ||
|
|
c238c8bddb | ||
|
|
60e1556871 | ||
|
|
e47bd31681 | ||
|
|
951df6b4e2 | ||
|
|
fcd3bc1133 | ||
|
|
f4db9ff212 | ||
|
|
87841886d4 | ||
|
|
cf883f5f05 | ||
|
|
677d75e173 | ||
|
|
cdccc2c433 | ||
|
|
070436a6ed | ||
|
|
69ded4b33e | ||
|
|
e5a8c0eb4a | ||
|
|
29815fb3f0 | ||
|
|
e930ac08aa | ||
|
|
3b969a0077 | ||
|
|
ae717d8f5f | ||
|
|
6c55c3c1be | ||
|
|
c0a0822843 | ||
|
|
08b08d1c47 | ||
|
|
c76cec186a | ||
|
|
5ba3f7cf6d | ||
|
|
468f878a38 | ||
|
|
c50a367063 | ||
|
|
4189f598b9 | ||
|
|
fdb3764f5c | ||
|
|
cb67e805de | ||
|
|
3f83368f42 | ||
|
|
8c7462678f | ||
|
|
6f11390cc4 | ||
|
|
33d390a228 | ||
|
|
937eb350b2 | ||
|
|
3b4ece6bd8 | ||
|
|
35a4fdacbe | ||
|
|
0332779d6a | ||
|
|
f19575b942 | ||
|
|
6a95809da3 | ||
|
|
ac8dab70ef | ||
|
|
243285f7c2 | ||
|
|
b6182216d5 | ||
|
|
e206121bbc | ||
|
|
deceffad00 | ||
|
|
c19909cef1 | ||
|
|
5b7e40638c | ||
|
|
a3673d1af5 | ||
|
|
d5f9cf85c9 | ||
|
|
549f09573f | ||
|
|
94e5bc86fb | ||
|
|
370ac2bd89 | ||
|
|
7041f41981 | ||
|
|
dd6a21257b | ||
|
|
445da0c4ea | ||
|
|
92980824b1 | ||
|
|
7d42b07a32 | ||
|
|
e502b9169f | ||
|
|
9f2d887ca0 | ||
|
|
e6aacc5f08 | ||
|
|
ff935be54a | ||
|
|
4e76162346 | ||
|
|
5fa4f2647b | ||
|
|
81b5889a26 | ||
|
|
514f59258a | ||
|
|
9f79393aa5 | ||
|
|
e49d514236 | ||
|
|
4e1462326c | ||
|
|
b0f1773a92 | ||
|
|
af9f28c010 | ||
|
|
fef0c66868 | ||
|
|
7a5da83ce2 | ||
|
|
97f01387b9 | ||
|
|
012ff83a06 | ||
|
|
a082c63849 | ||
|
|
8c27084086 | ||
|
|
f4fb45cbd6 | ||
|
|
0605fc4843 | ||
|
|
3bbd61f9d7 | ||
|
|
42fc0ea1cd | ||
|
|
6218d08037 | ||
|
|
10fe238ddb | ||
|
|
fe41cd486f | ||
|
|
bda80547ad | ||
|
|
70a786613d | ||
|
|
e4220a7ab1 | ||
|
|
bd45837d93 | ||
|
|
ddf5341b86 | ||
|
|
85a48a79a3 | ||
|
|
e3512c957d | ||
|
|
330301899a | ||
|
|
2a36b915cb | ||
|
|
c986a46fb9 | ||
|
|
ed8bd4b569 | ||
|
|
7f4df8328c | ||
|
|
ad1ce585d9 | ||
|
|
158908a3bf | ||
|
|
d8f066dec4 | ||
|
|
743225ab6a | ||
|
|
b38ae24908 | ||
|
|
70c1bddae0 | ||
|
|
4df285013e | ||
|
|
1e4fa8c045 | ||
|
|
31f2dc35bc | ||
|
|
fbb048ce56 | ||
|
|
8c3a30027e | ||
|
|
a8c270f3c1 | ||
|
|
829414ac19 | ||
|
|
8e0b8623bc | ||
|
|
e2bda2bb52 | ||
|
|
8c7b1a60d2 | ||
|
|
cb0710bb83 | ||
|
|
b54c8276b4 | ||
|
|
7056945f5d | ||
|
|
063ef02f2b | ||
|
|
6e6e4fd212 | ||
|
|
406438e391 | ||
|
|
d1977479bd | ||
|
|
b4563023f6 | ||
|
|
095ed8b54f | ||
|
|
af445f9618 | ||
|
|
82f977fbf1 | ||
|
|
4b52cfbb93 | ||
|
|
b61da23be7 | ||
|
|
35df3f72c9 | ||
|
|
d584502fef | ||
|
|
a691333c5e | ||
|
|
e053cf82ca | ||
|
|
076176a5f6 | ||
|
|
08c72f9a65 | ||
|
|
7593ddde38 | ||
|
|
6dce32aece | ||
|
|
5ff801c914 | ||
|
|
221046732b | ||
|
|
07d6c9bd5e | ||
|
|
a9eddce672 | ||
|
|
26c95f1397 | ||
|
|
4eca639212 | ||
|
|
beb8a004e6 | ||
|
|
3693e3c1b6 | ||
|
|
99e8ffcf90 | ||
|
|
0807600a2d | ||
|
|
f74293e8fb | ||
|
|
b99d092305 | ||
|
|
af7515247b | ||
|
|
ec77be00dc | ||
|
|
a854960476 | ||
|
|
9db8948f23 | ||
|
|
c24167f6a5 | ||
|
|
49c051c8b7 | ||
|
|
55efdb2f56 | ||
|
|
d03e420e75 | ||
|
|
06968a9295 | ||
|
|
244248b1a0 | ||
|
|
7187ed6950 | ||
|
|
b3eabd9bad | ||
|
|
b51dd4f558 | ||
|
|
f88b9c4629 | ||
|
|
f0eeb4781b | ||
|
|
57a3a9b111 | ||
|
|
8df73a3006 | ||
|
|
963663e0cd | ||
|
|
68a83ccf37 | ||
|
|
db13ea02b6 | ||
|
|
61a44dbfa8 | ||
|
|
aaa92279ca | ||
|
|
365f9723cc | ||
|
|
fdae272214 | ||
|
|
b7864d6ac2 | ||
|
|
d1f0c76db3 | ||
|
|
5652624fc2 | ||
|
|
35a9101f84 | ||
|
|
2b6a10712b | ||
|
|
c80e126b8f | ||
|
|
89048274dd | ||
|
|
d3b4d35b2a | ||
|
|
4080f41cc7 | ||
|
|
b4e2c99d46 | ||
|
|
c5923201a4 | ||
|
|
bafd21bb39 | ||
|
|
0f20fab02c | ||
|
|
c5ae5bb06a | ||
|
|
bbd46a763c | ||
|
|
dc145d65f8 | ||
|
|
ce2d1c0d88 | ||
|
|
f2f7f963e6 | ||
|
|
2e53ef038e | ||
|
|
8c82f43dce | ||
|
|
3a16f461e7 | ||
|
|
594a5dfe25 | ||
|
|
d5e007dbb3 | ||
|
|
f4335d3824 | ||
|
|
90b6f93697 | ||
|
|
3bb2849a88 | ||
|
|
17c4a45212 | ||
|
|
6d9ffb6922 | ||
|
|
8472fce8ba | ||
|
|
69d954a583 | ||
|
|
ec1087d56f | ||
|
|
be6a73d862 | ||
|
|
80c81b274d | ||
|
|
cd9dbac548 | ||
|
|
0c837796f0 | ||
|
|
3417e37e8d | ||
|
|
31ba36e0fa | ||
|
|
c444f1fd28 | ||
|
|
9f36ed06b8 | ||
|
|
c2ab5bcd50 | ||
|
|
48fab33b79 | ||
|
|
8d7a67645b | ||
|
|
186b49d429 | ||
|
|
40a2775f52 | ||
|
|
f802d6001d | ||
|
|
50f66a12da | ||
|
|
d8e6fd21d1 | ||
|
|
0964d83572 | ||
|
|
cee0466dd7 | ||
|
|
de4f334e24 | ||
|
|
ab205ae05b | ||
|
|
b29b24f507 | ||
|
|
b3472e9e62 | ||
|
|
ab59e08959 | ||
|
|
8b566485e8 | ||
|
|
231c02a0c7 | ||
|
|
65f30e77ba | ||
|
|
686677ae45 | ||
|
|
85037a75a6 | ||
|
|
6025afc2c0 | ||
|
|
a4e4f76775 | ||
|
|
02af113dda | ||
|
|
fc4ef838bf | ||
|
|
86bd3ca21b | ||
|
|
367398bafb | ||
|
|
dd502e55f8 | ||
|
|
798125ef7a | ||
|
|
5126f06e06 | ||
|
|
0bdbf314ef | ||
|
|
2deb9899bd | ||
|
|
694ccc38c4 | ||
|
|
c949940374 | ||
|
|
d09f70de1e | ||
|
|
ac338ce2c5 | ||
|
|
092a83a688 | ||
|
|
3533b13de8 | ||
|
|
d68a0f05be | ||
|
|
9e5293a08e | ||
|
|
3f04a0a0d5 | ||
|
|
e558f824c5 | ||
|
|
8ff72ddd8b | ||
|
|
3e05356a69 | ||
|
|
82673d3f82 | ||
|
|
f2757fdb76 | ||
|
|
81fb669d0e | ||
|
|
4a45b1b617 | ||
|
|
62b08b3cf1 | ||
|
|
1e83ea3151 | ||
|
|
9c42fdb3d6 | ||
|
|
42e3163e92 | ||
|
|
68912139f6 | ||
|
|
edc92bbcbb | ||
|
|
d7520642de | ||
|
|
adae111d5c | ||
|
|
dc9272790b | ||
|
|
05ea2ea238 | ||
|
|
ad293562bc | ||
|
|
f9a565d1e0 | ||
|
|
5b3909cb73 | ||
|
|
7913e765d5 | ||
|
|
ed3a3269d8 | ||
|
|
dc3994d2cf | ||
|
|
c972377bd1 | ||
|
|
d4be782c03 | ||
|
|
c29909726c | ||
|
|
45bac89298 | ||
|
|
c06770c353 | ||
|
|
52a627efc8 | ||
|
|
2e9383720c | ||
|
|
58f9f1be71 | ||
|
|
058441dda6 | ||
|
|
888802a519 | ||
|
|
1a09b6d2a6 | ||
|
|
9d3f7734a1 | ||
|
|
61f766b59f | ||
|
|
3f08376881 | ||
|
|
cf06e70429 | ||
|
|
0714e435cb | ||
|
|
c26315185f | ||
|
|
1be5139253 | ||
|
|
adc43c40c5 | ||
|
|
779b1ca1f8 | ||
|
|
e375f99b0c | ||
|
|
0ac55cd77a | ||
|
|
4aefa5049b | ||
|
|
5b14124258 | ||
|
|
1a9c38374c | ||
|
|
eefd504f08 | ||
|
|
19f838e3d1 | ||
|
|
63adb2b132 | ||
|
|
7d3a988d2f | ||
|
|
5cd705d6d1 | ||
|
|
af8b269b51 | ||
|
|
8c220eaaea | ||
|
|
0142e1ed0e | ||
|
|
ac0a349ed9 | ||
|
|
548b0916b6 | ||
|
|
009de5a3ee | ||
|
|
41d0d60017 | ||
|
|
12ac0fa73c | ||
|
|
835dfc0276 | ||
|
|
f60316835f | ||
|
|
f74a091be6 | ||
|
|
dd6cb5221d | ||
|
|
cab6df5c0e | ||
|
|
f6f0809558 | ||
|
|
11ed7e45d8 | ||
|
|
ed1874db05 | ||
|
|
cb60803a80 | ||
|
|
fc92a27cb2 | ||
|
|
29e62e608f | ||
|
|
8a7f93d722 | ||
|
|
420683fe65 | ||
|
|
a3256ed551 | ||
|
|
57add98e3c | ||
|
|
6e5c04e04f | ||
|
|
528a05ef61 | ||
|
|
cb2f892dc5 | ||
|
|
8b6e8d7fdd | ||
|
|
d0eeaa9fc3 | ||
|
|
b793913481 | ||
|
|
d48b438c40 | ||
|
|
11afe895e1 | ||
|
|
bc96f71a2d | ||
|
|
87dfded5e6 | ||
|
|
7c0ae4cb54 | ||
|
|
b917acbbfa | ||
|
|
7d9d6fb797 | ||
|
|
74040dd37f | ||
|
|
e94f06d0f7 | ||
|
|
af21b6dc3e | ||
|
|
e0e8257f1c | ||
|
|
743b067cae | ||
|
|
23333c074a | ||
|
|
f11644fa51 | ||
|
|
212be59fca | ||
|
|
e3a1f8224f | ||
|
|
4f40d5a26a | ||
|
|
df92896eef | ||
|
|
4c93cb42f1 | ||
|
|
34afb9de43 | ||
|
|
b6ea29d260 | ||
|
|
289d58a2f0 | ||
|
|
0501ce924b | ||
|
|
d12256f5e5 | ||
|
|
fcbc036f92 | ||
|
|
70d5839334 | ||
|
|
9c19540759 | ||
|
|
9fe0e49473 | ||
|
|
357b17e972 | ||
|
|
6334470f81 | ||
|
|
b8cdd59c68 | ||
|
|
6cf36fffd7 | ||
|
|
f10ecd4db5 | ||
|
|
26457e8443 | ||
|
|
6702846c7c | ||
|
|
0c6180bbb1 | ||
|
|
fcf07f6a19 | ||
|
|
e272e3b3b2 | ||
|
|
cdf0e72145 | ||
|
|
70245eb78c | ||
|
|
ee7631dfac | ||
|
|
5ecf5d12d1 | ||
|
|
0c8cd90f4e | ||
|
|
e1ea68913f | ||
|
|
dde854eba7 | ||
|
|
07a4607c04 | ||
|
|
883be37b98 | ||
|
|
d939d3d927 | ||
|
|
a0fa51eb92 | ||
|
|
755513bb33 | ||
|
|
8ad7a6669c | ||
|
|
60f7aada9e | ||
|
|
6aa0492434 | ||
|
|
019aa44837 | ||
|
|
8d3d5e147f | ||
|
|
44197b90e2 | ||
|
|
794c7ee333 | ||
|
|
0457ddbc69 | ||
|
|
283792af5e | ||
|
|
be7cfa603a | ||
|
|
3480bf7346 | ||
|
|
8314a2756c | ||
|
|
4de82d9fe0 | ||
|
|
d658e16801 | ||
|
|
26dcd5af88 | ||
|
|
8056013082 | ||
|
|
8e90545c4b | ||
|
|
426ddbacbd | ||
|
|
7e1f8a551f | ||
|
|
e169af0447 | ||
|
|
a2cb3fafee | ||
|
|
166710f14a | ||
|
|
c1a9b7726f | ||
|
|
afb339e9d8 | ||
|
|
64c23f498f | ||
|
|
1dc92b2a12 | ||
|
|
990a957383 | ||
|
|
7bb64d724c | ||
|
|
4609dd71c6 | ||
|
|
8d00f22d4c | ||
|
|
5532421ab2 | ||
|
|
d2b27978e2 | ||
|
|
30b05e9ab2 | ||
|
|
c350318c77 | ||
|
|
c9b49cef58 | ||
|
|
f9c642c672 | ||
|
|
c0a5e5f57a | ||
|
|
dfdc9b37e1 | ||
|
|
dfb22e6050 | ||
|
|
b95d84fe13 | ||
|
|
a73228b109 |
@@ -14,3 +14,7 @@ insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.y{a,}ml]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
5
.gitattributes
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
* text=auto
|
||||
*.bat text eol=crlf
|
||||
*.gradle text eol=lf
|
||||
*.mk text eol=lf
|
||||
*.sh text eol=lf
|
||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
patreon: termux
|
||||
custom: https://paypal.me/fornwall
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve Termux application
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
IMPORTANT:
|
||||
|
||||
1. Support of Android 5.x - 6.x is finished.
|
||||
2. Fill the template AFTER comments.
|
||||
-->
|
||||
|
||||
**Problem description**
|
||||
<!--
|
||||
A clear and concise description of what the problem is.
|
||||
You may post screenshots in addition to description.
|
||||
-->
|
||||
|
||||
**Steps to reproduce**
|
||||
<!--
|
||||
Steps to reproduce the behavior. Please post all necessary
|
||||
commands that are needed to reproduce the issue.
|
||||
-->
|
||||
|
||||
**Expected behavior**
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
**Additional information**
|
||||
|
||||
* Termux application version:
|
||||
* Android OS version:
|
||||
* Device model:
|
||||
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature for Termux application
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
IMPORTANT:
|
||||
|
||||
1. Support of Android 5.x - 6.x is finished.
|
||||
2. Fill the template AFTER comments.
|
||||
-->
|
||||
|
||||
**Feature description**
|
||||
<!--
|
||||
Describe the feature and why you want it.
|
||||
-->
|
||||
|
||||
**Reference implementation**
|
||||
|
||||
Does another app/terminal emulator have this feature?
|
||||
Provide links to more background information.
|
||||
26
.github/workflows/debug_build.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- android-10
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- android-10
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: |
|
||||
./gradlew assembleDebug
|
||||
- name: Store generated APK file
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: termux-app
|
||||
path: ./app/build/outputs/apk/debug
|
||||
19
.github/workflows/gradle-wrapper-validation.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: "Validate Gradle Wrapper"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- android-10
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- android-10
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
21
.github/workflows/run_tests.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Unit tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- android-10
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- android-10
|
||||
|
||||
jobs:
|
||||
testing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Execute tests
|
||||
run: |
|
||||
./gradlew test
|
||||
19
.gitignore
vendored
@@ -6,6 +6,9 @@
|
||||
build/
|
||||
*.apk
|
||||
*.so
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
*.zip
|
||||
|
||||
# Crashlytics configuations
|
||||
com_crashlytics_export_strings.xml
|
||||
@@ -19,19 +22,8 @@ local.properties
|
||||
# Signing files
|
||||
.signing/
|
||||
|
||||
# User-specific configurations
|
||||
.idea/libraries/
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/.name
|
||||
.idea/compiler.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
.idea/encodings.xml
|
||||
.idea/misc.xml
|
||||
.idea/modules.xml
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/vcs.xml
|
||||
.idea/dictionaries/
|
||||
# Intellij
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# OS-specific files
|
||||
@@ -40,5 +32,6 @@ local.properties
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
.swp
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
229
.idea/codeStyleSettings.xml
generated
@@ -1,229 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectCodeStyleSettingsManager">
|
||||
<option name="PER_PROJECT_SETTINGS">
|
||||
<value>
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
||||
<value />
|
||||
</option>
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
<option name="RIGHT_MARGIN" value="100" />
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="USE_CUSTOM_SETTINGS" value="true" />
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<Objective-C-extensions>
|
||||
<option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" />
|
||||
<option name="RELEASE_STYLE" value="IVAR" />
|
||||
<option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" />
|
||||
<file>
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
|
||||
</file>
|
||||
<class>
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
|
||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
|
||||
</class>
|
||||
<extensions>
|
||||
<pair source="cpp" header="h" />
|
||||
<pair source="c" header="h" />
|
||||
</extensions>
|
||||
</Objective-C-extensions>
|
||||
<XML>
|
||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||
</XML>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_width</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_height</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:width</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:height</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</value>
|
||||
</option>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
|
||||
</component>
|
||||
</project>
|
||||
24
.idea/gradle.xml
generated
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="disableWrapperSourceDistributionNotification" value="true" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="myModules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
71
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,71 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AndroidLintLogConditional" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintNegativeMargin" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="DeprecatedAPI" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="DuplicateSwitchCase" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="EmptyStatementBody" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="m_reportEmptyBlocks" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="EndlessLoop" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="EqualityInConditionalOperator" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="Finalize" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreTrivialFinalizers" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="FinalizeNotProtected" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="FormatSpecifiers" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="FunctionImplicitDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="HidesUpperScope" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="HidingNonVirtualFunction" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ImplicitIntegerAndEnumConversion" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ImplicitPointerAndIntegerConversion" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="IncompatibleEnums" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="IncompatibleInitializers" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="IncompatiblePointers" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="InstanceofChain" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoreInstanceofOnLibraryClasses" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="KRUnspecifiedParameters" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="LocalValueEscapesScope" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
|
||||
<option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="MissingReturn" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="MissingSwitchCase" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="NotImplementedFunctions" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="NotInitializedVariable" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="NotSuperclass" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="OCDFAInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="OCLoopDoesntUseConditionVariableInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="OCSimplifyInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="OCUnusedGlobalDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="OCUnusedMacroInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="OCUnusedStructInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="OCUnusedTemplateParameterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="OnDemandImport" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PrivateMemberAccessBetweenOuterAndInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="ResourceNotFoundInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SamePackageImport" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="SignednessMismatch" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="UnnecessaryFullyQualifiedName" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="m_ignoreJavadoc" value="false" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="UnreachableCode" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnusedExpressionResult" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnusedImportStatement" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnusedLocalVariable" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnusedLocalization" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnusedParameter" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnusedValue" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ValueMayNotFitIntoReceiver" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="VariableNotUsedInsideIf" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
7
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -1,7 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="PROJECT_PROFILE" value="Project Default" />
|
||||
<option name="USE_PROJECT_PROFILE" value="true" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
30
.travis.yml
@@ -1,30 +0,0 @@
|
||||
sudo: false
|
||||
language: android
|
||||
jdk: oraclejdk8
|
||||
|
||||
env:
|
||||
global:
|
||||
# The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
|
||||
# via the "travis encrypt" command using the project repo's public key
|
||||
- secure: "ACnFJxw0VusS2lnGXL+epP/CNJmftWS39YcPdgN2EurWw5ZfXSo7vi+zpMB+11IBS3LQyLFFUambi2N9L4lbReZkHVkoVcZFGZlwbXNTAeqT8CABPTcuOyEOZU4bJwqeYU87ztYipENMLNECaZrgWx5odbWLKnSJQw7Zkb4ArCstfXfYk9u8q49ThRxQyGwHW2xKp1an5aa+3Y6IY+ywsSHw6AvXbyFH078Kolxy86caagczcfmKcMi15QYzwAvFggUphvsO3M5PHJMQXuaNlQxDcQRGUEXsK8aZE0dPH5PB97SFjDALZqI7NEpjZAk5htWjX48ssW064LDbjcBg/ZLgDd8R8uhA159NVZgvcnP2czCn6pmggx1sW5MBmcj7i+bJS2ejaMO+KoovWlVvsch742H5QR6rQaNkjDZRsGVLYvJaR1gBLs898UoT1hcHWoqLVR22r2VFo7OWWCRfNRvZuZDR2HIrYRdFvn8P3nWVMkvXwgsOlxWG5sN+yQqW+6lZS7hivsFhtYs4CkRdoZIan3Qvi/CkY8Lg+ESkZ3IJ0NnId8qOWH+8Xl1sqZ7xlsWTd1sYYHlpvkdvqw1HNLP22EpwwKW5Kb5zBEd/qs3o1OO0Tqa0MR6JpgGdHHRk1iZ25+qTfRVP06vO2RXsgAx4SZfO7DyB0QZn8tGNMMI="
|
||||
|
||||
android:
|
||||
components:
|
||||
- platform-tools
|
||||
- tools
|
||||
- build-tools-24.0.1
|
||||
- android-24
|
||||
- extra-android-m2repository
|
||||
|
||||
script:
|
||||
- ./gradlew testDebugUnitTest
|
||||
|
||||
addons:
|
||||
coverity_scan:
|
||||
project:
|
||||
name: "termux/termux-app"
|
||||
description: "Terminal emulator and Linux environment for Android"
|
||||
notification_email: fredrik@fornwall.net
|
||||
build_command_prepend: "./gradlew clean"
|
||||
build_command: "./gradlew assemble"
|
||||
branch_pattern: coverity_scan
|
||||
6
LICENSE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
The `termux/termux-app` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
### Exceptions
|
||||
|
||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
||||
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
|
||||
170
README.md
@@ -1,39 +1,147 @@
|
||||
Termux app
|
||||
==========
|
||||
[](https://travis-ci.org/termux/termux-app)
|
||||
# Termux application
|
||||
|
||||
[](https://github.com/termux/termux-app/actions)
|
||||
[](https://github.com/termux/termux-app/actions)
|
||||
[](https://gitter.im/termux/termux)
|
||||
[](https://jitpack.io/#termux/termux-app)
|
||||
|
||||
|
||||
Termux is an Android terminal app and Linux environment.
|
||||
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
||||
|
||||
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux)
|
||||
* [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux)
|
||||
* [termux.com](http://termux.com)
|
||||
* [Termux Help](http://termux.com/help/)
|
||||
* [Termux app on GitHub](https://github.com/termux/termux-app)
|
||||
* [Termux packages on GitHub](https://github.com/termux/termux-packages)
|
||||
* [Termux Google+ community](http://termux.com/community/)
|
||||
Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages).
|
||||
|
||||
License
|
||||
=======
|
||||
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html). Contains code from `Terminal Emulator for Android` which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
|
||||
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.
|
||||
|
||||
Building JNI libraries
|
||||
======================
|
||||
Execute the `build-jnilibs.sh` script to build the required JNI libraries.
|
||||
***
|
||||
|
||||
Terminal resources
|
||||
==================
|
||||
* [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||
* [vt100.net](http://vt100.net/)
|
||||
* [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
||||
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.**
|
||||
|
||||
Terminal emulators
|
||||
==================
|
||||
* VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
|
||||
* iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
|
||||
* Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
|
||||
* hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
|
||||
* xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
||||
* Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
||||
* Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
||||
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
|
||||
|
||||
***
|
||||
|
||||
### Contents
|
||||
- [Termux App and Plugins](#Termux-App-and-Plugins)
|
||||
- [Installation](#Installation)
|
||||
- [Uninstallation](#Uninstallation)
|
||||
- [Important Links](#Important-Links)
|
||||
- [For Devs and Contributors](#For-Devs-and-Contributors)
|
||||
##
|
||||
|
||||
|
||||
|
||||
## Termux App and Plugins
|
||||
|
||||
The core [Termux](https://github.com/termux/termux-app) app comes with the following optional plugin apps.
|
||||
|
||||
- [Termux:API](https://github.com/termux/termux-api)
|
||||
- [Termux:Boot](https://github.com/termux/termux-boot)
|
||||
- [Termux:Float](https://github.com/termux/termux-float)
|
||||
- [Termux:Styling](https://github.com/termux/termux-styling)
|
||||
- [Termux:Tasker](https://github.com/termux/termux-tasker)
|
||||
- [Termux:Widget](https://github.com/termux/termux-widget)
|
||||
##
|
||||
|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### F-Droid
|
||||
|
||||
Termux application can be obtained from F-Droid [here](https://f-droid.org/en/packages/com.termux/). It usually takes a few days (or even a week or more) for updates to be available on F-Droid once an update has been released on Github. F-Droid releases are built and published by F-Droid once they detect a new Github release. The Termux maintainers **do not** have any control over building and publishing of Termux app on F-Droid. Moreover, the Termux maintainers also do not have access to the APK signing keys of F-Droid releases, so we cannot release an APK ourselves on Github that would be compatible with F-Droid releases.
|
||||
|
||||
### Debug Builds
|
||||
|
||||
For users who don't want to wait for F-Droid releases and want to try out the latest features immediately or want to test their pull requests can get the APKs from [Github Actions](https://github.com/termux/termux-app/actions) page from the workflow runs labeled `Build`. The APK will be listed under `Artifacts` section. These are published for each commit done to the repository. These APKs are [debuggable](https://developer.android.com/studio/debug) and are also not compatible with other sources.
|
||||
|
||||
### Google Playstore **(Deprecated)**
|
||||
|
||||
**Termux and its plugins are no longer updated on [Google playstore](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10).** The last version released for Android `>= 7` was `v0.101`. There are currently no immediate plans to resume updates on Google playstore. **It is highly recommended to not install Termux from playstore for now.** Any current users **should switch** to a different source like F-Droid.
|
||||
|
||||
If for some reason you don't want to switch, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
|
||||
##
|
||||
|
||||
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#Installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation.
|
||||
|
||||
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins).
|
||||
|
||||
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if its available on your device and search `termux` in the applications list.
|
||||
|
||||
Even if you think you have not installed any of the plugins, its strongly suggesting to go through the application list in Android settings and double check.
|
||||
##
|
||||
|
||||
|
||||
|
||||
## Important Links
|
||||
|
||||
### Community
|
||||
All community links are available [here](https://wiki.termux.com/wiki/Community).
|
||||
|
||||
The main ones are the following.
|
||||
|
||||
- [Termux Reddit community](https://reddit.com/r/termux)
|
||||
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
|
||||
- [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im)
|
||||
- [Termux Twitter](http://twitter.com/termux/)
|
||||
- [Termux Reports Email](mailto:termuxreports@groups.io)
|
||||
|
||||
### Wikis
|
||||
|
||||
- [Termux Wiki](https://wiki.termux.com/wiki/)
|
||||
- [Termux App Wiki](https://github.com/termux/termux-app/wiki)
|
||||
- [Termux Packages Wiki](https://github.com/termux/termux-packages/wiki)
|
||||
|
||||
### Miscellaneous
|
||||
- [FAQ](https://wiki.termux.com/wiki/FAQ)
|
||||
- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout)
|
||||
- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux)
|
||||
- [Package Management](https://wiki.termux.com/wiki/Package_Management)
|
||||
- [Remote_Access](https://wiki.termux.com/wiki/Remote_Access)
|
||||
- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux)
|
||||
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
|
||||
- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard)
|
||||
- [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage)
|
||||
- [Android APIs](https://wiki.termux.com/wiki/Termux:API)
|
||||
- [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348)
|
||||
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
|
||||
|
||||
### Terminal resources
|
||||
|
||||
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||
- [vt100.net](http://vt100.net/)
|
||||
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
||||
|
||||
### Terminal emulators
|
||||
|
||||
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
|
||||
|
||||
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
|
||||
|
||||
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
|
||||
|
||||
- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
|
||||
|
||||
- xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
||||
|
||||
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
||||
|
||||
- Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
||||
##
|
||||
|
||||
|
||||
|
||||
## For Devs and Contributors
|
||||
|
||||
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of Termux app and its plugins. It was created to allow for removal of all hardcoded paths in Termux app. The termux plugins will hopefully use this in future as well. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted.
|
||||
|
||||
The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info.
|
||||
|
||||
159
app/build.gradle
@@ -1,25 +1,61 @@
|
||||
apply plugin: 'com.android.application'
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion "24.0.1"
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:24.1.1'
|
||||
compile "com.android.support:support-v4:24.1.1"
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
implementation "androidx.core:core:1.6.0-rc01"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||
|
||||
implementation project(":terminal-view")
|
||||
implementation project(":termux-shared")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 24
|
||||
versionCode 36
|
||||
versionName "0.35"
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
versionCode 116
|
||||
versionName "0.116"
|
||||
|
||||
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
|
||||
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
||||
manifestPlaceholders.TERMUX_API_APP_NAME = "Termux:API"
|
||||
manifestPlaceholders.TERMUX_BOOT_APP_NAME = "Termux:Boot"
|
||||
manifestPlaceholders.TERMUX_FLOAT_APP_NAME = "Termux:Float"
|
||||
manifestPlaceholders.TERMUX_STYLING_APP_NAME = "Termux:Styling"
|
||||
manifestPlaceholders.TERMUX_TASKER_APP_NAME = "Termux:Tasker"
|
||||
manifestPlaceholders.TERMUX_WIDGET_APP_NAME = "Termux:Widget"
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
|
||||
}
|
||||
}
|
||||
|
||||
ndk {
|
||||
moduleName "libtermux"
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
||||
cFlags "-std=c11 -Wall -Wextra -Os -fno-stack-protector -nostdlib -Wl,--gc-sections"
|
||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('dev_keystore.jks')
|
||||
keyAlias 'alias'
|
||||
storePassword 'xrj45yWGLbsO7W0v'
|
||||
keyPassword 'xrj45yWGLbsO7W0v'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +65,106 @@ android {
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "src/main/cpp/Android.mk"
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'ProtectedPermissions'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
}
|
||||
|
||||
task versionName {
|
||||
doLast {
|
||||
print android.defaultConfig.versionName
|
||||
}
|
||||
}
|
||||
|
||||
def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
||||
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||
|
||||
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
|
||||
def file = new File(projectDir, localUrl)
|
||||
if (file.exists()) {
|
||||
def buffer = new byte[8192]
|
||||
def input = new FileInputStream(file)
|
||||
while (true) {
|
||||
def readBytes = input.read(buffer)
|
||||
if (readBytes < 0) break
|
||||
digest.update(buffer, 0, readBytes)
|
||||
}
|
||||
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
||||
if (checksum == expectedChecksum) {
|
||||
return
|
||||
} else {
|
||||
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
def remoteUrl = "https://github.com/termux/termux-packages/releases/download/bootstrap-" + version + "/bootstrap-" + arch + ".zip"
|
||||
logger.quiet("Downloading " + remoteUrl + " ...")
|
||||
|
||||
file.parentFile.mkdirs()
|
||||
def out = new BufferedOutputStream(new FileOutputStream(file))
|
||||
|
||||
def connection = new URL(remoteUrl).openConnection()
|
||||
connection.setInstanceFollowRedirects(true)
|
||||
def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
|
||||
out << digestStream
|
||||
out.close()
|
||||
|
||||
def checksum = new BigInteger(1, digest.digest()).toString(16)
|
||||
if (checksum != expectedChecksum) {
|
||||
file.delete()
|
||||
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
|
||||
}
|
||||
}
|
||||
|
||||
clean {
|
||||
doLast {
|
||||
def tree = fileTree(new File(projectDir, 'src/main/cpp'))
|
||||
tree.include 'bootstrap-*.zip'
|
||||
tree.each { it.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
task downloadBootstraps() {
|
||||
doLast {
|
||||
def version = "2021.06.30-r1"
|
||||
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version)
|
||||
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version)
|
||||
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version)
|
||||
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", version)
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/dev_keystore.jks
Normal file
11
app/proguard-rules.pro
vendored
@@ -7,11 +7,6 @@
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
-dontobfuscate
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.termux;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||
public ApplicationTest() {
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,126 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.termux"
|
||||
android:installLocation="internalOnly"
|
||||
android:sharedUserId="com.termux"
|
||||
android:sharedUserLabel="@string/shared_user_label" >
|
||||
android:sharedUserId="${TERMUX_PACKAGE_NAME}"
|
||||
android:sharedUserLabel="@string/shared_user_label">
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<permission
|
||||
android:name="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"
|
||||
android:description="@string/permission_run_command_description"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/permission_run_command_label"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.READ_LOGS" />
|
||||
<uses-permission android:name="android.permission.DUMP" />
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backupscheme"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:name=".app.TermuxApplication"
|
||||
android:allowBackup="false"
|
||||
android:banner="@drawable/banner"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/Theme.Termux"
|
||||
android:supportsRtl="false" >
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/Theme.Termux">
|
||||
|
||||
<!--
|
||||
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
||||
mark the app with "This app is optimized to run in full screen."
|
||||
-->
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="10.0" />
|
||||
|
||||
<activity
|
||||
android:name="com.termux.app.TermuxActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:name=".app.TermuxActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||
android:label="@string/application_name"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
||||
android:resizeableActivity="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.termux.app.TermuxHelpActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:label="@string/application_name" />
|
||||
<activity-alias
|
||||
android:name=".HomeActivity"
|
||||
android:targetActivity=".app.TermuxActivity">
|
||||
|
||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.IOT_LAUNCHER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
|
||||
android:name=".app.activities.HelpActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/application_name"
|
||||
android:taskAffinity="com.termux.filereceiver"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".app.activities.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_termux_settings"
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".shared.activities.ReportActivity"
|
||||
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".filepicker.TermuxFileReceiverActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:noHistory="true">
|
||||
android:label="@string/application_name"
|
||||
android:noHistory="true"
|
||||
android:resizeableActivity="true"
|
||||
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
|
||||
|
||||
<!-- Accept multiple file types when sending. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
@@ -61,23 +129,25 @@
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="application/json" />
|
||||
<data android:mimeType="application/*xml*" />
|
||||
<data android:mimeType="application/*latex*" />
|
||||
<data android:mimeType="application/javascript" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name=".filepicker.TermuxDocumentsProvider"
|
||||
android:authorities="com.termux.documents"
|
||||
android:grantUriPermissions="true"
|
||||
android:authorities="${TERMUX_PACKAGE_NAME}.documents"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
@@ -85,9 +155,32 @@
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name="com.termux.app.TermuxService"
|
||||
android:name=".app.TermuxService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".app.RunCommandService"
|
||||
android:exported="true"
|
||||
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND">
|
||||
<intent-filter>
|
||||
<action android:name="${TERMUX_PACKAGE_NAME}.RUN_COMMAND" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".app.TermuxOpenReceiver" />
|
||||
|
||||
<provider
|
||||
android:name=".app.TermuxOpenReceiver$ContentProvider"
|
||||
android:authorities="${TERMUX_PACKAGE_NAME}.files"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:readPermission="android.permission.permRead" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
5
app/src/main/cpp/Android.mk
Normal file
@@ -0,0 +1,5 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libtermux-bootstrap
|
||||
LOCAL_SRC_FILES := termux-bootstrap-zip.S termux-bootstrap.c
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
18
app/src/main/cpp/termux-bootstrap-zip.S
Normal file
@@ -0,0 +1,18 @@
|
||||
.global blob
|
||||
.global blob_size
|
||||
.section .rodata
|
||||
blob:
|
||||
#if defined __i686__
|
||||
.incbin "bootstrap-i686.zip"
|
||||
#elif defined __x86_64__
|
||||
.incbin "bootstrap-x86_64.zip"
|
||||
#elif defined __aarch64__
|
||||
.incbin "bootstrap-aarch64.zip"
|
||||
#elif defined __arm__
|
||||
.incbin "bootstrap-arm.zip"
|
||||
#else
|
||||
# error Unsupported arch
|
||||
#endif
|
||||
1:
|
||||
blob_size:
|
||||
.int 1b - blob
|
||||
11
app/src/main/cpp/termux-bootstrap.c
Normal file
@@ -0,0 +1,11 @@
|
||||
#include <jni.h>
|
||||
|
||||
extern jbyte blob[];
|
||||
extern int blob_size;
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_com_termux_app_TermuxInstaller_getZip(JNIEnv *env, __attribute__((__unused__)) jobject This)
|
||||
{
|
||||
jbyteArray ret = (*env)->NewByteArray(env, blob_size);
|
||||
(*env)->SetByteArrayRegion(env, ret, 0, blob_size, blob);
|
||||
return ret;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* A background job launched by Termux.
|
||||
*/
|
||||
public final class BackgroundJob {
|
||||
|
||||
private static final String LOG_TAG = "termux-background";
|
||||
|
||||
final Process mProcess;
|
||||
|
||||
public BackgroundJob(File cwd, File fileToExecute, String[] args) throws IOException {
|
||||
String[] env = buildEnvironment(false, cwd.getAbsolutePath());
|
||||
|
||||
String[] progArray = new String[args.length + 1];
|
||||
|
||||
mProcess = Runtime.getRuntime().exec(progArray, env, cwd);
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
int exitCode = mProcess.waitFor();
|
||||
if (exitCode == 0) {
|
||||
Log.i(LOG_TAG, "exited normally");
|
||||
return;
|
||||
} else {
|
||||
Log.i(LOG_TAG, "exited with exit code: " + exitCode);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
InputStream stdout = mProcess.getInputStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
try {
|
||||
// FIXME: Long lines.
|
||||
while ((line = reader.readLine()) != null) {
|
||||
Log.i(LOG_TAG, line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
InputStream stderr = mProcess.getErrorStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
try {
|
||||
// FIXME: Long lines.
|
||||
while ((line = reader.readLine()) != null) {
|
||||
Log.e(LOG_TAG, line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public String[] buildEnvironment(boolean failSafe, String cwd) {
|
||||
new File(TermuxService.HOME_PATH).mkdirs();
|
||||
|
||||
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
||||
|
||||
final String termEnv = "TERM=xterm-256color";
|
||||
final String homeEnv = "HOME=" + TermuxService.HOME_PATH;
|
||||
final String prefixEnv = "PREFIX=" + TermuxService.PREFIX_PATH;
|
||||
final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
|
||||
final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
|
||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||
final String externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE");
|
||||
String[] env;
|
||||
if (failSafe) {
|
||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||
final String pathEnv = "PATH=" + System.getenv("PATH");
|
||||
return new String[]{termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv, pathEnv, externalStorageEnv};
|
||||
} else {
|
||||
final String ps1Env = "PS1=$ ";
|
||||
final String ldEnv = "LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib";
|
||||
final String langEnv = "LANG=en_US.UTF-8";
|
||||
final String pathEnv = "PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets";
|
||||
final String pwdEnv = "PWD=" + cwd;
|
||||
|
||||
return new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
/**
|
||||
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||
* keyboard.
|
||||
*/
|
||||
public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
|
||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
reload();
|
||||
}
|
||||
|
||||
static void sendKey(View view, String keyName) {
|
||||
int keyCode = 0;
|
||||
String chars = null;
|
||||
switch (keyName) {
|
||||
case "ESC":
|
||||
keyCode = KeyEvent.KEYCODE_ESCAPE;
|
||||
break;
|
||||
case "TAB":
|
||||
keyCode = KeyEvent.KEYCODE_TAB;
|
||||
break;
|
||||
case "▲":
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||
break;
|
||||
case "◀":
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case "▶":
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
break;
|
||||
case "▼":
|
||||
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
break;
|
||||
case "―":
|
||||
chars = "-";
|
||||
break;
|
||||
default:
|
||||
chars = keyName;
|
||||
}
|
||||
|
||||
if (keyCode > 0) {
|
||||
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||
} else {
|
||||
TerminalView terminalView = (TerminalView) view.findViewById(R.id.terminal_view);
|
||||
TerminalSession session = terminalView.getCurrentSession();
|
||||
if (session != null) session.write(chars);
|
||||
}
|
||||
}
|
||||
|
||||
private ToggleButton controlButton;
|
||||
private ToggleButton altButton;
|
||||
private ToggleButton fnButton;
|
||||
|
||||
public boolean readControlButton() {
|
||||
if (controlButton.isPressed()) return true;
|
||||
boolean result = controlButton.isChecked();
|
||||
if (result) {
|
||||
controlButton.setChecked(false);
|
||||
controlButton.setTextColor(TEXT_COLOR);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean readAltButton() {
|
||||
if (altButton.isPressed()) return true;
|
||||
boolean result = altButton.isChecked();
|
||||
if (result) {
|
||||
altButton.setChecked(false);
|
||||
altButton.setTextColor(TEXT_COLOR);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean readFnButton() {
|
||||
if (fnButton.isPressed()) return true;
|
||||
boolean result = fnButton.isChecked();
|
||||
if (result) {
|
||||
fnButton.setChecked(false);
|
||||
fnButton.setTextColor(TEXT_COLOR);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void reload() {
|
||||
altButton = controlButton = null;
|
||||
removeAllViews();
|
||||
|
||||
String[][] buttons = {
|
||||
{"ESC", "CTRL", "ALT", "TAB", "―", "/", "|"}
|
||||
};
|
||||
|
||||
final int rows = buttons.length;
|
||||
final int cols = buttons[0].length;
|
||||
|
||||
setRowCount(rows);
|
||||
setColumnCount(cols);
|
||||
|
||||
for (int row = 0; row < rows; row++) {
|
||||
for (int col = 0; col < cols; col++) {
|
||||
final String buttonText = buttons[row][col];
|
||||
|
||||
Button button;
|
||||
switch (buttonText) {
|
||||
case "CTRL":
|
||||
button = controlButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setClickable(true);
|
||||
break;
|
||||
case "ALT":
|
||||
button = altButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setClickable(true);
|
||||
break;
|
||||
case "FN":
|
||||
button = fnButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setClickable(true);
|
||||
break;
|
||||
default:
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
break;
|
||||
}
|
||||
|
||||
button.setText(buttonText);
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
|
||||
final Button finalButton = button;
|
||||
button.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
View root = getRootView();
|
||||
switch (buttonText) {
|
||||
case "CTRL":
|
||||
case "ALT":
|
||||
case "FN":
|
||||
ToggleButton self = (ToggleButton) finalButton;
|
||||
self.setChecked(self.isChecked());
|
||||
self.setTextColor(self.isChecked() ? 0xFF80DEEA : TEXT_COLOR);
|
||||
break;
|
||||
default:
|
||||
sendKey(root, buttonText);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
|
||||
param.height = param.width = 0;
|
||||
param.rightMargin = param.topMargin = 0;
|
||||
param.setGravity(Gravity.LEFT);
|
||||
float weight = "▲▼◀▶".contains(buttonText) ? 0.7f : 1.f;
|
||||
param.columnSpec = GridLayout.spec(col, weight);
|
||||
param.rowSpec = GridLayout.spec(row, 1.f);
|
||||
button.setLayoutParams(param);
|
||||
|
||||
addView(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
import com.termux.R;
|
||||
|
||||
/**
|
||||
* Utility to manage full screen immersive mode.
|
||||
* <p/>
|
||||
* See https://code.google.com/p/android/issues/detail?id=5497
|
||||
*/
|
||||
final class FullScreenHelper {
|
||||
|
||||
private boolean mEnabled = false;
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
public FullScreenHelper(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
public void setImmersive(boolean enabled) {
|
||||
if (enabled == mEnabled) return;
|
||||
mEnabled = enabled;
|
||||
|
||||
View decorView = mActivity.getWindow().getDecorView();
|
||||
|
||||
if (enabled) {
|
||||
decorView.setOnSystemUiVisibilityChangeListener
|
||||
(new View.OnSystemUiVisibilityChangeListener() {
|
||||
@Override
|
||||
public void onSystemUiVisibilityChange(int visibility) {
|
||||
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
||||
if (mActivity.mSettings.isShowExtraKeys()) {
|
||||
mActivity.findViewById(R.id.viewpager).setVisibility(View.VISIBLE);
|
||||
}
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
mActivity.findViewById(R.id.viewpager).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
});
|
||||
setImmersiveMode();
|
||||
} else {
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
|
||||
decorView.setOnSystemUiVisibilityChangeListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isColorLight(int color) {
|
||||
double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
|
||||
return darkness < 0.5;
|
||||
}
|
||||
|
||||
void setImmersiveMode() {
|
||||
int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
int color = ((ColorDrawable) mActivity.getWindow().getDecorView().getBackground()).getColor();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isColorLight(color))
|
||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
||||
mActivity.getWindow().getDecorView().setSystemUiVisibility(flags);
|
||||
}
|
||||
|
||||
}
|
||||
259
app/src/main/java/com/termux/app/RunCommandService.java
Normal file
@@ -0,0 +1,259 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
|
||||
/**
|
||||
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||
* plugins that contains info on command execution and forwards the extras to {@link TermuxService}
|
||||
* for the actual execution.
|
||||
*
|
||||
* Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent for more info.
|
||||
*/
|
||||
public class RunCommandService extends Service {
|
||||
|
||||
private static final String LOG_TAG = "RunCommandService";
|
||||
|
||||
class LocalBinder extends Binder {
|
||||
public final RunCommandService service = RunCommandService.this;
|
||||
}
|
||||
|
||||
private final IBinder mBinder = new RunCommandService.LocalBinder();
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||
runStartForeground();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Logger.logDebug(LOG_TAG, "onStartCommand");
|
||||
|
||||
if (intent == null) return Service.START_NOT_STICKY;
|
||||
|
||||
// Run again in case service is already started and onCreate() is not called
|
||||
runStartForeground();
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||
|
||||
Error error;
|
||||
String errmsg;
|
||||
|
||||
// If invalid action passed, then just return
|
||||
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
|
||||
|
||||
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
|
||||
|
||||
/*
|
||||
* If intent was sent with `am` command, then normal comma characters may have been replaced
|
||||
* with alternate characters if a normal comma existed in an argument itself to prevent it
|
||||
* splitting into multiple arguments by `am` command.
|
||||
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
|
||||
* options can be used without passing the below extras, but native supports is helpful if
|
||||
* they are not being used.
|
||||
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
|
||||
*/
|
||||
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
|
||||
if (replaceCommaAlternativeCharsInArguments) {
|
||||
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
|
||||
if (commaAlternativeCharsInArguments == null)
|
||||
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
|
||||
// Replace any commaAlternativeCharsInArguments characters with normal commas
|
||||
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
|
||||
}
|
||||
|
||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
||||
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
||||
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
|
||||
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||
executionCommand.isPluginExecutionCommand = true;
|
||||
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
|
||||
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
|
||||
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||
}
|
||||
|
||||
// If "allow-external-apps" property to not set to "true", then just return
|
||||
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||
// user knows someone tried to run a command in termux context, since it may be malicious
|
||||
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
|
||||
// also sent, then its creator is also logged and shown.
|
||||
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||
if (errmsg != null) {
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
|
||||
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Get canonical path of executable
|
||||
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||
|
||||
// If executable is not a regular file, or is not readable or executable, then just return
|
||||
// Setting of missing read and execute permissions is not done
|
||||
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
|
||||
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||
false);
|
||||
if (error != null) {
|
||||
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
|
||||
executionCommand.setStateFailed(error);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// If workingDirectory is not null or empty
|
||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||
// Get canonical path of workingDirectory
|
||||
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||
|
||||
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||
// under allowed termux working directory paths.
|
||||
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||
// for working directories.
|
||||
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
|
||||
true, true, true,
|
||||
false, true);
|
||||
if (error != null) {
|
||||
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
|
||||
executionCommand.setStateFailed(error);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(TermuxFileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||
|
||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||
|
||||
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
||||
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
|
||||
execIntent.setClass(this, TermuxService.class);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
|
||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
|
||||
}
|
||||
|
||||
// Start TERMUX_SERVICE and pass it execution intent
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.startForegroundService(execIntent);
|
||||
} else {
|
||||
this.startService(execIntent);
|
||||
}
|
||||
|
||||
runStopForeground();
|
||||
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private void runStartForeground() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setupNotificationChannel();
|
||||
startForeground(TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID, buildNotification());
|
||||
}
|
||||
}
|
||||
|
||||
private void runStopForeground() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
stopForeground(true);
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
// Build the notification
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
|
||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
|
||||
null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||
if (builder == null) return null;
|
||||
|
||||
// No need to show a timestamp:
|
||||
builder.setShowWhen(false);
|
||||
|
||||
// Set notification icon
|
||||
builder.setSmallIcon(R.drawable.ic_service_notification);
|
||||
|
||||
// Set background color for small notification icon
|
||||
builder.setColor(0xFF607D8B);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void setupNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
|
||||
NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
|
||||
}
|
||||
|
||||
}
|
||||
29
app/src/main/java/com/termux/app/TermuxApplication.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.termux.shared.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
|
||||
public class TermuxApplication extends Application {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Set crash handler for the app
|
||||
TermuxCrashUtils.setCrashHandler(this);
|
||||
|
||||
// Set log level for the app
|
||||
setLogLevel();
|
||||
}
|
||||
|
||||
private void setLogLevel() {
|
||||
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(getApplicationContext());
|
||||
if (preferences == null) return;
|
||||
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||
Logger.logDebug("Starting Application");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,26 @@ import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.DialogInterface.OnDismissListener;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.UserManager;
|
||||
import android.system.Os;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.app.utils.CrashUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
@@ -34,16 +32,16 @@ import java.util.zip.ZipInputStream;
|
||||
* Install the Termux bootstrap packages if necessary by following the below steps:
|
||||
* <p/>
|
||||
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
|
||||
* broken $PREFIX folder below.
|
||||
* broken $PREFIX directory below.
|
||||
* <p/>
|
||||
* (2) A progress dialog is shown with "Installing..." message and a spinner.
|
||||
* <p/>
|
||||
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
|
||||
* (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below.
|
||||
* <p/>
|
||||
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
|
||||
* (4) The zip file is loaded from a shared library.
|
||||
* <p/>
|
||||
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
|
||||
* continously encountering zip file entries:
|
||||
* continuously encountering zip file entries:
|
||||
* <p/>
|
||||
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
|
||||
* <p/>
|
||||
@@ -51,27 +49,38 @@ import java.util.zip.ZipInputStream;
|
||||
*/
|
||||
final class TermuxInstaller {
|
||||
|
||||
/** Performs setup if necessary. */
|
||||
static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
|
||||
private static final String LOG_TAG = "TermuxInstaller";
|
||||
|
||||
/** Performs bootstrap setup if necessary. */
|
||||
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
|
||||
// Termux can only be run as the primary user (device owner) since only that
|
||||
// account has the expected file system paths. Verify that:
|
||||
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
|
||||
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
|
||||
if (!isPrimaryUser) {
|
||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
|
||||
.setOnDismissListener(new OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
System.exit(0);
|
||||
}
|
||||
}).setPositiveButton(android.R.string.ok, null).show();
|
||||
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
||||
activity.getString(R.string.bootstrap_error_title),
|
||||
bootstrapErrorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
|
||||
if (PREFIX_FILE.isDirectory()) {
|
||||
whenDone.run();
|
||||
return;
|
||||
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
||||
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
|
||||
|
||||
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
||||
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
||||
// If prefix directory is empty or only contains the tmp directory
|
||||
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
|
||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||
} else {
|
||||
whenDone.run();
|
||||
return;
|
||||
}
|
||||
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
||||
}
|
||||
|
||||
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
||||
@@ -79,18 +88,34 @@ final class TermuxInstaller {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
|
||||
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
||||
|
||||
Error error;
|
||||
|
||||
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
||||
|
||||
if (STAGING_PREFIX_FILE.exists()) {
|
||||
deleteFolder(STAGING_PREFIX_FILE);
|
||||
// Delete prefix staging directory or any file at its destination
|
||||
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete prefix directory or any file at its destination
|
||||
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
||||
|
||||
final byte[] buffer = new byte[8096];
|
||||
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
||||
|
||||
final URL zipUrl = determineZipUrl();
|
||||
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
|
||||
final byte[] zipBytes = loadZipBytes();
|
||||
try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||
ZipEntry zipEntry;
|
||||
while ((zipEntry = zipInput.getNextEntry()) != null) {
|
||||
if (zipEntry.getName().equals("SYMLINKS.txt")) {
|
||||
@@ -103,14 +128,25 @@ final class TermuxInstaller {
|
||||
String oldPath = parts[0];
|
||||
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
||||
symlinks.add(Pair.create(oldPath, newPath));
|
||||
|
||||
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String zipEntryName = zipEntry.getName();
|
||||
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
||||
if (zipEntry.isDirectory()) {
|
||||
if (!targetFile.mkdirs())
|
||||
throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
|
||||
} else {
|
||||
boolean isDirectory = zipEntry.isDirectory();
|
||||
|
||||
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDirectory) {
|
||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||
int readBytes;
|
||||
while ((readBytes = zipInput.read(buffer)) != -1)
|
||||
@@ -131,50 +167,24 @@ final class TermuxInstaller {
|
||||
Os.symlink(symlink.first, symlink.second);
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
|
||||
|
||||
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
|
||||
throw new RuntimeException("Unable to rename staging folder");
|
||||
throw new RuntimeException("Moving prefix staging to prefix directory failed");
|
||||
}
|
||||
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
whenDone.run();
|
||||
}
|
||||
});
|
||||
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||
activity.runOnUiThread(whenDone);
|
||||
|
||||
} catch (final Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||
.setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
activity.finish();
|
||||
}
|
||||
}).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
TermuxInstaller.setupIfNeeded(activity, whenDone);
|
||||
}
|
||||
}).show();
|
||||
} catch (WindowManager.BadTokenException e) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
}
|
||||
});
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
||||
|
||||
} finally {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
progress.dismiss();
|
||||
} catch (RuntimeException e) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
activity.runOnUiThread(() -> {
|
||||
try {
|
||||
progress.dismiss();
|
||||
} catch (RuntimeException e) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -182,58 +192,50 @@ final class TermuxInstaller {
|
||||
}.start();
|
||||
}
|
||||
|
||||
/** Get bootstrap zip url for this systems cpu architecture. */
|
||||
static URL determineZipUrl() throws MalformedURLException {
|
||||
String termuxArch = null;
|
||||
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
|
||||
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
|
||||
// Instead we search through the supported abi:s on the device, see:
|
||||
// http://developer.android.com/ndk/guides/abis.html
|
||||
// Note that we search for abi:s in preferred order, and want to avoid installing arm on
|
||||
// an x86 system where arm emulation is available.
|
||||
final String[] androidArchNames = {"arm64-v8a", "x86_64", "x86", "armeabi-v7a"};
|
||||
final String[] termuxArchNames = {"aarch64", "x86_64", "i686", "arm"};
|
||||
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
|
||||
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
||||
|
||||
final List<String> supportedArches = Arrays.asList(Build.SUPPORTED_ABIS);
|
||||
for (int i = 0; i < termuxArchNames.length; i++) {
|
||||
if (supportedArches.contains(androidArchNames[i])) {
|
||||
termuxArch = termuxArchNames[i];
|
||||
break;
|
||||
// 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);
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
try {
|
||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
activity.finish();
|
||||
})
|
||||
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||
}).show();
|
||||
} catch (WindowManager.BadTokenException e1) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
}
|
||||
|
||||
return new URL("https://termux.net/bootstrap/bootstrap-" + termuxArch + ".zip");
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a folder and all its content or throw. */
|
||||
static void deleteFolder(File fileOrDirectory) {
|
||||
File[] children = fileOrDirectory.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteFolder(child);
|
||||
}
|
||||
}
|
||||
if (!fileOrDirectory.delete()) {
|
||||
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
public static void setupStorageSymlinks(final Context context) {
|
||||
static void setupStorageSymlinks(final Context context) {
|
||||
final String LOG_TAG = "termux-storage";
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
||||
|
||||
new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
||||
Error error;
|
||||
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||
|
||||
if (storageDir.exists() && !storageDir.delete()) {
|
||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage");
|
||||
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
|
||||
if (error != null) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!storageDir.mkdirs()) {
|
||||
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
|
||||
return;
|
||||
}
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
|
||||
|
||||
File sharedDir = Environment.getExternalStorageDirectory();
|
||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
||||
@@ -254,15 +256,36 @@ final class TermuxInstaller {
|
||||
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
||||
|
||||
final File[] dirs = context.getExternalFilesDirs(null);
|
||||
if (dirs != null && dirs.length >= 2) {
|
||||
final File externalDir = dirs[1];
|
||||
Os.symlink(externalDir.getAbsolutePath(), new File(storageDir, "external").getAbsolutePath());
|
||||
if (dirs != null && dirs.length > 1) {
|
||||
for (int i = 1; i < dirs.length; i++) {
|
||||
File dir = dirs[i];
|
||||
if (dir == null) continue;
|
||||
String symlinkName = "external-" + i;
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
|
||||
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Error setting up link", e);
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private static Error ensureDirectoryExists(File directory) {
|
||||
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
|
||||
}
|
||||
|
||||
public static byte[] loadZipBytes() {
|
||||
// Only load the shared library when necessary to save memory usage.
|
||||
System.loadLibrary("termux-bootstrap");
|
||||
return getZip();
|
||||
}
|
||||
|
||||
public static native byte[] getZip();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.view.TerminalKeyListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class TermuxKeyListener implements TerminalKeyListener {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
||||
public TermuxKeyListener(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float onScale(float scale) {
|
||||
if (scale < 0.9f || scale > 1.1f) {
|
||||
boolean increase = scale > 1.f;
|
||||
mActivity.changeFontSize(increase);
|
||||
return 1.0f;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
mgr.showSoftInput(mActivity.mTerminalView, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBackButtonBeMappedToEscape() {
|
||||
return mActivity.mSettings.mBackIsEscape;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyModeChanged(boolean copyMode) {
|
||||
// Disable drawer while copying.
|
||||
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
||||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||
|
||||
TermuxService service = mActivity.mTermService;
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
// Return pressed with finished session - remove it.
|
||||
currentSession.finishIfRunning();
|
||||
|
||||
int index = service.removeTermSession(currentSession);
|
||||
mActivity.mListViewAdapter.notifyDataSetChanged();
|
||||
if (mActivity.mTermService.getSessions().isEmpty()) {
|
||||
// There are no sessions to show, so finish the activity.
|
||||
mActivity.finish();
|
||||
} else {
|
||||
if (index >= service.getSessions().size()) {
|
||||
index = service.getSessions().size() - 1;
|
||||
}
|
||||
mActivity.switchToSession(service.getSessions().get(index));
|
||||
}
|
||||
return true;
|
||||
} else if (e.isCtrlPressed() && e.isShiftPressed()) {
|
||||
// Get the unmodified code point:
|
||||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||
mActivity.switchToSession(true);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||
mActivity.switchToSession(false);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
} else if (unicodeChar == 'f'/* full screen */) {
|
||||
mActivity.toggleImmersive();
|
||||
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mActivity.mTerminalView.showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
mActivity.renameSession(currentSession);
|
||||
} else if (unicodeChar == 'c'/* create */) {
|
||||
mActivity.addNewSession(false, null);
|
||||
} else if (unicodeChar == 'u' /* urls */) {
|
||||
mActivity.showUrlSelection();
|
||||
} else if (unicodeChar == 'v') {
|
||||
mActivity.doPaste();
|
||||
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
|
||||
// We also check for the shifted char here since shift may be required to produce '+',
|
||||
// see https://github.com/termux/termux-api/issues/2
|
||||
mActivity.changeFontSize(true);
|
||||
} else if (unicodeChar == '-') {
|
||||
mActivity.changeFontSize(false);
|
||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||
int num = unicodeChar - '1';
|
||||
if (service.getSessions().size() > num)
|
||||
mActivity.switchToSession(service.getSessions().get(num));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||
return handleVirtualKeys(keyCode, e, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readControlKey() {
|
||||
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readControlButton()) || mVirtualControlKeyDown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readAltKey() {
|
||||
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readAltButton());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||
if (mVirtualFnKeyDown) {
|
||||
int resultingKeyCode = -1;
|
||||
int resultingCodePoint = -1;
|
||||
boolean altDown = false;
|
||||
int lowerCase = Character.toLowerCase(codePoint);
|
||||
switch (lowerCase) {
|
||||
// Arrow keys.
|
||||
case 'w':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||
break;
|
||||
case 'a':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case 's':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
break;
|
||||
case 'd':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
break;
|
||||
|
||||
// Page up and down.
|
||||
case 'p':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
|
||||
break;
|
||||
case 'n':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
||||
break;
|
||||
|
||||
// Some special keys:
|
||||
case 't':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_TAB;
|
||||
break;
|
||||
case 'i':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
||||
break;
|
||||
case 'h':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_MOVE_HOME;
|
||||
break;
|
||||
|
||||
// Special characters to input.
|
||||
case 'u':
|
||||
resultingCodePoint = '_';
|
||||
break;
|
||||
case 'l':
|
||||
resultingCodePoint = '|';
|
||||
break;
|
||||
|
||||
// Function keys.
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
|
||||
break;
|
||||
case '0':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_F10;
|
||||
break;
|
||||
|
||||
// Other special keys.
|
||||
case 'e':
|
||||
resultingCodePoint = /*Escape*/ 27;
|
||||
break;
|
||||
case '.':
|
||||
resultingCodePoint = /*^.*/ 28;
|
||||
break;
|
||||
|
||||
case 'b': // alt+b, jumping backward in readline.
|
||||
case 'f': // alf+f, jumping forward in readline.
|
||||
case 'x': // alt+x, common in emacs.
|
||||
resultingCodePoint = lowerCase;
|
||||
altDown = true;
|
||||
break;
|
||||
|
||||
// Volume control.
|
||||
case 'v':
|
||||
resultingCodePoint = -1;
|
||||
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
|
||||
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
||||
break;
|
||||
|
||||
// Writing mode:
|
||||
case 'q':
|
||||
mActivity.toggleShowExtraKeys();
|
||||
break;
|
||||
}
|
||||
|
||||
if (resultingKeyCode != -1) {
|
||||
TerminalEmulator term = session.getEmulator();
|
||||
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
|
||||
} else if (resultingCodePoint != -1) {
|
||||
session.writeCodePoint(altDown, resultingCodePoint);
|
||||
}
|
||||
return true;
|
||||
} else if (ctrlDown) {
|
||||
List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts;
|
||||
if (!shortcuts.isEmpty()) {
|
||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||
TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i);
|
||||
if (codePoint == shortcut.codePoint) {
|
||||
switch (shortcut.shortcutAction) {
|
||||
case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION:
|
||||
mActivity.addNewSession(false, null);
|
||||
return true;
|
||||
case TermuxPreferences.SHORTCUT_ACTION_PREVIOUS_SESSION:
|
||||
mActivity.switchToSession(false);
|
||||
return true;
|
||||
case TermuxPreferences.SHORTCUT_ACTION_NEXT_SESSION:
|
||||
mActivity.switchToSession(true);
|
||||
return true;
|
||||
case TermuxPreferences.SHORTCUT_ACTION_RENAME_SESSION:
|
||||
mActivity.renameSession(mActivity.getCurrentTermSession());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||
InputDevice inputDevice = event.getDevice();
|
||||
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||
// Do not steal dedicated buttons from a full external keyboard.
|
||||
return false;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
mVirtualControlKeyDown = down;
|
||||
return true;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
mVirtualFnKeyDown = down;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
193
app/src/main/java/com/termux/app/TermuxOpenReceiver.java
Normal file
@@ -0,0 +1,193 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String LOG_TAG = "TermuxOpenReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final Uri data = intent.getData();
|
||||
if (data == null) {
|
||||
Logger.logError(LOG_TAG, "termux-open: Called without intent data");
|
||||
return;
|
||||
}
|
||||
|
||||
final String filePath = data.getPath();
|
||||
final String contentTypeExtra = intent.getStringExtra("content-type");
|
||||
final boolean useChooser = intent.getBooleanExtra("chooser", false);
|
||||
final String intentAction = intent.getAction() == null ? Intent.ACTION_VIEW : intent.getAction();
|
||||
switch (intentAction) {
|
||||
case Intent.ACTION_SEND:
|
||||
case Intent.ACTION_VIEW:
|
||||
// Ok.
|
||||
break;
|
||||
default:
|
||||
Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
||||
break;
|
||||
}
|
||||
|
||||
final boolean isExternalUrl = data.getScheme() != null && !data.getScheme().equals("file");
|
||||
if (isExternalUrl) {
|
||||
Intent urlIntent = new Intent(intentAction, data);
|
||||
if (intentAction.equals(Intent.ACTION_SEND)) {
|
||||
urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString());
|
||||
urlIntent.setData(null);
|
||||
} else if (contentTypeExtra != null) {
|
||||
urlIntent.setDataAndType(data, contentTypeExtra);
|
||||
}
|
||||
urlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
try {
|
||||
context.startActivity(urlIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final File fileToShare = new File(filePath);
|
||||
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
||||
Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(intentAction);
|
||||
sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
String contentTypeToUse;
|
||||
if (contentTypeExtra == null) {
|
||||
String fileName = fileToShare.getName();
|
||||
int lastDotIndex = fileName.lastIndexOf('.');
|
||||
String fileExtension = fileName.substring(lastDotIndex + 1);
|
||||
MimeTypeMap mimeTypes = MimeTypeMap.getSingleton();
|
||||
// Lower casing makes it work with e.g. "JPG":
|
||||
contentTypeToUse = mimeTypes.getMimeTypeFromExtension(fileExtension.toLowerCase());
|
||||
if (contentTypeToUse == null) contentTypeToUse = "application/octet-stream";
|
||||
} else {
|
||||
contentTypeToUse = contentTypeExtra;
|
||||
}
|
||||
|
||||
Uri uriToShare = Uri.parse("content://" + TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY + fileToShare.getAbsolutePath());
|
||||
|
||||
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||
sendIntent.setType(contentTypeToUse);
|
||||
} else {
|
||||
sendIntent.setDataAndType(uriToShare, contentTypeToUse);
|
||||
}
|
||||
|
||||
if (useChooser) {
|
||||
sendIntent = Intent.createChooser(sendIntent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(sendIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ContentProvider extends android.content.ContentProvider {
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
File file = new File(uri.getPath());
|
||||
|
||||
if (projection == null) {
|
||||
projection = new String[]{
|
||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||
MediaStore.MediaColumns.SIZE,
|
||||
MediaStore.MediaColumns._ID
|
||||
};
|
||||
}
|
||||
|
||||
Object[] row = new Object[projection.length];
|
||||
for (int i = 0; i < projection.length; i++) {
|
||||
String column = projection[i];
|
||||
Object value;
|
||||
switch (column) {
|
||||
case MediaStore.MediaColumns.DISPLAY_NAME:
|
||||
value = file.getName();
|
||||
break;
|
||||
case MediaStore.MediaColumns.SIZE:
|
||||
value = (int) file.length();
|
||||
break;
|
||||
case MediaStore.MediaColumns._ID:
|
||||
value = 1;
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
}
|
||||
row[i] = value;
|
||||
}
|
||||
|
||||
MatrixCursor cursor = new MatrixCursor(projection);
|
||||
cursor.addRow(row);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
File file = new File(uri.getPath());
|
||||
try {
|
||||
String path = file.getCanonicalPath();
|
||||
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||
// See https://support.google.com/faqs/answer/7496913:
|
||||
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
|
||||
throw new IllegalArgumentException("Invalid path: " + path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
final class TermuxPreferences {
|
||||
|
||||
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface AsciiBellBehaviour {
|
||||
}
|
||||
|
||||
static final int BELL_VIBRATE = 1;
|
||||
static final int BELL_BEEP = 2;
|
||||
static final int BELL_IGNORE = 3;
|
||||
|
||||
private final int MIN_FONTSIZE;
|
||||
private static final int MAX_FONTSIZE = 256;
|
||||
|
||||
private static final String FULLSCREEN_KEY = "fullscreen";
|
||||
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
|
||||
private static final String FONTSIZE_KEY = "fontsize";
|
||||
private static final String CURRENT_SESSION_KEY = "current_session";
|
||||
private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog";
|
||||
|
||||
private boolean mFullScreen;
|
||||
private int mFontSize;
|
||||
|
||||
@AsciiBellBehaviour
|
||||
int mBellBehaviour = BELL_VIBRATE;
|
||||
|
||||
boolean mBackIsEscape;
|
||||
boolean mShowExtraKeys;
|
||||
|
||||
TermuxPreferences(Context context) {
|
||||
reloadFromProperties(context);
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
|
||||
|
||||
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
|
||||
// to prevent invisible text due to zoom be mistake:
|
||||
MIN_FONTSIZE = (int) (4f * dipInPixels);
|
||||
|
||||
mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false);
|
||||
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, false);
|
||||
|
||||
// http://www.google.com/design/spec/style/typography.html#typography-line-height
|
||||
int defaultFontSize = Math.round(12 * dipInPixels);
|
||||
// Make it divisible by 2 since that is the minimal adjustment step:
|
||||
if (defaultFontSize % 2 == 1) defaultFontSize--;
|
||||
|
||||
try {
|
||||
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
|
||||
} catch (NumberFormatException | ClassCastException e) {
|
||||
mFontSize = defaultFontSize;
|
||||
}
|
||||
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
|
||||
}
|
||||
|
||||
boolean isFullScreen() {
|
||||
return mFullScreen;
|
||||
}
|
||||
|
||||
void setFullScreen(Context context, boolean newValue) {
|
||||
mFullScreen = newValue;
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
|
||||
}
|
||||
|
||||
boolean isShowExtraKeys() {
|
||||
return mShowExtraKeys;
|
||||
}
|
||||
|
||||
boolean toggleShowExtraKeys(Context context) {
|
||||
mShowExtraKeys = !mShowExtraKeys;
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_EXTRA_KEYS_KEY, mShowExtraKeys).apply();
|
||||
return mShowExtraKeys;
|
||||
}
|
||||
|
||||
int getFontSize() {
|
||||
return mFontSize;
|
||||
}
|
||||
|
||||
void changeFontSize(Context context, boolean increase) {
|
||||
mFontSize += (increase ? 1 : -1) * 2;
|
||||
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
|
||||
}
|
||||
|
||||
static void storeCurrentSession(Context context, TerminalSession session) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit();
|
||||
}
|
||||
|
||||
static TerminalSession getCurrentSession(TermuxActivity context) {
|
||||
String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, "");
|
||||
for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) {
|
||||
TerminalSession session = context.mTermService.getSessions().get(i);
|
||||
if (session.mHandle.equals(sessionHandle)) return session;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isShowWelcomeDialog(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true);
|
||||
}
|
||||
|
||||
public static void disableWelcomeDialog(Context context) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply();
|
||||
}
|
||||
|
||||
public void reloadFromProperties(Context context) {
|
||||
try {
|
||||
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
||||
if (!propsFile.exists())
|
||||
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||
|
||||
Properties props = new Properties();
|
||||
if (propsFile.isFile() && propsFile.canRead()) {
|
||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
|
||||
switch (props.getProperty("bell-character", "vibrate")) {
|
||||
case "beep":
|
||||
mBellBehaviour = BELL_BEEP;
|
||||
break;
|
||||
case "ignore":
|
||||
mBellBehaviour = BELL_IGNORE;
|
||||
break;
|
||||
default: // "vibrate".
|
||||
mBellBehaviour = BELL_VIBRATE;
|
||||
break;
|
||||
}
|
||||
|
||||
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
||||
|
||||
shortcuts.clear();
|
||||
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
|
||||
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
|
||||
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
|
||||
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(context, "Error loading properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
Log.e("termux", "Error loading props", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final int SHORTCUT_ACTION_CREATE_SESSION = 1;
|
||||
public static final int SHORTCUT_ACTION_NEXT_SESSION = 2;
|
||||
public static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3;
|
||||
public static final int SHORTCUT_ACTION_RENAME_SESSION = 4;
|
||||
|
||||
public final static class KeyboardShortcut {
|
||||
|
||||
public KeyboardShortcut(int codePoint, int shortcutAction) {
|
||||
this.codePoint = codePoint;
|
||||
this.shortcutAction = shortcutAction;
|
||||
}
|
||||
|
||||
final int codePoint;
|
||||
final int shortcutAction;
|
||||
}
|
||||
|
||||
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
|
||||
|
||||
private void parseAction(String name, int shortcutAction, Properties props) {
|
||||
String value = props.getProperty(name);
|
||||
if (value == null) return;
|
||||
String[] parts = value.trim().split("\\+");
|
||||
String input = parts.length == 2 ? parts[1].trim() : null;
|
||||
if (!(parts.length == 2 && parts[0].trim().equalsIgnoreCase("ctrl")) || input.isEmpty() || input.length() > 2) {
|
||||
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
|
||||
return;
|
||||
}
|
||||
|
||||
char c = input.charAt(0);
|
||||
int codePoint = c;
|
||||
if (Character.isLowSurrogate(c)) {
|
||||
if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) {
|
||||
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
|
||||
return;
|
||||
} else {
|
||||
codePoint = Character.toCodePoint(input.charAt(1), c);
|
||||
}
|
||||
}
|
||||
shortcuts.add(new KeyboardShortcut(codePoint, shortcutAction));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.termux.app;
|
||||
package com.termux.app.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
@@ -12,8 +12,10 @@ import android.webkit.WebViewClient;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
/** Basic embedded browser for viewing help pages. */
|
||||
public final class TermuxHelpActivity extends Activity {
|
||||
public final class HelpActivity extends Activity {
|
||||
|
||||
WebView mWebView;
|
||||
|
||||
@@ -39,7 +41,7 @@ public final class TermuxHelpActivity extends Activity {
|
||||
mWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.startsWith("https://termux.com")) {
|
||||
if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
|
||||
// Inline help.
|
||||
setContentView(progressLayout);
|
||||
return false;
|
||||
@@ -60,7 +62,7 @@ public final class TermuxHelpActivity extends Activity {
|
||||
setContentView(mWebView);
|
||||
}
|
||||
});
|
||||
mWebView.loadUrl("https://termux.com/help.html");
|
||||
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.termux.app.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new RootPreferencesFragment())
|
||||
.commit();
|
||||
}
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class RootPreferencesFragment extends PreferenceFragmentCompat {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||
|
||||
configureTermuxTaskerPreference(context);
|
||||
configureAboutPreference(context);
|
||||
configureDonatePreference(context);
|
||||
}
|
||||
|
||||
private void configureTermuxTaskerPreference(@NonNull Context context) {
|
||||
Preference termuxTaskerPrefernce = findPreference("termux_tasker");
|
||||
if (termuxTaskerPrefernce != null) {
|
||||
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
|
||||
// If failed to get app preferences, then likely app is not installed, so do not show its preference
|
||||
termuxTaskerPrefernce.setVisible(preferences != null);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureAboutPreference(@NonNull Context context) {
|
||||
Preference aboutPreference = findPreference("about");
|
||||
if (aboutPreference != null) {
|
||||
aboutPreference.setOnPreferenceClickListener(preference -> {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
String title = "About";
|
||||
|
||||
StringBuilder aboutString = new StringBuilder();
|
||||
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, false));
|
||||
|
||||
String termuxPluginAppsInfo = TermuxUtils.getTermuxPluginAppsInfoMarkdownString(context);
|
||||
if (termuxPluginAppsInfo != null)
|
||||
aboutString.append("\n\n").append(termuxPluginAppsInfo);
|
||||
|
||||
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||
|
||||
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
||||
}
|
||||
}.start();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void configureDonatePreference(@NonNull Context context) {
|
||||
Preference donatePreference = findPreference("donate");
|
||||
if (donatePreference != null) {
|
||||
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
|
||||
if (signingCertificateSHA256Digest != null) {
|
||||
// If APK is a Google Playstore release, then do not show the donation link
|
||||
// since Termux isn't exempted from the playstore policy donation links restriction
|
||||
// Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/
|
||||
String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest);
|
||||
if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) {
|
||||
donatePreference.setVisible(false);
|
||||
return;
|
||||
} else {
|
||||
donatePreference.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
donatePreference.setOnPreferenceClickListener(preference -> {
|
||||
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.termux.app.fragments.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(TermuxPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_preferences, rootKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TermuxPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
private static TermuxPreferencesDataStore mInstance;
|
||||
|
||||
private TermuxPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized TermuxPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new TermuxPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.termux.app.fragments.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(TermuxTaskerPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_tasker_preferences, rootKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TermuxTaskerPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxTaskerAppSharedPreferences mPreferences;
|
||||
|
||||
private static TermuxTaskerPreferencesDataStore mInstance;
|
||||
|
||||
private TermuxTaskerPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized TermuxTaskerPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new TermuxTaskerPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.termux.app.fragments.settings.termux;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_debugging_preferences, rootKey);
|
||||
|
||||
configureLoggingPreferences(context);
|
||||
}
|
||||
|
||||
private void configureLoggingPreferences(@NonNull Context context) {
|
||||
PreferenceCategory loggingCategory = findPreference("logging");
|
||||
if (loggingCategory == null) return;
|
||||
|
||||
ListPreference logLevelListPreference = findPreference("log_level");
|
||||
if (logLevelListPreference != null) {
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context, true);
|
||||
if (preferences == null) return;
|
||||
|
||||
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel());
|
||||
loggingCategory.addPreference(logLevelListPreference);
|
||||
}
|
||||
}
|
||||
|
||||
public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) {
|
||||
if (logLevelListPreference == null)
|
||||
logLevelListPreference = new ListPreference(context);
|
||||
|
||||
CharSequence[] logLevels = Logger.getLogLevelsArray();
|
||||
CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true);
|
||||
|
||||
logLevelListPreference.setEntryValues(logLevels);
|
||||
logLevelListPreference.setEntries(logLevelLabels);
|
||||
|
||||
logLevelListPreference.setValue(String.valueOf(logLevel));
|
||||
logLevelListPreference.setDefaultValue(Logger.DEFAULT_LOG_LEVEL);
|
||||
|
||||
return logLevelListPreference;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
private static DebuggingPreferencesDataStore mInstance;
|
||||
|
||||
private DebuggingPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new DebuggingPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getString(String key, @Nullable String defValue) {
|
||||
if (mPreferences == null) return null;
|
||||
if (key == null) return null;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
return String.valueOf(mPreferences.getLogLevel());
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String key, @Nullable String value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
if (value != null) {
|
||||
mPreferences.setLogLevel(mContext, Integer.parseInt(value));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void putBoolean(String key, boolean value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "terminal_view_key_logging_enabled":
|
||||
mPreferences.setTerminalViewKeyLoggingEnabled(value);
|
||||
break;
|
||||
case "plugin_error_notifications_enabled":
|
||||
mPreferences.setPluginErrorNotificationsEnabled(value);
|
||||
break;
|
||||
case "crash_report_notifications_enabled":
|
||||
mPreferences.setCrashReportNotificationsEnabled(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
if (mPreferences == null) return false;
|
||||
switch (key) {
|
||||
case "terminal_view_key_logging_enabled":
|
||||
return mPreferences.isTerminalViewKeyLoggingEnabled();
|
||||
case "plugin_error_notifications_enabled":
|
||||
return mPreferences.arePluginErrorNotificationsEnabled();
|
||||
case "crash_report_notifications_enabled":
|
||||
return mPreferences.areCrashReportNotificationsEnabled();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.termux.app.fragments.settings.termux;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_terminal_io_preferences, rootKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TerminalIOPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
private static TerminalIOPreferencesDataStore mInstance;
|
||||
|
||||
private TerminalIOPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new TerminalIOPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void putBoolean(String key, boolean value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "soft_keyboard_enabled":
|
||||
mPreferences.setSoftKeyboardEnabled(value);
|
||||
break;
|
||||
case "soft_keyboard_enabled_only_if_no_hardware":
|
||||
mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
if (mPreferences == null) return false;
|
||||
|
||||
switch (key) {
|
||||
case "soft_keyboard_enabled":
|
||||
return mPreferences.isSoftKeyboardEnabled();
|
||||
case "soft_keyboard_enabled_only_if_no_hardware":
|
||||
return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.termux.app.fragments.settings.termux_tasker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_tasker_debugging_preferences, rootKey);
|
||||
|
||||
configureLoggingPreferences(context);
|
||||
}
|
||||
|
||||
private void configureLoggingPreferences(@NonNull Context context) {
|
||||
PreferenceCategory loggingCategory = findPreference("logging");
|
||||
if (loggingCategory == null) return;
|
||||
|
||||
ListPreference logLevelListPreference = findPreference("log_level");
|
||||
if (logLevelListPreference != null) {
|
||||
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, true);
|
||||
if (preferences == null) return;
|
||||
|
||||
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
|
||||
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
|
||||
loggingCategory.addPreference(logLevelListPreference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxTaskerAppSharedPreferences mPreferences;
|
||||
|
||||
private static DebuggingPreferencesDataStore mInstance;
|
||||
|
||||
private DebuggingPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new DebuggingPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getString(String key, @Nullable String defValue) {
|
||||
if (mPreferences == null) return null;
|
||||
if (key == null) return null;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
return String.valueOf(mPreferences.getLogLevel(true));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String key, @Nullable String value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
if (value != null) {
|
||||
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
20
app/src/main/java/com/termux/app/models/UserAction.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.termux.app.models;
|
||||
|
||||
public enum UserAction {
|
||||
|
||||
ABOUT("about"),
|
||||
CRASH_REPORT("crash report"),
|
||||
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
|
||||
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
||||
|
||||
private final String name;
|
||||
|
||||
UserAction(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.termux.app.settings.properties;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class TermuxAppSharedProperties extends TermuxSharedProperties {
|
||||
|
||||
private ExtraKeysInfo mExtraKeysInfo;
|
||||
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
||||
|
||||
private static final String LOG_TAG = "TermuxAppSharedProperties";
|
||||
|
||||
public TermuxAppSharedProperties(@Nonnull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the termux properties from disk into an in-memory cache.
|
||||
*/
|
||||
@Override
|
||||
public void loadTermuxPropertiesFromDisk() {
|
||||
super.loadTermuxPropertiesFromDisk();
|
||||
|
||||
setExtraKeys();
|
||||
setSessionShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal extra keys and style.
|
||||
*/
|
||||
private void setExtraKeys() {
|
||||
mExtraKeysInfo = null;
|
||||
|
||||
try {
|
||||
// The mMap stores the extra key and style string values while loading properties
|
||||
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
|
||||
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
||||
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
|
||||
} catch (JSONException e) {
|
||||
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
||||
|
||||
try {
|
||||
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||
} catch (JSONException e2) {
|
||||
Logger.showToast(mContext, "Can't create default extra keys",true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||
mExtraKeysInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal sessions shortcuts.
|
||||
*/
|
||||
private void setSessionShortcuts() {
|
||||
if (mSessionShortcuts == null)
|
||||
mSessionShortcuts = new ArrayList<>();
|
||||
else
|
||||
mSessionShortcuts.clear();
|
||||
|
||||
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
|
||||
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
|
||||
// The mMap stores the code points for the session shortcuts while loading properties
|
||||
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
|
||||
// If codePoint is null, then session shortcut did not exist in properties or was invalid
|
||||
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
|
||||
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
|
||||
// add the code point to sessionShortcuts
|
||||
if (codePoint != null)
|
||||
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
public List<KeyboardShortcut> getSessionShortcuts() {
|
||||
return mSessionShortcuts;
|
||||
}
|
||||
|
||||
public ExtraKeysInfo getExtraKeysInfo() {
|
||||
return mExtraKeysInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk.
|
||||
*/
|
||||
public static int getTerminalTranscriptRows(Context context) {
|
||||
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
|
||||
|
||||
/**
|
||||
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
|
||||
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
|
||||
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
|
||||
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
|
||||
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
|
||||
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
|
||||
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
|
||||
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
|
||||
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
|
||||
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
|
||||
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
|
||||
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
|
||||
*
|
||||
* With the above configuration, the additional clipboard suggestions row partially covers the
|
||||
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
|
||||
* in the Android OS where it does not consider the candidate's view height in its calculation to push
|
||||
* up the view or because Gboard does not include the candidate's view height in the height reported
|
||||
* to android that should be used, hence causing an overlap.
|
||||
*
|
||||
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
|
||||
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
|
||||
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
|
||||
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
|
||||
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
|
||||
* Also check https://github.com/termux/termux-app/issues/1539.
|
||||
*
|
||||
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
|
||||
* like number row, etc.
|
||||
*
|
||||
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
|
||||
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
|
||||
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
|
||||
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
|
||||
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
|
||||
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
|
||||
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
|
||||
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
|
||||
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
|
||||
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
|
||||
*/
|
||||
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
public TermuxActivity mActivity;
|
||||
public Integer marginBottom;
|
||||
public Integer lastMarginBottom;
|
||||
public long lastMarginBottomTime;
|
||||
public long lastMarginBottomExtraTime;
|
||||
|
||||
/** Log root view events. */
|
||||
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
|
||||
|
||||
private static final String LOG_TAG = "TermuxActivityRootView";
|
||||
|
||||
private static int mStatusBarHeight;
|
||||
|
||||
public TermuxActivityRootView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setActivity(TermuxActivity activity) {
|
||||
mActivity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether root view logging is enabled or not.
|
||||
*
|
||||
* @param value The boolean value that defines the state.
|
||||
*/
|
||||
public void setIsRootViewLoggingEnabled(boolean value) {
|
||||
ROOT_VIEW_LOGGING_ENABLED = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
if (marginBottom != null) {
|
||||
if (ROOT_VIEW_LOGGING_ENABLED)
|
||||
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
|
||||
params.setMargins(0, 0, 0, marginBottom);
|
||||
setLayoutParams(params);
|
||||
marginBottom = null;
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
if (mActivity == null || !mActivity.isVisible()) return;
|
||||
|
||||
View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView();
|
||||
if (bottomSpaceView == null) return;
|
||||
|
||||
boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED;
|
||||
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:");
|
||||
|
||||
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
// Get the position Rects of the bottom space view and the main window holding it
|
||||
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight);
|
||||
if (windowAndViewRects == null)
|
||||
return;
|
||||
|
||||
Rect windowAvailableRect = windowAndViewRects[0];
|
||||
Rect bottomSpaceViewRect = windowAndViewRects[1];
|
||||
|
||||
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
|
||||
//boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape
|
||||
boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect);
|
||||
boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0;
|
||||
boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0);
|
||||
|
||||
if (root_view_logging_enabled) {
|
||||
Logger.logVerbose(LOG_TAG, "windowAvailableRect " + ViewUtils.toRectString(windowAvailableRect) + ", bottomSpaceViewRect " + ViewUtils.toRectString(bottomSpaceViewRect));
|
||||
Logger.logVerbose(LOG_TAG, "windowAvailableRect.bottom " + windowAvailableRect.bottom +
|
||||
", bottomSpaceViewRect.bottom " +bottomSpaceViewRect.bottom +
|
||||
", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin +
|
||||
", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) +
|
||||
", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
|
||||
}
|
||||
|
||||
// If the bottomSpaceViewRect is visible, then remove the margin if needed
|
||||
if (isVisible) {
|
||||
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
|
||||
// and a margin has been added
|
||||
// Necessary so that we don't get stuck in an infinite loop since setting margin
|
||||
// will call OnGlobalLayoutListener again and next time bottom space view
|
||||
// will be visible and margin will be set to 0, which again will call
|
||||
// OnGlobalLayoutListener...
|
||||
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
|
||||
// set appropriate margins when views are changed quickly since some changes
|
||||
// may be missed.
|
||||
if (isVisibleBecauseMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Visible due to margin");
|
||||
|
||||
// Once the view has been redrawn with new margin, we set margin back to 0 so that
|
||||
// when next time onMeasure() is called, margin 0 is used. This is necessary for
|
||||
// cases when view has been redrawn with new margin because bottom space view was
|
||||
// hidden by keyboard and then view was redrawn again due to layout change (like
|
||||
// keyboard symbol view is switched to), android will add margin below its new position
|
||||
// if its greater than 0, which was already above the keyboard creating x2x margin.
|
||||
// Adding time check since moving split screen divider in landscape causes jitter
|
||||
// and prevents some infinite loops
|
||||
if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) {
|
||||
lastMarginBottomTime = System.currentTimeMillis();
|
||||
marginBottom = 0;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Ignoring restoring marginBottom to 0 since called to quickly");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
boolean setMargin = params.bottomMargin != 0;
|
||||
|
||||
// If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect
|
||||
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||
// onGlobalLayout: Bottom margin already equals 0
|
||||
if (isVisibleBecauseExtraMargin) {
|
||||
// Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar
|
||||
if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Resetting margin since visible due to extra margin");
|
||||
lastMarginBottomExtraTime = System.currentTimeMillis();
|
||||
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
|
||||
lastMarginBottom = null;
|
||||
setMargin = true;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Ignoring resetting margin since visible due to extra margin since called to quickly");
|
||||
}
|
||||
}
|
||||
|
||||
if (setMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Setting bottom margin to 0");
|
||||
params.setMargins(0, 0, 0, 0);
|
||||
setLayoutParams(params);
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Bottom margin already equals 0");
|
||||
// This is done so that when next time onMeasure() is called, lastMarginBottom is used.
|
||||
// This is done since we **expect** the keyboard to have same dimensions next time layout
|
||||
// changes, so best set margin while view is drawn the first time, otherwise it will
|
||||
// cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the
|
||||
// likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic
|
||||
// works fine for all cases.
|
||||
marginBottom = lastMarginBottom;
|
||||
}
|
||||
}
|
||||
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
|
||||
else {
|
||||
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
|
||||
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
|
||||
|
||||
boolean setMargin = params.bottomMargin != pxHidden;
|
||||
|
||||
// If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect
|
||||
// is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener
|
||||
// again, so that margins are set properly. May happen when toolbar/extra keys is disabled
|
||||
// and enabled from left drawer, just like case for isVisibleBecauseExtraMargin.
|
||||
// onMeasure: Setting bottom margin to 176
|
||||
// onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||
// onGlobalLayout: Bottom margin already equals 176
|
||||
if (pxHidden > 0 && params.bottomMargin > 0) {
|
||||
if (pxHidden != params.bottomMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since not visible due to wrong margin");
|
||||
pxHidden = 0;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin since not visible despite required margin");
|
||||
}
|
||||
setMargin = true;
|
||||
}
|
||||
|
||||
if (pxHidden < 0) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative");
|
||||
pxHidden = 0;
|
||||
}
|
||||
|
||||
|
||||
if (setMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Setting bottom margin to " + pxHidden);
|
||||
params.setMargins(0, 0, 0, pxHidden);
|
||||
setLayoutParams(params);
|
||||
lastMarginBottom = pxHidden;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||
// Let view window handle insets however it wants
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession> implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
||||
|
||||
public TermuxSessionsListViewController(TermuxActivity activity, List<TermuxSession> sessionList) {
|
||||
super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, sessionList);
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
View sessionRowView = convertView;
|
||||
if (sessionRowView == null) {
|
||||
LayoutInflater inflater = mActivity.getLayoutInflater();
|
||||
sessionRowView = inflater.inflate(R.layout.item_terminal_sessions_list, parent, false);
|
||||
}
|
||||
|
||||
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
|
||||
|
||||
TerminalSession sessionAtRow = getItem(position).getTerminalSession();
|
||||
if (sessionAtRow == null) {
|
||||
sessionTitleView.setText("null session");
|
||||
return sessionRowView;
|
||||
}
|
||||
|
||||
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||
|
||||
if (isUsingBlackUI) {
|
||||
sessionTitleView.setBackground(
|
||||
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||
);
|
||||
}
|
||||
|
||||
String name = sessionAtRow.mSessionName;
|
||||
String sessionTitle = sessionAtRow.getTitle();
|
||||
|
||||
String numberPart = "[" + (position + 1) + "] ";
|
||||
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
|
||||
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
|
||||
|
||||
String fullSessionTitle = numberPart + sessionNamePart + sessionTitlePart;
|
||||
SpannableString fullSessionTitleStyled = new SpannableString(fullSessionTitle);
|
||||
fullSessionTitleStyled.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
fullSessionTitleStyled.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), fullSessionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
sessionTitleView.setText(fullSessionTitleStyled);
|
||||
|
||||
boolean sessionRunning = sessionAtRow.isRunning();
|
||||
|
||||
if (sessionRunning) {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
} else {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
}
|
||||
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||
sessionTitleView.setTextColor(color);
|
||||
return sessionRowView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
TermuxSession clickedSession = getItem(position);
|
||||
mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession());
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final TermuxSession selectedSession = getItem(position);
|
||||
mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.getTerminalSession());
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.SoundPool;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.app.terminal.io.BellHandler;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TextStyle;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
||||
|
||||
private final TermuxActivity mActivity;
|
||||
|
||||
private static final int MAX_SESSIONS = 8;
|
||||
|
||||
private SoundPool mBellSoundPool;
|
||||
|
||||
private int mBellSoundId;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
||||
|
||||
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onCreate() is called
|
||||
*/
|
||||
public void onCreate() {
|
||||
// Set terminal fonts and colors
|
||||
checkForFontAndColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onStart() is called
|
||||
*/
|
||||
public void onStart() {
|
||||
// The service has connected, but data may have changed since we were last in the foreground.
|
||||
// Get the session stored in shared preferences stored by {@link #onStop} if its valid,
|
||||
// otherwise get the last session currently running.
|
||||
if (mActivity.getTermuxService() != null) {
|
||||
setCurrentSession(getCurrentStoredSessionOrLast());
|
||||
termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
// The current terminal session may have changed while being away, force
|
||||
// a refresh of the displayed terminal.
|
||||
mActivity.getTerminalView().onScreenUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onResume() is called
|
||||
*/
|
||||
public void onResume() {
|
||||
// Just initialize the mBellSoundPool and load the sound, otherwise bell might not run
|
||||
// the first time bell key is pressed and play() is called, since sound may not be loaded
|
||||
// quickly enough before the call to play(). https://stackoverflow.com/questions/35435625
|
||||
getBellSoundPool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onStop() is called
|
||||
*/
|
||||
public void onStop() {
|
||||
// Store current session in shared preferences so that it can be restored later in
|
||||
// {@link #onStart} if needed.
|
||||
setCurrentStoredSession();
|
||||
|
||||
// Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown
|
||||
// java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds
|
||||
// Bell is not played in background anyways
|
||||
// Related: https://stackoverflow.com/a/28708351/14686958
|
||||
releaseBellSoundPool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReload() {
|
||||
// Set terminal fonts and colors
|
||||
checkForFontAndColors();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession updatedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (updatedSession != mActivity.getCurrentSession()) {
|
||||
// Only show toast for other sessions than the current one, since the user
|
||||
// probably consciously caused the title change to change in the current session
|
||||
// and don't want an annoying toast for that.
|
||||
mActivity.showToast(toToastTitle(updatedSession), true);
|
||||
}
|
||||
|
||||
termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
|
||||
if (service == null || service.wantsToStop()) {
|
||||
// The service wants to stop as soon as possible.
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
|
||||
// Show toast for non-current sessions that exit.
|
||||
int indexOfSession = service.getIndexOfSession(finishedSession);
|
||||
// Verify that session was not removed before we got told about it finishing:
|
||||
if (indexOfSession >= 0)
|
||||
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
|
||||
}
|
||||
|
||||
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||
// On Android TV devices we need to use older behaviour because we may
|
||||
// not be able to have multiple launcher icons.
|
||||
if (service.getTermuxSessionsSize() > 1) {
|
||||
removeFinishedSession(finishedSession);
|
||||
}
|
||||
} else {
|
||||
// Once we have a separate launcher icon for the failsafe session, it
|
||||
// should be safe to auto-close session on exit code '0' or '130'.
|
||||
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
|
||||
removeFinishedSession(finishedSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClipboardText(TerminalSession session, String text) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
|
||||
BellHandler.getInstance(mActivity).doBell();
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
||||
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||
// Ignore the bell character.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession changedSession) {
|
||||
if (mActivity.getCurrentSession() == changedSession)
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminalCursorStateChange(boolean enabled) {
|
||||
// Do not start cursor blinking thread if activity is not visible
|
||||
if (enabled && !mActivity.isVisible()) {
|
||||
Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible");
|
||||
return;
|
||||
}
|
||||
|
||||
// If cursor is to enabled now, then start cursor blinking if blinking is enabled
|
||||
// otherwise stop cursor blinking
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onResetTerminalSession() is called
|
||||
*/
|
||||
public void onResetTerminalSession() {
|
||||
// Ensure blinker starts again after reset if cursor blinking was disabled before reset like
|
||||
// with "tput civis" which would have called onTerminalCursorStateChange()
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Integer getTerminalCursorStyle() {
|
||||
return mActivity.getProperties().getTerminalCursorStyle();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Initialize and get mBellSoundPool */
|
||||
private synchronized SoundPool getBellSoundPool() {
|
||||
if (mBellSoundPool == null) {
|
||||
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
|
||||
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||
}
|
||||
|
||||
return mBellSoundPool;
|
||||
}
|
||||
|
||||
/** Release mBellSoundPool resources */
|
||||
private synchronized void releaseBellSoundPool() {
|
||||
if (mBellSoundPool != null) {
|
||||
mBellSoundPool.release();
|
||||
mBellSoundPool = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Try switching to session. */
|
||||
public void setCurrentSession(TerminalSession session) {
|
||||
if (session == null) return;
|
||||
|
||||
if (mActivity.getTerminalView().attachSession(session)) {
|
||||
// notify about switched session if not already displaying the session
|
||||
notifyOfSessionChange();
|
||||
}
|
||||
|
||||
// We call the following even when the session is already being displayed since config may
|
||||
// be stale, like current session not selected or scrolled to.
|
||||
checkAndScrollToSession(session);
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
||||
void notifyOfSessionChange() {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
mActivity.showToast(toToastTitle(session), false);
|
||||
}
|
||||
}
|
||||
|
||||
public void switchToSession(boolean forward) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
|
||||
int index = service.getIndexOfSession(currentTerminalSession);
|
||||
int size = service.getTermuxSessionsSize();
|
||||
if (forward) {
|
||||
if (++index >= size) index = 0;
|
||||
} else {
|
||||
if (--index < 0) index = size - 1;
|
||||
}
|
||||
|
||||
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||
if (termuxSession != null)
|
||||
setCurrentSession(termuxSession.getTerminalSession());
|
||||
}
|
||||
|
||||
public void switchToSession(int index) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||
if (termuxSession != null)
|
||||
setCurrentSession(termuxSession.getTerminalSession());
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
public void renameSession(final TerminalSession sessionToRename) {
|
||||
if (sessionToRename == null) return;
|
||||
|
||||
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||
sessionToRename.mSessionName = text;
|
||||
termuxSessionListNotifyUpdated();
|
||||
}, -1, null, -1, null, null);
|
||||
}
|
||||
|
||||
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
if (service.getTermuxSessionsSize() >= MAX_SESSIONS) {
|
||||
new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
|
||||
.setPositiveButton(android.R.string.ok, null).show();
|
||||
} else {
|
||||
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||
|
||||
String workingDirectory;
|
||||
if (currentSession == null) {
|
||||
workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory();
|
||||
} else {
|
||||
workingDirectory = currentSession.getCwd();
|
||||
}
|
||||
|
||||
TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
|
||||
if (newTermuxSession == null) return;
|
||||
|
||||
TerminalSession newTerminalSession = newTermuxSession.getTerminalSession();
|
||||
setCurrentSession(newTerminalSession);
|
||||
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentStoredSession() {
|
||||
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||
if (currentSession != null)
|
||||
mActivity.getPreferences().setCurrentSession(currentSession.mHandle);
|
||||
else
|
||||
mActivity.getPreferences().setCurrentSession(null);
|
||||
}
|
||||
|
||||
/** The current session as stored or the last one if that does not exist. */
|
||||
public TerminalSession getCurrentStoredSessionOrLast() {
|
||||
TerminalSession stored = getCurrentStoredSession();
|
||||
|
||||
if (stored != null) {
|
||||
// If a stored session is in the list of currently running sessions, then return it
|
||||
return stored;
|
||||
} else {
|
||||
// Else return the last session currently running
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return null;
|
||||
|
||||
TermuxSession termuxSession = service.getLastTermuxSession();
|
||||
if (termuxSession != null)
|
||||
return termuxSession.getTerminalSession();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalSession getCurrentStoredSession() {
|
||||
String sessionHandle = mActivity.getPreferences().getCurrentSession();
|
||||
|
||||
// If no session is stored in shared preferences
|
||||
if (sessionHandle == null)
|
||||
return null;
|
||||
|
||||
// Check if the session handle found matches one of the currently running sessions
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return null;
|
||||
|
||||
return service.getTerminalSessionForHandle(sessionHandle);
|
||||
}
|
||||
|
||||
public void removeFinishedSession(TerminalSession finishedSession) {
|
||||
// Return pressed with finished session - remove it.
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
int index = service.removeTermuxSession(finishedSession);
|
||||
|
||||
int size = service.getTermuxSessionsSize();
|
||||
if (size == 0) {
|
||||
// There are no sessions to show, so finish the activity.
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
} else {
|
||||
if (index >= size) {
|
||||
index = size - 1;
|
||||
}
|
||||
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||
if (termuxSession != null)
|
||||
setCurrentSession(termuxSession.getTerminalSession());
|
||||
}
|
||||
}
|
||||
|
||||
public void termuxSessionListNotifyUpdated() {
|
||||
mActivity.termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
public void checkAndScrollToSession(TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
final int indexOfSession = service.getIndexOfSession(session);
|
||||
if (indexOfSession < 0) return;
|
||||
final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list);
|
||||
if (termuxSessionsListView == null) return;
|
||||
|
||||
termuxSessionsListView.setItemChecked(indexOfSession, true);
|
||||
// Delay is necessary otherwise sometimes scroll to newly added session does not happen
|
||||
termuxSessionsListView.postDelayed(() -> termuxSessionsListView.smoothScrollToPosition(indexOfSession), 1000);
|
||||
}
|
||||
|
||||
|
||||
String toToastTitle(TerminalSession session) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return null;
|
||||
|
||||
final int indexOfSession = service.getIndexOfSession(session);
|
||||
if (indexOfSession < 0) return null;
|
||||
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
|
||||
if (!TextUtils.isEmpty(session.mSessionName)) {
|
||||
toastTitle.append(" ").append(session.mSessionName);
|
||||
}
|
||||
String title = session.getTitle();
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
// Space to "[${NR}] or newline after session name:
|
||||
toastTitle.append(session.mSessionName == null ? " " : "\n");
|
||||
toastTitle.append(title);
|
||||
}
|
||||
return toastTitle.toString();
|
||||
}
|
||||
|
||||
|
||||
public void checkForFontAndColors() {
|
||||
try {
|
||||
File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE;
|
||||
File fontFile = TermuxConstants.TERMUX_FONT_FILE;
|
||||
|
||||
final Properties props = new Properties();
|
||||
if (colorsFile.isFile()) {
|
||||
try (InputStream in = new FileInputStream(colorsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
|
||||
TerminalColors.COLOR_SCHEME.updateWith(props);
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null && session.getEmulator() != null) {
|
||||
session.getEmulator().mColors.reset();
|
||||
}
|
||||
updateBackgroundColor();
|
||||
|
||||
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
||||
mActivity.getTerminalView().setTypeface(newTypeface);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateBackgroundColor() {
|
||||
if (!mActivity.isVisible()) return;
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null && session.getEmulator() != null) {
|
||||
mActivity.getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.data.UrlUtils;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.view.KeyboardUtils;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
||||
private Runnable mShowSoftKeyboardRunnable;
|
||||
|
||||
private boolean mShowSoftKeyboardIgnoreOnce;
|
||||
private boolean mShowSoftKeyboardWithDelayOnce;
|
||||
|
||||
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
||||
|
||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||
this.mActivity = activity;
|
||||
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onCreate() is called
|
||||
*/
|
||||
public void onCreate() {
|
||||
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onStart() is called
|
||||
*/
|
||||
public void onStart() {
|
||||
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
|
||||
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
|
||||
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
|
||||
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||
|
||||
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
|
||||
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onResume() is called
|
||||
*/
|
||||
public void onResume() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(true, false);
|
||||
|
||||
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||
|
||||
if (mActivity.getTerminalView().mEmulator != null) {
|
||||
// Start terminal cursor blinking if enabled
|
||||
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
|
||||
// event to start it. This is needed since onEmulatorSet() may not be called after
|
||||
// TermuxActivity is started after device display timeout with double tap and not power button.
|
||||
setTerminalCursorBlinkerState(true);
|
||||
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onStop() is called
|
||||
*/
|
||||
public void onStop() {
|
||||
// Stop terminal cursor blinking if enabled
|
||||
setTerminalCursorBlinkerState(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReload() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(false, true);
|
||||
|
||||
// Start terminal cursor blinking if enabled
|
||||
setTerminalCursorBlinkerState(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when {@link com.termux.view.TerminalView#mEmulator}
|
||||
*/
|
||||
@Override
|
||||
public void onEmulatorSet() {
|
||||
if (!mTerminalCursorBlinkerStateAlreadySet) {
|
||||
// Start terminal cursor blinking if enabled
|
||||
// We need to wait for the first session to be attached that's set in
|
||||
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
|
||||
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
|
||||
// blinker will not start again if TermuxActivity is started again after exiting it with
|
||||
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
|
||||
setTerminalCursorBlinkerState(true);
|
||||
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public float onScale(float scale) {
|
||||
if (scale < 0.9f || scale > 1.1f) {
|
||||
boolean increase = scale > 1.f;
|
||||
changeFontSize(increase);
|
||||
return 1.0f;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
else
|
||||
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBackButtonBeMappedToEscape() {
|
||||
return mActivity.getProperties().isBackKeyTheEscapeKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnforceCharBasedInput() {
|
||||
return mActivity.getProperties().isEnforcingCharBasedInput();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldUseCtrlSpaceWorkaround() {
|
||||
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void copyModeChanged(boolean copyMode) {
|
||||
// Disable drawer while copying.
|
||||
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
||||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
||||
return true;
|
||||
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
||||
// Get the unmodified code point:
|
||||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||
onToggleSoftKeyboardRequest();
|
||||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mActivity.getTerminalView().showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
mTermuxTerminalSessionClient.renameSession(currentSession);
|
||||
} else if (unicodeChar == 'c'/* create */) {
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
} else if (unicodeChar == 'u' /* urls */) {
|
||||
showUrlSelection();
|
||||
} else if (unicodeChar == 'v') {
|
||||
doPaste();
|
||||
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
|
||||
// We also check for the shifted char here since shift may be required to produce '+',
|
||||
// see https://github.com/termux/termux-api/issues/2
|
||||
changeFontSize(true);
|
||||
} else if (unicodeChar == '-') {
|
||||
changeFontSize(false);
|
||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||
int index = unicodeChar - '1';
|
||||
mTermuxTerminalSessionClient.switchToSession(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
|
||||
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
return true;
|
||||
}
|
||||
|
||||
return handleVirtualKeys(keyCode, e, false);
|
||||
}
|
||||
|
||||
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||
InputDevice inputDevice = event.getDevice();
|
||||
if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) {
|
||||
return false;
|
||||
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||
// Do not steal dedicated buttons from a full external keyboard.
|
||||
return false;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
mVirtualControlKeyDown = down;
|
||||
return true;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
mVirtualFnKeyDown = down;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean readControlKey() {
|
||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readAltKey() {
|
||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongPress(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||
if (mVirtualFnKeyDown) {
|
||||
int resultingKeyCode = -1;
|
||||
int resultingCodePoint = -1;
|
||||
boolean altDown = false;
|
||||
int lowerCase = Character.toLowerCase(codePoint);
|
||||
switch (lowerCase) {
|
||||
// Arrow keys.
|
||||
case 'w':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||
break;
|
||||
case 'a':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case 's':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
break;
|
||||
case 'd':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
break;
|
||||
|
||||
// Page up and down.
|
||||
case 'p':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
|
||||
break;
|
||||
case 'n':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
||||
break;
|
||||
|
||||
// Some special keys:
|
||||
case 't':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_TAB;
|
||||
break;
|
||||
case 'i':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
||||
break;
|
||||
case 'h':
|
||||
resultingCodePoint = '~';
|
||||
break;
|
||||
|
||||
// Special characters to input.
|
||||
case 'u':
|
||||
resultingCodePoint = '_';
|
||||
break;
|
||||
case 'l':
|
||||
resultingCodePoint = '|';
|
||||
break;
|
||||
|
||||
// Function keys.
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
|
||||
break;
|
||||
case '0':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_F10;
|
||||
break;
|
||||
|
||||
// Other special keys.
|
||||
case 'e':
|
||||
resultingCodePoint = /*Escape*/ 27;
|
||||
break;
|
||||
case '.':
|
||||
resultingCodePoint = /*^.*/ 28;
|
||||
break;
|
||||
|
||||
case 'b': // alt+b, jumping backward in readline.
|
||||
case 'f': // alf+f, jumping forward in readline.
|
||||
case 'x': // alt+x, common in emacs.
|
||||
resultingCodePoint = lowerCase;
|
||||
altDown = true;
|
||||
break;
|
||||
|
||||
// Volume control.
|
||||
case 'v':
|
||||
resultingCodePoint = -1;
|
||||
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
|
||||
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
||||
break;
|
||||
|
||||
// Writing mode:
|
||||
case 'q':
|
||||
case 'k':
|
||||
mActivity.toggleTerminalToolbar();
|
||||
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
|
||||
break;
|
||||
}
|
||||
|
||||
if (resultingKeyCode != -1) {
|
||||
TerminalEmulator term = session.getEmulator();
|
||||
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
|
||||
} else if (resultingCodePoint != -1) {
|
||||
session.writeCodePoint(altDown, resultingCodePoint);
|
||||
}
|
||||
return true;
|
||||
} else if (ctrlDown) {
|
||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
||||
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||
KeyboardShortcut shortcut = shortcuts.get(i);
|
||||
if (codePointLowerCase == shortcut.codePoint) {
|
||||
switch (shortcut.shortcutAction) {
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void changeFontSize(boolean increase) {
|
||||
mActivity.getPreferences().changeFontSize(increase);
|
||||
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in
|
||||
* drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut.
|
||||
*/
|
||||
public void onToggleSoftKeyboardRequest() {
|
||||
// If soft keyboard toggle behaviour is enable/disabled
|
||||
if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) {
|
||||
// If soft keyboard is visible
|
||||
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) {
|
||||
Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle");
|
||||
mActivity.getPreferences().setSoftKeyboardEnabled(false);
|
||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
} else {
|
||||
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
|
||||
// switching back from another app if keyboard was previously disabled by user.
|
||||
// Also request focus, since it wouldn't have been requested at startup by
|
||||
// setSoftKeyboardState if keyboard was disabled. #2112
|
||||
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
|
||||
mActivity.getPreferences().setSoftKeyboardEnabled(true);
|
||||
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||
if(mShowSoftKeyboardWithDelayOnce) {
|
||||
mShowSoftKeyboardWithDelayOnce = false;
|
||||
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
} else
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
}
|
||||
}
|
||||
// If soft keyboard toggle behaviour is show/hide
|
||||
else {
|
||||
// If soft keyboard is disabled by user for Termux
|
||||
if (!mActivity.getPreferences().isSoftKeyboardEnabled()) {
|
||||
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle");
|
||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
} else {
|
||||
Logger.logVerbose(LOG_TAG, "Showing/Hiding soft keyboard on toggle");
|
||||
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||
KeyboardUtils.toggleSoftKeyboard(mActivity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
||||
boolean noRequestFocus = false;
|
||||
|
||||
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
||||
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
||||
mActivity.getPreferences().isSoftKeyboardEnabled(),
|
||||
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
||||
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
noRequestFocus = true;
|
||||
// Delay is only required if onCreate() is called like when Termux app is exited with
|
||||
// double back press, not when Termux app is switched back from another app and keyboard
|
||||
// toggle is pressed to enable keyboard
|
||||
if (isStartup && mActivity.isOnResumeAfterOnCreate())
|
||||
mShowSoftKeyboardWithDelayOnce = true;
|
||||
} else {
|
||||
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
|
||||
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
|
||||
|
||||
// Clear any previous flags to disable soft keyboard in case setting updated
|
||||
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||
|
||||
// If soft keyboard is to be hidden on startup
|
||||
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
|
||||
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
|
||||
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
// Required to keep keyboard hidden when Termux app is switched back from another app
|
||||
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
||||
noRequestFocus = true;
|
||||
// Required to keep keyboard hidden on app startup
|
||||
mShowSoftKeyboardIgnoreOnce = true;
|
||||
}
|
||||
}
|
||||
|
||||
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View view, boolean hasFocus) {
|
||||
// Force show soft keyboard if TerminalView or toolbar text input view has
|
||||
// focus and close it if they don't
|
||||
boolean textInputViewHasFocus = false;
|
||||
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
|
||||
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
|
||||
|
||||
if (hasFocus || textInputViewHasFocus) {
|
||||
if (mShowSoftKeyboardIgnoreOnce) {
|
||||
mShowSoftKeyboardIgnoreOnce = false; return;
|
||||
}
|
||||
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
||||
} else {
|
||||
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
|
||||
}
|
||||
|
||||
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
|
||||
}
|
||||
});
|
||||
|
||||
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
||||
// or soft keyboard is to be hidden or is disabled
|
||||
if (!isReloadTermuxProperties && !noRequestFocus) {
|
||||
// Request focus for TerminalView
|
||||
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
|
||||
// had focus on startup to show the keyboard, like when opening url with context menu
|
||||
// "Select URL" long press and returning to Termux app with back button. This
|
||||
// will also show keyboard even if it was closed before opening url. #2111
|
||||
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
private Runnable getShowSoftKeyboardRunnable() {
|
||||
if (mShowSoftKeyboardRunnable == null) {
|
||||
mShowSoftKeyboardRunnable = () -> {
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
};
|
||||
}
|
||||
return mShowSoftKeyboardRunnable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setTerminalCursorBlinkerState(boolean start) {
|
||||
if (start) {
|
||||
// If set/update the cursor blinking rate is successful, then enable cursor blinker
|
||||
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||
else
|
||||
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
|
||||
} else {
|
||||
// Disable cursor blinker
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void shareSessionTranscript() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||
if (transcriptText == null) return;
|
||||
|
||||
try {
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
||||
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void showUrlSelection() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
|
||||
if (urlSet.isEmpty()) {
|
||||
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||
return;
|
||||
}
|
||||
|
||||
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
|
||||
Collections.reverse(Arrays.asList(urls)); // Latest first.
|
||||
|
||||
// Click to copy url to clipboard:
|
||||
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||
String url = (String) urls[which];
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
}).setTitle(R.string.title_select_url_dialog).create();
|
||||
|
||||
// Long press to open URL:
|
||||
dialog.setOnShowListener(di -> {
|
||||
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
|
||||
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||
dialog.dismiss();
|
||||
String url = (String) urls[position];
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
try {
|
||||
mActivity.startActivity(i, null);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// If no applications match, Android displays a system message.
|
||||
mActivity.startActivity(Intent.createChooser(i, null));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void reportIssueFromTranscript() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||
if (transcriptText == null) return;
|
||||
|
||||
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
String transcriptTextTruncated = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
||||
|
||||
reportString.append("## Transcript\n");
|
||||
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true));
|
||||
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
||||
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
|
||||
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(), TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public void doPaste() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
if (!session.isRunning()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData == null) return;
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||
if (!TextUtils.isEmpty(paste))
|
||||
session.getEmulator().paste(paste.toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Vibrator;
|
||||
|
||||
public class BellHandler {
|
||||
private static BellHandler instance = null;
|
||||
private static final Object lock = new Object();
|
||||
|
||||
public static BellHandler getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (lock) {
|
||||
if (instance == null) {
|
||||
instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static final long DURATION = 50;
|
||||
private static final long MIN_PAUSE = 3 * DURATION;
|
||||
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private long lastBell = 0;
|
||||
private final Runnable bellRunnable;
|
||||
|
||||
private BellHandler(final Vibrator vibrator) {
|
||||
bellRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (vibrator != null) {
|
||||
vibrator.vibrate(DURATION);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public synchronized void doBell() {
|
||||
long now = now();
|
||||
long timeSinceLastBell = now - lastBell;
|
||||
|
||||
if (timeSinceLastBell < 0) {
|
||||
// there is a next bell pending; don't schedule another one
|
||||
} else if (timeSinceLastBell < MIN_PAUSE) {
|
||||
// there was a bell recently, schedule the next one
|
||||
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
||||
lastBell = lastBell + MIN_PAUSE;
|
||||
} else {
|
||||
// the last bell was long ago, do it now
|
||||
bellRunnable.run();
|
||||
lastBell = now;
|
||||
}
|
||||
}
|
||||
|
||||
private long now() {
|
||||
return SystemClock.uptimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.termux.app.TermuxActivity;
|
||||
|
||||
/**
|
||||
* Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible.
|
||||
* This class is derived from:
|
||||
* https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible
|
||||
* and has some additional tweaks
|
||||
* ---
|
||||
* For more information, see https://issuetracker.google.com/issues/36911528
|
||||
*/
|
||||
public class FullScreenWorkAround {
|
||||
private final View mChildOfContent;
|
||||
private int mUsableHeightPrevious;
|
||||
private final ViewGroup.LayoutParams mViewGroupLayoutParams;
|
||||
|
||||
private final int mNavBarHeight;
|
||||
|
||||
|
||||
public static void apply(TermuxActivity activity) {
|
||||
new FullScreenWorkAround(activity);
|
||||
}
|
||||
|
||||
private FullScreenWorkAround(TermuxActivity activity) {
|
||||
ViewGroup content = activity.findViewById(android.R.id.content);
|
||||
mChildOfContent = content.getChildAt(0);
|
||||
mViewGroupLayoutParams = mChildOfContent.getLayoutParams();
|
||||
mNavBarHeight = activity.getNavBarHeight();
|
||||
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
|
||||
}
|
||||
|
||||
private void possiblyResizeChildOfContent() {
|
||||
int usableHeightNow = computeUsableHeight();
|
||||
if (usableHeightNow != mUsableHeightPrevious) {
|
||||
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
|
||||
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
|
||||
if (heightDifference > (usableHeightSansKeyboard / 4)) {
|
||||
// keyboard probably just became visible
|
||||
|
||||
// ensures that usable layout space does not extend behind the
|
||||
// soft keyboard, causing the extra keys to not be visible
|
||||
mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight();
|
||||
} else {
|
||||
// keyboard probably just became hidden
|
||||
mViewGroupLayoutParams.height = usableHeightSansKeyboard;
|
||||
}
|
||||
mChildOfContent.requestLayout();
|
||||
mUsableHeightPrevious = usableHeightNow;
|
||||
}
|
||||
}
|
||||
|
||||
private int getNavBarHeight() {
|
||||
return mNavBarHeight;
|
||||
}
|
||||
|
||||
private int computeUsableHeight() {
|
||||
Rect r = new Rect();
|
||||
mChildOfContent.getWindowVisibleDisplayFrame(r);
|
||||
return (r.bottom - r.top);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
public class KeyboardShortcut {
|
||||
|
||||
public final int codePoint;
|
||||
public final int shortcutAction;
|
||||
|
||||
public KeyboardShortcut(int codePoint, int shortcutAction) {
|
||||
this.codePoint = codePoint;
|
||||
this.shortcutAction = shortcutAction;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
public class TerminalToolbarViewPager {
|
||||
|
||||
public static class PageAdapter extends PagerAdapter {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
String mSavedTextInput;
|
||||
|
||||
public PageAdapter(TermuxActivity activity, String savedTextInput) {
|
||||
this.mActivity = activity;
|
||||
this.mSavedTextInput = savedTextInput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
|
||||
LayoutInflater inflater = LayoutInflater.from(mActivity);
|
||||
View layout;
|
||||
if (position == 0) {
|
||||
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
||||
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
|
||||
mActivity.setExtraKeysView(extraKeysView);
|
||||
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||
|
||||
// apply extra keys fix if enabled in prefs
|
||||
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
||||
FullScreenWorkAround.apply(mActivity);
|
||||
}
|
||||
|
||||
} else {
|
||||
layout = inflater.inflate(R.layout.view_terminal_toolbar_text_input, collection, false);
|
||||
final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input);
|
||||
|
||||
if (mSavedTextInput != null) {
|
||||
editText.setText(mSavedTextInput);
|
||||
mSavedTextInput = null;
|
||||
}
|
||||
|
||||
editText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null) {
|
||||
if (session.isRunning()) {
|
||||
String textToSend = editText.getText().toString();
|
||||
if (textToSend.length() == 0) textToSend = "\r";
|
||||
session.write(textToSend);
|
||||
} else {
|
||||
mActivity.getTermuxTerminalSessionClient().removeFinishedSession(session);
|
||||
}
|
||||
editText.setText("");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
collection.addView(layout);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
|
||||
collection.removeView((View) view);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class OnPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
final ViewPager mTerminalToolbarViewPager;
|
||||
|
||||
public OnPageChangeListener(TermuxActivity activity, ViewPager viewPager) {
|
||||
this.mActivity = activity;
|
||||
this.mTerminalToolbarViewPager = viewPager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (position == 0) {
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
} else {
|
||||
final EditText editText = mTerminalToolbarViewPager.findViewById(R.id.terminal_toolbar_text_input);
|
||||
if (editText != null) editText.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExtraKeyButton {
|
||||
|
||||
/**
|
||||
* The key that will be sent to the terminal, either a control character
|
||||
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||
* some text.
|
||||
*/
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||
*/
|
||||
private final boolean macro;
|
||||
|
||||
/**
|
||||
* The text that will be shown on the button.
|
||||
*/
|
||||
private final String display;
|
||||
|
||||
/**
|
||||
* The information of the popup (triggered by swipe up).
|
||||
*/
|
||||
@Nullable
|
||||
private ExtraKeyButton popup;
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||
this(charDisplayMap, config, null);
|
||||
}
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
|
||||
String keyFromConfig = config.optString("key", null);
|
||||
String macroFromConfig = config.optString("macro", null);
|
||||
String[] keys;
|
||||
if (keyFromConfig != null && macroFromConfig != null) {
|
||||
throw new JSONException("Both key and macro can't be set for the same key");
|
||||
} else if (keyFromConfig != null) {
|
||||
keys = new String[]{keyFromConfig};
|
||||
this.macro = false;
|
||||
} else if (macroFromConfig != null) {
|
||||
keys = macroFromConfig.split(" ");
|
||||
this.macro = true;
|
||||
} else {
|
||||
throw new JSONException("All keys have to specify either key or macro");
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
|
||||
}
|
||||
|
||||
this.key = TextUtils.join(" ", keys);
|
||||
|
||||
String displayFromConfig = config.optString("display", null);
|
||||
if (displayFromConfig != null) {
|
||||
this.display = displayFromConfig;
|
||||
} else {
|
||||
this.display = Arrays.stream(keys)
|
||||
.map(key -> charDisplayMap.get(key, key))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public boolean isMacro() {
|
||||
return macro;
|
||||
}
|
||||
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ExtraKeyButton getPopup() {
|
||||
return popup;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ExtraKeysInfo {
|
||||
|
||||
/**
|
||||
* Matrix of buttons displayed
|
||||
*/
|
||||
private final ExtraKeyButton[][] buttons;
|
||||
|
||||
/**
|
||||
* This corresponds to one of the CharMapDisplay below
|
||||
*/
|
||||
private String style;
|
||||
|
||||
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
|
||||
this.style = style;
|
||||
|
||||
// Convert String propertiesInfo to Array of Arrays
|
||||
JSONArray arr = new JSONArray(propertiesInfo);
|
||||
Object[][] matrix = new Object[arr.length()][];
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONArray line = arr.getJSONArray(i);
|
||||
matrix[i] = new Object[line.length()];
|
||||
for (int j = 0; j < line.length(); j++) {
|
||||
matrix[i][j] = line.get(j);
|
||||
}
|
||||
}
|
||||
|
||||
// convert matrix to buttons
|
||||
this.buttons = new ExtraKeyButton[matrix.length][];
|
||||
for (int i = 0; i < matrix.length; i++) {
|
||||
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
|
||||
for (int j = 0; j < matrix[i].length; j++) {
|
||||
Object key = matrix[i][j];
|
||||
|
||||
JSONObject jobject = normalizeKeyConfig(key);
|
||||
|
||||
ExtraKeyButton button;
|
||||
|
||||
if (! jobject.has("popup")) {
|
||||
// no popup
|
||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||
} else {
|
||||
// a popup
|
||||
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
|
||||
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
|
||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
|
||||
}
|
||||
|
||||
this.buttons[i][j] = button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "hello" -> {"key": "hello"}
|
||||
*/
|
||||
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||
JSONObject jobject;
|
||||
if (key instanceof String) {
|
||||
jobject = new JSONObject();
|
||||
jobject.put("key", key);
|
||||
} else if (key instanceof JSONObject) {
|
||||
jobject = (JSONObject) key;
|
||||
} else {
|
||||
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||
}
|
||||
return jobject;
|
||||
}
|
||||
|
||||
public ExtraKeyButton[][] getMatrix() {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* HashMap that implements Python dict.get(key, default) function.
|
||||
* Default java.util .get(key) is then the same as .get(key, null);
|
||||
*/
|
||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||
V get(K key, V defaultValue) {
|
||||
if (containsKey(key))
|
||||
return get(key);
|
||||
else
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
static class CharDisplayMap extends CleverMap<String, String> {}
|
||||
|
||||
/**
|
||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
||||
*/
|
||||
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
|
||||
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||
}};
|
||||
|
||||
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
|
||||
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
||||
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
||||
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
||||
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
||||
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
|
||||
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||
}};
|
||||
|
||||
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
|
||||
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
||||
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
||||
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
||||
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
||||
}};
|
||||
|
||||
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
|
||||
// alternative to classic arrow keys
|
||||
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||
}};
|
||||
|
||||
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
|
||||
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
||||
// put("FN", "FN"); // no ISO character exists
|
||||
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
||||
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
||||
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
||||
}};
|
||||
|
||||
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
|
||||
// nicer looking for most cases
|
||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||
}};
|
||||
|
||||
/*
|
||||
* Multiple maps are available to quickly change
|
||||
* the style of the keys.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some classic symbols everybody knows
|
||||
*/
|
||||
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(nicerLookingDisplay);
|
||||
// all other characters are displayed as themselves
|
||||
}};
|
||||
|
||||
/**
|
||||
* Classic symbols and less known symbols
|
||||
*/
|
||||
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(lessKnownCharactersDisplay); // NEW
|
||||
putAll(nicerLookingDisplay);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Only arrows
|
||||
*/
|
||||
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||
putAll(nicerLookingDisplay);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Full Iso
|
||||
*/
|
||||
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
|
||||
putAll(classicArrowsDisplay);
|
||||
putAll(wellKnownCharactersDisplay);
|
||||
putAll(lessKnownCharactersDisplay); // NEW
|
||||
putAll(nicerLookingDisplay);
|
||||
putAll(notKnownIsoCharacters); // NEW
|
||||
}};
|
||||
|
||||
/**
|
||||
* Some people might call our keys differently
|
||||
*/
|
||||
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
|
||||
put("ESCAPE", "ESC");
|
||||
put("CONTROL", "CTRL");
|
||||
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
||||
put("FUNCTION", "FN");
|
||||
// no alias for ALT
|
||||
|
||||
// Directions are sometimes written as first and last letter for brevety
|
||||
put("LT", "LEFT");
|
||||
put("RT", "RIGHT");
|
||||
put("DN", "DOWN");
|
||||
// put("UP", "UP"); well, "UP" is already two letters
|
||||
|
||||
put("PAGEUP", "PGUP");
|
||||
put("PAGE_UP", "PGUP");
|
||||
put("PAGE UP", "PGUP");
|
||||
put("PAGE-UP", "PGUP");
|
||||
|
||||
// no alias for HOME
|
||||
// no alias for END
|
||||
|
||||
put("PAGEDOWN", "PGDN");
|
||||
put("PAGE_DOWN", "PGDN");
|
||||
put("PAGE-DOWN", "PGDN");
|
||||
|
||||
put("DELETE", "DEL");
|
||||
put("BACKSPACE", "BKSP");
|
||||
|
||||
// easier for writing in termux.properties
|
||||
put("BACKSLASH", "\\");
|
||||
put("QUOTE", "\"");
|
||||
put("APOSTROPHE", "'");
|
||||
}};
|
||||
|
||||
CharDisplayMap getSelectedCharMap() {
|
||||
switch (style) {
|
||||
case "arrows-only":
|
||||
return arrowsOnlyCharDisplay;
|
||||
case "arrows-all":
|
||||
return lotsOfArrowsCharDisplay;
|
||||
case "all":
|
||||
return fullIsoCharDisplay;
|
||||
case "none":
|
||||
return new CharDisplayMap();
|
||||
default:
|
||||
if (!TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(style))
|
||||
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + style + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
|
||||
return defaultCharDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
|
||||
* Modifies the array, doesn't return a new one.
|
||||
*/
|
||||
public static String replaceAlias(String key) {
|
||||
return controlCharsAliases.get(key, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import android.view.Gravity;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
/**
|
||||
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||
* keyboard.
|
||||
*/
|
||||
public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int BUTTON_COLOR = 0x00000000;
|
||||
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
||||
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
|
||||
|
||||
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||
|
||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
|
||||
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||
put("END", KeyEvent.KEYCODE_MOVE_END);
|
||||
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
||||
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
||||
put("INS", KeyEvent.KEYCODE_INSERT);
|
||||
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
||||
put("BKSP", KeyEvent.KEYCODE_DEL);
|
||||
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
||||
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||
put("F1", KeyEvent.KEYCODE_F1);
|
||||
put("F2", KeyEvent.KEYCODE_F2);
|
||||
put("F3", KeyEvent.KEYCODE_F3);
|
||||
put("F4", KeyEvent.KEYCODE_F4);
|
||||
put("F5", KeyEvent.KEYCODE_F5);
|
||||
put("F6", KeyEvent.KEYCODE_F6);
|
||||
put("F7", KeyEvent.KEYCODE_F7);
|
||||
put("F8", KeyEvent.KEYCODE_F8);
|
||||
put("F9", KeyEvent.KEYCODE_F9);
|
||||
put("F10", KeyEvent.KEYCODE_F10);
|
||||
put("F11", KeyEvent.KEYCODE_F11);
|
||||
put("F12", KeyEvent.KEYCODE_F12);
|
||||
}};
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||
if ("KEYBOARD".equals(keyName)) {
|
||||
if(mTermuxTerminalViewClient != null)
|
||||
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
|
||||
} else if ("DRAWER".equals(keyName)) {
|
||||
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||
drawer.openDrawer(Gravity.LEFT);
|
||||
} else if (keyCodesForString.containsKey(keyName)) {
|
||||
Integer keyCode = keyCodesForString.get(keyName);
|
||||
if (keyCode == null) return;
|
||||
int metaState = 0;
|
||||
if (forceCtrlDown) {
|
||||
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||
}
|
||||
if (forceLeftAltDown) {
|
||||
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||
}
|
||||
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
|
||||
terminalView.onKeyDown(keyCode, keyEvent);
|
||||
} else {
|
||||
// not a control char
|
||||
keyName.codePoints().forEach(codePoint -> {
|
||||
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sendKey(View view, ExtraKeyButton buttonInfo) {
|
||||
if (buttonInfo.isMacro()) {
|
||||
String[] keys = buttonInfo.getKey().split(" ");
|
||||
boolean ctrlDown = false;
|
||||
boolean altDown = false;
|
||||
for (String key : keys) {
|
||||
if ("CTRL".equals(key)) {
|
||||
ctrlDown = true;
|
||||
} else if ("ALT".equals(key)) {
|
||||
altDown = true;
|
||||
} else {
|
||||
sendKey(view, key, ctrlDown, altDown);
|
||||
ctrlDown = false;
|
||||
altDown = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendKey(view, buttonInfo.getKey(), false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public enum SpecialButton {
|
||||
CTRL, ALT, FN
|
||||
}
|
||||
|
||||
private static class SpecialButtonState {
|
||||
boolean isOn = false;
|
||||
boolean isActive = false;
|
||||
List<Button> buttons = new ArrayList<>();
|
||||
|
||||
void setIsActive(boolean value) {
|
||||
isActive = value;
|
||||
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||
put(SpecialButton.CTRL, new SpecialButtonState());
|
||||
put(SpecialButton.ALT, new SpecialButtonState());
|
||||
put(SpecialButton.FN, new SpecialButtonState());
|
||||
}};
|
||||
|
||||
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
|
||||
|
||||
private boolean isSpecialButton(ExtraKeyButton button) {
|
||||
return specialButtonsKeys.contains(button.getKey());
|
||||
}
|
||||
|
||||
private ScheduledExecutorService scheduledExecutor;
|
||||
private PopupWindow popupWindow;
|
||||
private int longPressCount;
|
||||
|
||||
public boolean readSpecialButton(SpecialButton name) {
|
||||
SpecialButtonState state = specialButtons.get(name);
|
||||
if (state == null)
|
||||
throw new RuntimeException("Must be a valid special button (see source)");
|
||||
|
||||
if (!state.isOn || !state.isActive)
|
||||
return false;
|
||||
|
||||
state.setIsActive(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||
if (state == null) return null;
|
||||
state.isOn = true;
|
||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
|
||||
if (needUpdate) {
|
||||
state.buttons.add(button);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
void popup(View view, ExtraKeyButton extraButton) {
|
||||
int width = view.getMeasuredWidth();
|
||||
int height = view.getMeasuredHeight();
|
||||
Button button;
|
||||
if (isSpecialButton(extraButton)) {
|
||||
button = createSpecialButton(extraButton.getKey(), false);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
}
|
||||
button.setText(extraButton.getDisplay());
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
button.setMinHeight(0);
|
||||
button.setMinWidth(0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinimumHeight(0);
|
||||
button.setWidth(width);
|
||||
button.setHeight(height);
|
||||
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
popupWindow = new PopupWindow(this);
|
||||
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
||||
popupWindow.setContentView(button);
|
||||
popupWindow.setOutsideTouchable(true);
|
||||
popupWindow.setFocusable(false);
|
||||
popupWindow.showAsDropDown(view, 0, -2 * height);
|
||||
}
|
||||
|
||||
/**
|
||||
* General util function to compute the longest column length in a matrix.
|
||||
*/
|
||||
static int maximumLength(Object[][] matrix) {
|
||||
int m = 0;
|
||||
for (Object[] row : matrix)
|
||||
m = Math.max(m, row.length);
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the view given parameters in termux.properties
|
||||
*
|
||||
* @param infos matrix as defined in termux.properties extrakeys
|
||||
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
|
||||
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
|
||||
* Any string of length > 1 in total Uppercase will print a warning
|
||||
*
|
||||
* Examples:
|
||||
* "ENTER" will trigger the ENTER keycode
|
||||
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
|
||||
* "→" will input a "→" character
|
||||
* "−" will input a "−" character
|
||||
* "-_-" will input the string "-_-"
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void reload(ExtraKeysInfo infos) {
|
||||
if (infos == null)
|
||||
return;
|
||||
|
||||
for(SpecialButtonState state : specialButtons.values())
|
||||
state.buttons = new ArrayList<>();
|
||||
|
||||
removeAllViews();
|
||||
|
||||
ExtraKeyButton[][] buttons = infos.getMatrix();
|
||||
|
||||
setRowCount(buttons.length);
|
||||
setColumnCount(maximumLength(buttons));
|
||||
|
||||
for (int row = 0; row < buttons.length; row++) {
|
||||
for (int col = 0; col < buttons[row].length; col++) {
|
||||
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||
|
||||
Button button;
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
}
|
||||
|
||||
button.setText(buttonInfo.getDisplay());
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
|
||||
final Button finalButton = button;
|
||||
button.setOnClickListener(v -> {
|
||||
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else {
|
||||
// Perform haptic feedback only if no total silence mode enabled.
|
||||
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
|
||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
View root = getRootView();
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||
if (state == null) return;
|
||||
state.setIsActive(!state.isActive);
|
||||
} else {
|
||||
sendKey(root, buttonInfo);
|
||||
}
|
||||
});
|
||||
|
||||
button.setOnTouchListener((v, event) -> {
|
||||
final View root = getRootView();
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
longPressCount = 0;
|
||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
|
||||
// autorepeat
|
||||
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
scheduledExecutor.scheduleWithFixedDelay(() -> {
|
||||
longPressCount++;
|
||||
sendKey(root, buttonInfo);
|
||||
}, 400, 80, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
if (popupWindow == null && event.getY() < 0) {
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
popup(v, buttonInfo.getPopup());
|
||||
}
|
||||
if (popupWindow != null && event.getY() > 0) {
|
||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||
popupWindow.dismiss();
|
||||
popupWindow = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
v.setBackgroundColor(BUTTON_COLOR);
|
||||
if (scheduledExecutor != null) {
|
||||
scheduledExecutor.shutdownNow();
|
||||
scheduledExecutor = null;
|
||||
}
|
||||
if (longPressCount == 0 || popupWindow != null) {
|
||||
if (popupWindow != null) {
|
||||
popupWindow.setContentView(null);
|
||||
popupWindow.dismiss();
|
||||
popupWindow = null;
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
if (isSpecialButton(buttonInfo.getPopup())) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
|
||||
if (state == null) return true;
|
||||
state.setIsActive(!state.isActive);
|
||||
} else {
|
||||
sendKey(root, buttonInfo.getPopup());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
v.performClick();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
LayoutParams param = new GridLayout.LayoutParams();
|
||||
param.width = 0;
|
||||
param.height = 0;
|
||||
param.setMargins(0, 0, 0, 0);
|
||||
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
|
||||
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
||||
button.setLayoutParams(param);
|
||||
|
||||
addView(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
|
||||
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||
}
|
||||
|
||||
}
|
||||
188
app/src/main/java/com/termux/app/utils/CrashUtils.java
Normal file
@@ -0,0 +1,188 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public class CrashUtils {
|
||||
|
||||
private static final String LOG_TAG = "CrashUtils";
|
||||
|
||||
/**
|
||||
* Notify the user of an app crash at last run by reading the crash info from the crash log file
|
||||
* at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||
*
|
||||
* If the crash log file exists and is not empty and
|
||||
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
|
||||
* enabled, then a notification will be shown for the crash on the
|
||||
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
|
||||
*
|
||||
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTagParam The log tag to use for logging.
|
||||
*/
|
||||
public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
|
||||
if (context == null) return;
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for crashes
|
||||
if (!preferences.areCrashReportNotificationsEnabled())
|
||||
return;
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
|
||||
|
||||
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
||||
return;
|
||||
|
||||
Error error;
|
||||
StringBuilder reportStringBuilder = new StringBuilder();
|
||||
|
||||
// Read report string from crash log file
|
||||
error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(logTag, error.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Move crash log file to backup location if it exists
|
||||
error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(logTag, error.toString());
|
||||
}
|
||||
|
||||
String reportString = reportStringBuilder.toString();
|
||||
|
||||
if (reportString.isEmpty())
|
||||
return;
|
||||
|
||||
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
|
||||
|
||||
sendCrashReportNotification(context, logTag, reportString, false);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param reportString The text for the crash report.
|
||||
* @param forceNotification If set to {@code true}, then a notification will be shown
|
||||
* regardless of if pending intent is {@code null} or
|
||||
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
*/
|
||||
public static void sendCrashReportNotification(final Context context, String logTag, String reportString, boolean forceNotification) {
|
||||
if (context == null) return;
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for crashes
|
||||
if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification)
|
||||
return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||
// to show the details of the crash
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||
|
||||
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
|
||||
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupCrashReportsNotificationChannel(context);
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param title The title for the notification.
|
||||
* @param notificationText The second line text of the notification.
|
||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||
* @return Returns the {@link Notification.Builder}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||
|
||||
if (builder == null) return null;
|
||||
|
||||
// Enable timestamp
|
||||
builder.setShowWhen(true);
|
||||
|
||||
// Set notification icon
|
||||
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||
|
||||
// Set background color for small notification icon
|
||||
builder.setColor(0xFF607D8B);
|
||||
|
||||
// Dismiss on click
|
||||
builder.setAutoCancel(true);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the notification channel for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} and
|
||||
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
public static void setupCrashReportsNotificationChannel(final Context context) {
|
||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
}
|
||||
|
||||
}
|
||||
307
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
@@ -0,0 +1,307 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.models.ResultConfig;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||
import com.termux.shared.shell.ResultSender;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||
import com.termux.shared.settings.properties.SharedProperties;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class PluginUtils {
|
||||
|
||||
private static final String LOG_TAG = "PluginUtils";
|
||||
|
||||
/**
|
||||
* Process {@link ExecutionCommand} result.
|
||||
*
|
||||
* The ExecutionCommand currentState must be greater or equal to
|
||||
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||
* is not {@code null}, then the result of commands are sent back to the command caller.
|
||||
*
|
||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param executionCommand The {@link ExecutionCommand} to process.
|
||||
*/
|
||||
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
Error error = null;
|
||||
ResultData resultData = executionCommand.resultData;
|
||||
|
||||
if (!executionCommand.hasExecuted()) {
|
||||
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||
|
||||
// Log the output. ResultData should not be logged if pending result since ResultSender will do it
|
||||
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||
|
||||
// If execution command was started by a plugin which expects the result back
|
||||
if (isPluginExecutionCommandWithPendingResult) {
|
||||
// Set variables which will be used by sendCommandResultData to send back the result
|
||||
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||
setPluginResultPendingIntentVariables(executionCommand);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||
setPluginResultDirectoryVariables(executionCommand);
|
||||
|
||||
// Send result to caller
|
||||
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||
if (error != null) {
|
||||
// error will be added to existing Errors
|
||||
resultData.setStateFailed(error);
|
||||
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||
|
||||
// Flash and send notification for the error
|
||||
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!executionCommand.isStateFailed() && error == null)
|
||||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {@link ExecutionCommand} error.
|
||||
*
|
||||
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
||||
* The {@link ResultData#getErrCode()} must have been set to a value greater than
|
||||
* {@link Errno#ERRNO_SUCCESS}.
|
||||
* The {@link ResultData#errorsList} must also be set with appropriate error info.
|
||||
*
|
||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||
* is not {@code null}, then the errors of commands are sent back to the command caller.
|
||||
*
|
||||
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||
* enabled, then a flash and a notification will be shown for the error as well
|
||||
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
|
||||
* the error.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
||||
* regardless of if pending intent is {@code null} or
|
||||
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
*/
|
||||
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
||||
if (context == null || executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
Error error;
|
||||
ResultData resultData = executionCommand.resultData;
|
||||
|
||||
if (!executionCommand.isStateFailed()) {
|
||||
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||
|
||||
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
|
||||
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||
|
||||
// If execution command was started by a plugin which expects the result back
|
||||
if (isPluginExecutionCommandWithPendingResult) {
|
||||
// Set variables which will be used by sendCommandResultData to send back the result
|
||||
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||
setPluginResultPendingIntentVariables(executionCommand);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||
setPluginResultDirectoryVariables(executionCommand);
|
||||
|
||||
// Send result to caller
|
||||
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||
if (error != null) {
|
||||
// error will be added to existing Errors
|
||||
resultData.setStateFailed(error);
|
||||
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||
forceNotification = true;
|
||||
}
|
||||
|
||||
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
||||
if (!forceNotification) return;
|
||||
}
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for plugin commands, then just return
|
||||
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
||||
return;
|
||||
|
||||
// Flash and send notification for the error
|
||||
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||
|
||||
}
|
||||
|
||||
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||
* to send back the result via {@link ResultConfig#resultPendingIntent}. */
|
||||
public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) {
|
||||
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||
|
||||
resultConfig.resultBundleKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE;
|
||||
resultConfig.resultStdoutKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT;
|
||||
resultConfig.resultStdoutOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH;
|
||||
resultConfig.resultStderrKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR;
|
||||
resultConfig.resultStderrOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH;
|
||||
resultConfig.resultExitCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE;
|
||||
resultConfig.resultErrCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR;
|
||||
resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG;
|
||||
}
|
||||
|
||||
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||
* to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */
|
||||
public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) {
|
||||
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||
|
||||
resultConfig.resultDirectoryPath = TermuxFileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null, true);
|
||||
resultConfig.resultDirectoryAllowedParentPath = TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(resultConfig.resultDirectoryPath);
|
||||
|
||||
// Set default resultFileBasename if resultSingleFile is true to `<executable_basename>-<timestamp>.log`
|
||||
if (resultConfig.resultSingleFile && resultConfig.resultFileBasename == null)
|
||||
resultConfig.resultFileBasename = ShellUtils.getExecutableBasename(executionCommand.executable) + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp() + ".log";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||
* @param notificationTextString The text of the notification.
|
||||
*/
|
||||
public static void sendPluginCommandErrorNotification(Context context, String logTag, ExecutionCommand executionCommand, String notificationTextString) {
|
||||
// Send a notification to show the error which when clicked will open the ReportActivity
|
||||
// to show the details of the error
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND.getName(), logTag, title, null, reportString.toString(), null,true));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupPluginCommandErrorsNotificationChannel(context);
|
||||
|
||||
// Use markdown in notification
|
||||
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
|
||||
//CharSequence notificationTextCharSequence = notificationTextString;
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param title The title for the notification.
|
||||
* @param notificationText The second line text of the notification.
|
||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||
* @return Returns the {@link Notification.Builder}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||
|
||||
if (builder == null) return null;
|
||||
|
||||
// Enable timestamp
|
||||
builder.setShowWhen(true);
|
||||
|
||||
// Set notification icon
|
||||
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||
|
||||
// Set background color for small notification icon
|
||||
builder.setColor(0xFF607D8B);
|
||||
|
||||
// Dismiss on click
|
||||
builder.setAutoCancel(true);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the notification channel for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
|
||||
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||
*
|
||||
* @param context The {@link Context} to get error string.
|
||||
* @return Returns the {@code error} if policy is violated, otherwise {@code null}.
|
||||
*/
|
||||
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||
String errmsg = null;
|
||||
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
|
||||
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
|
||||
}
|
||||
|
||||
return errmsg;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import android.provider.DocumentsProvider;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -22,7 +22,7 @@ import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* A document provider for the Storage Access Framework which exposes the files in the
|
||||
* $HOME/ folder to other apps.
|
||||
* $HOME/ directory to other apps.
|
||||
* <p/>
|
||||
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
||||
* <p/>
|
||||
@@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
|
||||
private static final String ALL_MIME_TYPES = "*/*";
|
||||
|
||||
private static final File BASE_DIR = new File(TermuxService.HOME_PATH);
|
||||
private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR;
|
||||
|
||||
|
||||
// The default columns to return information about a root if no specific
|
||||
@@ -63,19 +63,19 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
};
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
public Cursor queryRoots(String[] projection) {
|
||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
||||
@SuppressWarnings("ConstantConditions") final String applicationName = getContext().getString(R.string.application_name);
|
||||
final String applicationName = getContext().getString(R.string.application_name);
|
||||
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
||||
row.add(Root.COLUMN_SUMMARY, null);
|
||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
|
||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
|
||||
row.add(Root.COLUMN_TITLE, applicationName);
|
||||
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
||||
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
||||
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
|
||||
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -91,9 +91,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||
final File parent = getFileForDocId(parentDocumentId);
|
||||
for (File file : parent.listFiles()) {
|
||||
if (!file.getName().startsWith(".")) {
|
||||
includeFile(result, null, file);
|
||||
}
|
||||
includeFile(result, null, file);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -117,6 +115,29 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
||||
File newFile = new File(parentDocumentId, displayName);
|
||||
int noConflictId = 2;
|
||||
while (newFile.exists()) {
|
||||
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
|
||||
}
|
||||
try {
|
||||
boolean succeeded;
|
||||
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
succeeded = newFile.mkdir();
|
||||
} else {
|
||||
succeeded = newFile.createNewFile();
|
||||
}
|
||||
if (!succeeded) {
|
||||
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||
}
|
||||
return newFile.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||
File file = getFileForDocId(documentId);
|
||||
@@ -146,16 +167,15 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
final int MAX_SEARCH_RESULTS = 50;
|
||||
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
|
||||
final File file = pending.removeFirst();
|
||||
// Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
|
||||
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search
|
||||
// through the whole SD card).
|
||||
boolean isInsideHome;
|
||||
try {
|
||||
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
|
||||
isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
} catch (IOException e) {
|
||||
isInsideHome = true;
|
||||
}
|
||||
final boolean isHidden = file.getName().startsWith(".");
|
||||
if (isInsideHome && !isHidden) {
|
||||
if (isInsideHome) {
|
||||
if (file.isDirectory()) {
|
||||
Collections.addAll(pending, file.listFiles());
|
||||
} else {
|
||||
@@ -169,6 +189,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||
return documentId.startsWith(parentDocumentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the document id given a file. This document id must be consistent across time as other
|
||||
* applications may save the ID and use it to reference documents later.
|
||||
@@ -195,7 +220,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
final String name = file.getName();
|
||||
final int lastDot = name.lastIndexOf('.');
|
||||
if (lastDot >= 0) {
|
||||
final String extension = name.substring(lastDot + 1);
|
||||
final String extension = name.substring(lastDot + 1).toLowerCase();
|
||||
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mime != null) return mime;
|
||||
}
|
||||
@@ -220,10 +245,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
|
||||
int flags = 0;
|
||||
if (file.isDirectory()) {
|
||||
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
} else if (file.canWrite()) {
|
||||
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
|
||||
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||
}
|
||||
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
|
||||
|
||||
final String displayName = file.getName();
|
||||
final String mimeType = getMimeType(file);
|
||||
@@ -236,7 +262,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||
row.add(Document.COLUMN_FLAGS, flags);
|
||||
row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
|
||||
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ package com.termux.filepicker;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.util.Patterns;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.DialogUtils;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
@@ -22,12 +23,13 @@ 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 = TermuxService.FILES_PATH + "/home/downloads";
|
||||
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
|
||||
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
|
||||
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
|
||||
@@ -37,6 +39,13 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
*/
|
||||
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();
|
||||
@@ -51,7 +60,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
|
||||
if (sharedText != null) {
|
||||
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
|
||||
if (isSharedTextAnUrl(sharedText)) {
|
||||
handleUrlAndFinish(sharedText);
|
||||
} else {
|
||||
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
|
||||
@@ -83,17 +92,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
|
||||
void showErrorDialogAndQuit(String message) {
|
||||
mFinishOnDismissNameDialog = false;
|
||||
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
}).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
finish();
|
||||
}
|
||||
}).show();
|
||||
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) {
|
||||
@@ -114,59 +113,45 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
promptNameAndSave(in, attachmentFileName);
|
||||
} catch (Exception e) {
|
||||
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
||||
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
File outFile = saveStreamWithName(in, text);
|
||||
if (outFile == null) return;
|
||||
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;
|
||||
}
|
||||
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);
|
||||
// Do this for the user if necessary:
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
editorProgramFile.setExecutable(true);
|
||||
|
||||
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
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();
|
||||
},
|
||||
R.string.file_received_open_folder_button, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(String text) {
|
||||
if (saveStreamWithName(in, text) == null) return;
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
},
|
||||
android.R.string.cancel, new DialogUtils.TextSetListener() {
|
||||
@Override
|
||||
public void onTextSet(final String text) {
|
||||
finish();
|
||||
}
|
||||
}, new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
if (mFinishOnDismissNameDialog) finish();
|
||||
}
|
||||
android.R.string.cancel, text -> finish(), dialog -> {
|
||||
if (mFinishOnDismissNameDialog) finish();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,7 +173,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
return outFile;
|
||||
} catch (IOException e) {
|
||||
showErrorDialogAndQuit("Error saving file:\n\n" + e);
|
||||
Log.e("termux", "Error saving file", e);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -207,9 +192,9 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
|
||||
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public final class EmulatorDebug {
|
||||
|
||||
/** The tag to use with {@link Log}. */
|
||||
public static final String LOG_TAG = "termux";
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* Encodes effects, foreground and background colors into a 32 bit integer, which are stored for each cell in a terminal
|
||||
* row in {@link TerminalRow#mStyle}.
|
||||
* <p/>
|
||||
* The foreground and background colors take 9 bits each, leaving (32-9-9)=14 bits for effect flags. Using 9 for now
|
||||
* (the different CHARACTER_ATTRIBUTE_* bits).
|
||||
*/
|
||||
public final class TextStyle {
|
||||
|
||||
public final static int CHARACTER_ATTRIBUTE_BOLD = 1;
|
||||
public final static int CHARACTER_ATTRIBUTE_ITALIC = 1 << 1;
|
||||
public final static int CHARACTER_ATTRIBUTE_UNDERLINE = 1 << 2;
|
||||
public final static int CHARACTER_ATTRIBUTE_BLINK = 1 << 3;
|
||||
public final static int CHARACTER_ATTRIBUTE_INVERSE = 1 << 4;
|
||||
public final static int CHARACTER_ATTRIBUTE_INVISIBLE = 1 << 5;
|
||||
public final static int CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 << 6;
|
||||
/**
|
||||
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
|
||||
* <p/>
|
||||
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
|
||||
* come after it as erasable from the screen.
|
||||
*/
|
||||
public final static int CHARACTER_ATTRIBUTE_PROTECTED = 1 << 7;
|
||||
/** Dim colors. Also known as faint or half intensity. */
|
||||
public final static int CHARACTER_ATTRIBUTE_DIM = 1 << 8;
|
||||
|
||||
public final static int COLOR_INDEX_FOREGROUND = 256;
|
||||
public final static int COLOR_INDEX_BACKGROUND = 257;
|
||||
public final static int COLOR_INDEX_CURSOR = 258;
|
||||
|
||||
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
|
||||
public final static int NUM_INDEXED_COLORS = 259;
|
||||
|
||||
/** Normal foreground and background colors and no effects. */
|
||||
final static int NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0);
|
||||
|
||||
static int encode(int foreColor, int backColor, int effect) {
|
||||
return ((effect & 0b111111111) << 18) | ((foreColor & 0b111111111) << 9) | (backColor & 0b111111111);
|
||||
}
|
||||
|
||||
public static int decodeForeColor(int encodedColor) {
|
||||
return (encodedColor >> 9) & 0b111111111;
|
||||
}
|
||||
|
||||
public static int decodeBackColor(int encodedColor) {
|
||||
return encodedColor & 0b111111111;
|
||||
}
|
||||
|
||||
public static int decodeEffect(int encodedColor) {
|
||||
return (encodedColor >> 18) & 0b111111111;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* wcwidth() implementation from http://git.musl-libc.org/cgit/musl/tree/src/ctype
|
||||
* <p/>
|
||||
* Modified to return 0 instead of -1.
|
||||
*/
|
||||
public final class WcWidth {
|
||||
|
||||
private static final short table[] = {16, 16, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 16, 16, 32, 16, 16, 16, 33, 34, 35, 36, 37, 38,
|
||||
39, 16, 16, 40, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 41, 42, 16, 16, 43, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 44, 16, 45, 46, 47, 48, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
49, 16, 16, 50, 51, 16, 52, 16, 16, 16, 16, 16, 16, 16, 16, 53, 16, 16, 16, 16, 16, 54, 55, 16, 16, 16, 16, 56, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 57, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 58, 59, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
248, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 191, 182, 0, 0, 0,
|
||||
0, 0, 0, 0, 31, 0, 255, 7, 0, 0, 0, 0, 0, 248, 255, 255, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 191, 159, 61, 0, 0, 0, 128, 2, 0, 0, 0,
|
||||
255, 255, 255, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 255, 1, 0, 0, 0, 0, 0, 0, 248, 15, 0, 0, 0, 192, 251, 239, 62, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 255, 255, 127, 7, 0, 0, 0, 0, 0, 0, 20, 254, 33, 254, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 16, 30, 32, 0,
|
||||
0, 12, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 16, 134, 57, 2, 0, 0, 0, 35, 0, 6, 0, 0, 0, 0, 0, 0, 16, 190, 33, 0, 0, 12, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 144,
|
||||
30, 32, 64, 0, 12, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 193, 61, 96, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 144, 64, 48, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 32, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 92, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 242, 7, 128, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 242, 27, 0, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 160, 2, 0, 0, 0, 0, 0, 0, 254,
|
||||
127, 223, 224, 255, 254, 255, 255, 255, 31, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 253, 102, 0, 0, 0, 195, 1, 0, 30, 0, 100, 32, 0, 32, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0,
|
||||
28, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 176, 63, 64, 254, 15, 32, 0, 0, 0, 0, 0, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 1, 4, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
128, 1, 0, 0, 0, 0, 0, 0, 64, 127, 229, 31, 248, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 208, 23, 4, 0, 0, 0, 0,
|
||||
248, 15, 0, 3, 0, 0, 0, 60, 11, 0, 0, 0, 0, 0, 0, 64, 163, 3, 0, 0, 0, 0, 0, 0, 240, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
247, 255, 253, 33, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 127, 0, 0, 240, 0, 248, 0, 0,
|
||||
0, 124, 0, 0, 0, 0, 0, 0, 31, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255,
|
||||
255, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128,
|
||||
247, 63, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 68, 8, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0,
|
||||
255, 255, 3, 0, 0, 0, 0, 0, 192, 63, 0, 0, 128, 255, 3, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 200, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 102,
|
||||
0, 8, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 157, 193, 2, 0, 0, 0, 0, 48, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 32, 33, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0,
|
||||
127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 110, 240, 0,
|
||||
0, 0, 0, 0, 135, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 255, 127, 0, 0, 0, 0, 0, 0, 0, 3, 0,
|
||||
0, 0, 0, 0, 120, 38, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 128, 239, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 192, 127, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 128, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 3, 248, 255, 231, 15, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,};
|
||||
|
||||
private static final short wtable[] = {16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 18, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 19, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 21, 22, 23, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 26, 16, 16, 16, 16, 27, 16, 16, 17, 17, 17, 17, 17,
|
||||
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
|
||||
17, 28, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17,
|
||||
16, 16, 16, 29, 30, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 31, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 32, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 251, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 63, 0, 0, 0, 255, 15, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 254, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 224, 255, 255, 255, 255, 63, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255,
|
||||
255, 255, 255, 7, 255, 255, 255, 255, 15, 0, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 31, 255, 255, 255, 255, 255, 255, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 15, 0, 255, 255, 127, 248,
|
||||
255, 255, 255, 255, 255, 15, 0, 0, 255, 3, 0, 0, 255, 255, 255, 255, 247, 255, 127, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 255, 255, 255, 255, 255, 7, 255, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
/** Return the terminal display width of a code point: 0, 1 or 2. */
|
||||
public static int width(int wc) {
|
||||
if (wc < 0xff) return (wc + 1 & 0x7f) >= 0x21 ? 1 : (wc != 0) ? 0 : 0;
|
||||
if ((wc & 0xfffeffff) < 0xfffe) {
|
||||
if (((table[table[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 0;
|
||||
if (((wtable[wtable[wc >> 8] * 32 + ((wc & 255) >> 3)] >> (wc & 7)) & 1) != 0) return 2;
|
||||
return 1;
|
||||
}
|
||||
if ((wc & 0xfffe) == 0xfffe) return 0;
|
||||
if (wc - 0x20000 < 0x20000) return 2;
|
||||
if (wc == 0xe0001 || wc - 0xe0020 < 0x5f || wc - 0xe0100 < 0xef) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** The width at an index position in a java char array. */
|
||||
public static int width(char[] chars, int index) {
|
||||
char c = chars[index];
|
||||
return Character.isHighSurrogate(c) ? width(Character.toCodePoint(c, chars[index + 1])) : width(c);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.termux.view;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
/**
|
||||
* Input and scale listener which may be set on a {@link TerminalView} through
|
||||
* {@link TerminalView#setOnKeyListener(TerminalKeyListener)}.
|
||||
* <p/>
|
||||
* TODO: Rename to TerminalViewClient.
|
||||
*/
|
||||
public interface TerminalKeyListener {
|
||||
|
||||
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
|
||||
float onScale(float scale);
|
||||
|
||||
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
|
||||
void onSingleTapUp(MotionEvent e);
|
||||
|
||||
boolean shouldBackButtonBeMappedToEscape();
|
||||
|
||||
void copyModeChanged(boolean copyMode);
|
||||
|
||||
boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session);
|
||||
|
||||
boolean onKeyUp(int keyCode, KeyEvent e);
|
||||
|
||||
boolean readControlKey();
|
||||
|
||||
boolean readAltKey();
|
||||
|
||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||
|
||||
}
|
||||
@@ -1,910 +0,0 @@
|
||||
package com.termux.view;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Build;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.ActionMode;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
/** View displaying and interacting with a {@link TerminalSession}. */
|
||||
public final class TerminalView extends View {
|
||||
|
||||
/** Log view key and IME events. */
|
||||
private static final boolean LOG_KEY_EVENTS = false;
|
||||
|
||||
/** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */
|
||||
TerminalSession mTermSession;
|
||||
/** Our terminal emulator whose session is {@link #mTermSession}. */
|
||||
TerminalEmulator mEmulator;
|
||||
|
||||
TerminalRenderer mRenderer;
|
||||
|
||||
TerminalKeyListener mOnKeyListener;
|
||||
|
||||
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
|
||||
int mTopRow;
|
||||
|
||||
boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection;
|
||||
int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||
float mSelectionDownX, mSelectionDownY;
|
||||
private ActionMode mActionMode;
|
||||
private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle;
|
||||
|
||||
float mScaleFactor = 1.f;
|
||||
final GestureAndScaleRecognizer mGestureRecognizer;
|
||||
|
||||
/** Keep track of where mouse touch event started which we report as mouse scroll. */
|
||||
private int mMouseScrollStartX = -1, mMouseScrollStartY = -1;
|
||||
/** Keep track of the time when a touch event leading to sending mouse scroll events started. */
|
||||
private long mMouseStartDownTime = -1;
|
||||
|
||||
final Scroller mScroller;
|
||||
|
||||
/** What was left in from scrolling movement. */
|
||||
float mScrollRemainder;
|
||||
|
||||
/** If non-zero, this is the last unicode code point received if that was a combining character. */
|
||||
int mCombiningAccent;
|
||||
|
||||
public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
|
||||
super(context, attributes);
|
||||
mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() {
|
||||
|
||||
boolean scrolledWithFinger;
|
||||
|
||||
@Override
|
||||
public boolean onUp(MotionEvent e) {
|
||||
mScrollRemainder = 0.0f;
|
||||
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText && !scrolledWithFinger) {
|
||||
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
|
||||
// for zooming.
|
||||
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
|
||||
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, false);
|
||||
return true;
|
||||
}
|
||||
scrolledWithFinger = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
if (mEmulator == null) return true;
|
||||
if (mIsSelectingText) {
|
||||
toggleSelectingText(null);
|
||||
return true;
|
||||
}
|
||||
requestFocus();
|
||||
if (!mEmulator.isMouseTrackingActive()) {
|
||||
if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
mOnKeyListener.onSingleTapUp(e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e, float distanceX, float distanceY) {
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
if (mEmulator.isMouseTrackingActive() && e.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
// If moving with mouse pointer while pressing button, report that instead of scroll.
|
||||
// This means that we never report moving with button press-events for touch input,
|
||||
// since we cannot just start sending these events without a starting press event,
|
||||
// which we do not do for touch input, only mouse in onTouchEvent().
|
||||
sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
||||
} else {
|
||||
scrolledWithFinger = true;
|
||||
distanceY += mScrollRemainder;
|
||||
int deltaRows = (int) (distanceY / mRenderer.mFontLineSpacing);
|
||||
mScrollRemainder = distanceY - deltaRows * mRenderer.mFontLineSpacing;
|
||||
doScroll(e, deltaRows);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(float focusX, float focusY, float scale) {
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
mScaleFactor *= scale;
|
||||
mScaleFactor = mOnKeyListener.onScale(mScaleFactor);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) {
|
||||
if (mEmulator == null || mIsSelectingText) return true;
|
||||
// Do not start scrolling until last fling has been taken care of:
|
||||
if (!mScroller.isFinished()) return true;
|
||||
|
||||
final boolean mouseTrackingAtStartOfFling = mEmulator.isMouseTrackingActive();
|
||||
float SCALE = 0.25f;
|
||||
if (mouseTrackingAtStartOfFling) {
|
||||
mScroller.fling(0, 0, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.mRows / 2, mEmulator.mRows / 2);
|
||||
} else {
|
||||
mScroller.fling(0, mTopRow, 0, -(int) (velocityY * SCALE), 0, 0, -mEmulator.getScreen().getActiveTranscriptRows(), 0);
|
||||
}
|
||||
|
||||
post(new Runnable() {
|
||||
private int mLastY = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mouseTrackingAtStartOfFling != mEmulator.isMouseTrackingActive()) {
|
||||
mScroller.abortAnimation();
|
||||
return;
|
||||
}
|
||||
if (mScroller.isFinished()) return;
|
||||
boolean more = mScroller.computeScrollOffset();
|
||||
int newY = mScroller.getCurrY();
|
||||
int diff = mouseTrackingAtStartOfFling ? (newY - mLastY) : (newY - mTopRow);
|
||||
doScroll(e2, diff);
|
||||
mLastY = newY;
|
||||
if (more) post(this);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(float x, float y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
// Do not treat is as a single confirmed tap - it may be followed by zoom.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
if (!mGestureRecognizer.isInProgress() && !mIsSelectingText) {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
toggleSelectingText(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
mScroller = new Scroller(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param onKeyListener Listener for all kinds of key events, both hardware and IME (which makes it different from that
|
||||
* available with {@link View#setOnKeyListener(OnKeyListener)}.
|
||||
*/
|
||||
public void setOnKeyListener(TerminalKeyListener onKeyListener) {
|
||||
this.mOnKeyListener = onKeyListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a {@link TerminalSession} to this view.
|
||||
*
|
||||
* @param session The {@link TerminalSession} this view will be displaying.
|
||||
*/
|
||||
public boolean attachSession(TerminalSession session) {
|
||||
if (session == mTermSession) return false;
|
||||
mTopRow = 0;
|
||||
|
||||
mTermSession = session;
|
||||
mEmulator = null;
|
||||
mCombiningAccent = 0;
|
||||
|
||||
updateSize();
|
||||
|
||||
// Wait with enabling the scrollbar until we have a terminal to get scroll position from.
|
||||
setVerticalScrollBarEnabled(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
// Using InputType.TYPE_TEXT_VARIATION_URI avoids having an extra row of numbers on the
|
||||
// Google keyboard. https://github.com/termux/termux-app/issues/87.
|
||||
// It also makes the '/' keyboard more accessible, and makes some sense.
|
||||
//
|
||||
// If using just "TYPE_NULL", there is a problem with the "Google Pinyin Input" being in
|
||||
// word mode when used with the "En" tab available when the "Show English keyboard" option
|
||||
// is enabled - see https://github.com/termux/termux-packages/issues/25.
|
||||
// Adding TYPE_TEXT_FLAG_NO_SUGGESTIONS fixes Pinyin Input and removes the row of numbers
|
||||
// on the Google keyboard. . It also causes Swype to be put in
|
||||
// word mode, but using TYPE_TEXT_VARIATION_VISIBLE_PASSWORD would fix that. But for now
|
||||
// use InputType.TYPE_TEXT_VARIATION_URI as it makes more sense.
|
||||
outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_URI;
|
||||
|
||||
// Let part of the application show behind when in landscape:
|
||||
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||
|
||||
return new BaseInputConnection(this, true) {
|
||||
|
||||
@Override
|
||||
public boolean finishComposingText() {
|
||||
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()");
|
||||
commitText(getEditable(), 0);
|
||||
|
||||
// Clear the editable.
|
||||
getEditable().clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
if (LOG_KEY_EVENTS)
|
||||
Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")");
|
||||
if (mEmulator == null) return true;
|
||||
final int textLengthInChars = text.length();
|
||||
for (int i = 0; i < textLengthInChars; i++) {
|
||||
char firstChar = text.charAt(i);
|
||||
int codePoint;
|
||||
if (Character.isHighSurrogate(firstChar)) {
|
||||
if (++i < textLengthInChars) {
|
||||
codePoint = Character.toCodePoint(firstChar, text.charAt(i));
|
||||
} else {
|
||||
// At end of string, with no low surrogate following the high:
|
||||
codePoint = TerminalEmulator.UNICODE_REPLACEMENT_CHAR;
|
||||
}
|
||||
} else {
|
||||
codePoint = firstChar;
|
||||
}
|
||||
|
||||
boolean ctrlHeld = false;
|
||||
if (codePoint <= 31 && codePoint != 27) {
|
||||
// E.g. penti keyboard for ctrl input.
|
||||
ctrlHeld = true;
|
||||
switch (codePoint) {
|
||||
case 31:
|
||||
codePoint = '_';
|
||||
break;
|
||||
case 30:
|
||||
codePoint = '^';
|
||||
break;
|
||||
case 29:
|
||||
codePoint = ']';
|
||||
break;
|
||||
case 28:
|
||||
codePoint = '\\';
|
||||
break;
|
||||
default:
|
||||
codePoint += 96;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inputCodePoint(codePoint, ctrlHeld, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
||||
if (LOG_KEY_EVENTS)
|
||||
Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
|
||||
|
||||
// Swype keyboard sometimes(?) sends this on backspace:
|
||||
if (leftLength == 0 && rightLength == 0) leftLength = 1;
|
||||
|
||||
for (int i = 0; i < leftLength; i++)
|
||||
sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||
if (text.length() == 0) {
|
||||
// Avoid log spam "SpannableStringBuilder: SPAN_EXCLUSIVE_EXCLUSIVE spans cannot
|
||||
// have a zero length" when backspacing with the Google keyboard.
|
||||
getEditable().clear();
|
||||
} else {
|
||||
super.setComposingText(text, newCursorPosition);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeVerticalScrollRange() {
|
||||
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeVerticalScrollExtent() {
|
||||
return mEmulator == null ? 1 : mEmulator.mRows;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeVerticalScrollOffset() {
|
||||
return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows() + mTopRow - mEmulator.mRows;
|
||||
}
|
||||
|
||||
public void onScreenUpdated() {
|
||||
if (mEmulator == null) return;
|
||||
|
||||
boolean skipScrolling = false;
|
||||
if (mIsSelectingText) {
|
||||
// Do not scroll when selecting text.
|
||||
int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows();
|
||||
int rowShift = mEmulator.getScrollCounter();
|
||||
if (-mTopRow + rowShift > rowsInHistory) {
|
||||
// .. unless we're hitting the end of history transcript, in which
|
||||
// case we abort text selection and scroll to end.
|
||||
toggleSelectingText(null);
|
||||
} else {
|
||||
skipScrolling = true;
|
||||
mTopRow -= rowShift;
|
||||
mSelY1 -= rowShift;
|
||||
mSelY2 -= rowShift;
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipScrolling && mTopRow != 0) {
|
||||
// Scroll down if not already there.
|
||||
if (mTopRow < -3) {
|
||||
// Awaken scroll bars only if scrolling a noticeable amount
|
||||
// - we do not want visible scroll bars during normal typing
|
||||
// of one row at a time.
|
||||
awakenScrollBars();
|
||||
}
|
||||
mTopRow = 0;
|
||||
}
|
||||
|
||||
mEmulator.clearScrollCounter();
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text size, which in turn sets the number of rows and columns.
|
||||
*
|
||||
* @param textSize the new font size, in density-independent pixels.
|
||||
*/
|
||||
public void setTextSize(int textSize) {
|
||||
mRenderer = new TerminalRenderer(textSize, mRenderer == null ? Typeface.MONOSPACE : mRenderer.mTypeface);
|
||||
updateSize();
|
||||
}
|
||||
|
||||
public void setTypeface(Typeface newTypeface) {
|
||||
mRenderer = new TerminalRenderer(mRenderer.mTextSize, newTypeface);
|
||||
updateSize();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCheckIsTextEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpaque() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Send a single mouse event code to the terminal. */
|
||||
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
|
||||
int x = (int) (e.getX() / mRenderer.mFontWidth) + 1;
|
||||
int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1;
|
||||
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
|
||||
if (mMouseStartDownTime == e.getDownTime()) {
|
||||
x = mMouseScrollStartX;
|
||||
y = mMouseScrollStartY;
|
||||
} else {
|
||||
mMouseStartDownTime = e.getDownTime();
|
||||
mMouseScrollStartX = x;
|
||||
mMouseScrollStartY = y;
|
||||
}
|
||||
}
|
||||
mEmulator.sendMouseEvent(button, x, y, pressed);
|
||||
}
|
||||
|
||||
/** Perform a scroll, either from dragging the screen or by scrolling a mouse wheel. */
|
||||
void doScroll(MotionEvent event, int rowsDown) {
|
||||
boolean up = rowsDown < 0;
|
||||
int amount = Math.abs(rowsDown);
|
||||
for (int i = 0; i < amount; i++) {
|
||||
if (mEmulator.isMouseTrackingActive()) {
|
||||
sendMouseEventCode(event, up ? TerminalEmulator.MOUSE_WHEELUP_BUTTON : TerminalEmulator.MOUSE_WHEELDOWN_BUTTON, true);
|
||||
} else if (mEmulator.isAlternateBufferActive()) {
|
||||
// Send up and down key events for scrolling, which is what some terminals do to make scroll work in
|
||||
// e.g. less, which shifts to the alt screen without mouse handling.
|
||||
handleKeyCode(up ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN, 0);
|
||||
} else {
|
||||
mTopRow = Math.min(0, Math.max(-(mEmulator.getScreen().getActiveTranscriptRows()), mTopRow + (up ? -1 : 1)));
|
||||
if (!awakenScrollBars()) invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Overriding {@link View#onGenericMotionEvent(MotionEvent)}. */
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
if (mEmulator != null && event.isFromSource(InputDevice.SOURCE_MOUSE) && event.getAction() == MotionEvent.ACTION_SCROLL) {
|
||||
// Handle mouse wheel scrolling.
|
||||
boolean up = event.getAxisValue(MotionEvent.AXIS_VSCROLL) > 0.0f;
|
||||
doScroll(event, up ? -3 : 3);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
@TargetApi(23)
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (mEmulator == null) return true;
|
||||
final int action = ev.getAction();
|
||||
|
||||
if (mIsSelectingText) {
|
||||
int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
mInitialTextSelection = false;
|
||||
break;
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
int distanceFromSel1 = Math.abs(cx - mSelX1) + Math.abs(cy - mSelY1);
|
||||
int distanceFromSel2 = Math.abs(cx - mSelX2) + Math.abs(cy - mSelY2);
|
||||
mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2;
|
||||
mSelectionDownX = ev.getX();
|
||||
mSelectionDownY = ev.getY();
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (mInitialTextSelection) break;
|
||||
float deltaX = ev.getX() - mSelectionDownX;
|
||||
float deltaY = ev.getY() - mSelectionDownY;
|
||||
int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth);
|
||||
int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing);
|
||||
mSelectionDownX += deltaCols * mRenderer.mFontWidth;
|
||||
mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing;
|
||||
if (mIsDraggingLeftSelection) {
|
||||
mSelX1 += deltaCols;
|
||||
mSelY1 += deltaRows;
|
||||
} else {
|
||||
mSelX2 += deltaCols;
|
||||
mSelY2 += deltaRows;
|
||||
}
|
||||
|
||||
mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1));
|
||||
mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2));
|
||||
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) {
|
||||
// Switch handles.
|
||||
mIsDraggingLeftSelection = !mIsDraggingLeftSelection;
|
||||
int tmpX1 = mSelX1, tmpY1 = mSelY1;
|
||||
mSelX1 = mSelX2;
|
||||
mSelY1 = mSelY2;
|
||||
mSelX2 = tmpX1;
|
||||
mSelY2 = tmpY1;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
mActionMode.invalidateContentRect();
|
||||
invalidate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
} else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) {
|
||||
if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) {
|
||||
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
|
||||
return true;
|
||||
} else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||
}
|
||||
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
|
||||
switch (ev.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_UP:
|
||||
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
mGestureRecognizer.onTouchEvent(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
if (LOG_KEY_EVENTS)
|
||||
Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")");
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (mIsSelectingText) {
|
||||
toggleSelectingText(null);
|
||||
return true;
|
||||
} else if (mOnKeyListener.shouldBackButtonBeMappedToEscape()) {
|
||||
// Intercept back button to treat it as escape:
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
return onKeyDown(keyCode, event);
|
||||
case KeyEvent.ACTION_UP:
|
||||
return onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (LOG_KEY_EVENTS)
|
||||
Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")");
|
||||
if (mEmulator == null) return true;
|
||||
|
||||
if (mOnKeyListener.onKeyDown(keyCode, event, mTermSession)) {
|
||||
invalidate();
|
||||
return true;
|
||||
} else if (event.isSystem() && (!mOnKeyListener.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
} else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && keyCode == KeyEvent.KEYCODE_UNKNOWN) {
|
||||
mTermSession.write(event.getCharacters());
|
||||
return true;
|
||||
}
|
||||
|
||||
final int metaState = event.getMetaState();
|
||||
final boolean controlDownFromEvent = event.isCtrlPressed();
|
||||
final boolean leftAltDownFromEvent = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0;
|
||||
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
|
||||
|
||||
int keyMod = 0;
|
||||
if (controlDownFromEvent) keyMod |= KeyHandler.KEYMOD_CTRL;
|
||||
if (event.isAltPressed()) keyMod |= KeyHandler.KEYMOD_ALT;
|
||||
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
|
||||
if (handleKeyCode(keyCode, keyMod)) {
|
||||
if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clear Ctrl since we handle that ourselves:
|
||||
int bitsToClear = KeyEvent.META_CTRL_MASK;
|
||||
if (rightAltDownFromEvent) {
|
||||
// Let right Alt/Alt Gr be used to compose characters.
|
||||
} else {
|
||||
// Use left alt to send to terminal (e.g. Left Alt+B to jump back a word), so remove:
|
||||
bitsToClear |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||
}
|
||||
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
|
||||
|
||||
int result = event.getUnicodeChar(effectiveMetaState);
|
||||
if (LOG_KEY_EVENTS)
|
||||
Log.i(EmulatorDebug.LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int oldCombiningAccent = mCombiningAccent;
|
||||
if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
||||
// If entered combining accent previously, write it out:
|
||||
if (mCombiningAccent != 0)
|
||||
inputCodePoint(mCombiningAccent, controlDownFromEvent, leftAltDownFromEvent);
|
||||
mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK;
|
||||
} else {
|
||||
if (mCombiningAccent != 0) {
|
||||
int combinedChar = KeyCharacterMap.getDeadChar(mCombiningAccent, result);
|
||||
if (combinedChar > 0) result = combinedChar;
|
||||
mCombiningAccent = 0;
|
||||
}
|
||||
inputCodePoint(result, controlDownFromEvent, leftAltDownFromEvent);
|
||||
}
|
||||
|
||||
if (mCombiningAccent != oldCombiningAccent) invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) {
|
||||
if (LOG_KEY_EVENTS) {
|
||||
Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent="
|
||||
+ leftAltDownFromEvent + ")");
|
||||
}
|
||||
|
||||
final boolean controlDown = controlDownFromEvent || mOnKeyListener.readControlKey();
|
||||
final boolean altDown = leftAltDownFromEvent || mOnKeyListener.readAltKey();
|
||||
|
||||
if (mOnKeyListener.onCodePoint(codePoint, controlDown, mTermSession)) return;
|
||||
|
||||
if (controlDown) {
|
||||
if (codePoint >= 'a' && codePoint <= 'z') {
|
||||
codePoint = codePoint - 'a' + 1;
|
||||
} else if (codePoint >= 'A' && codePoint <= 'Z') {
|
||||
codePoint = codePoint - 'A' + 1;
|
||||
} else if (codePoint == ' ' || codePoint == '2') {
|
||||
codePoint = 0;
|
||||
} else if (codePoint == '[' || codePoint == '3') {
|
||||
codePoint = 27; // ^[ (Esc)
|
||||
} else if (codePoint == '\\' || codePoint == '4') {
|
||||
codePoint = 28;
|
||||
} else if (codePoint == ']' || codePoint == '5') {
|
||||
codePoint = 29;
|
||||
} else if (codePoint == '^' || codePoint == '6') {
|
||||
codePoint = 30; // control-^
|
||||
} else if (codePoint == '_' || codePoint == '7' || codePoint == '/') {
|
||||
// "Ctrl-/ sends 0x1f which is equivalent of Ctrl-_ since the days of VT102"
|
||||
// - http://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
|
||||
codePoint = 31;
|
||||
} else if (codePoint == '8') {
|
||||
codePoint = 127; // DEL
|
||||
}
|
||||
}
|
||||
|
||||
if (codePoint > -1) {
|
||||
// Work around bluetooth keyboards sending funny unicode characters instead
|
||||
// of the more normal ones from ASCII that terminal programs expect - the
|
||||
// desire to input the original characters should be low.
|
||||
switch (codePoint) {
|
||||
case 0x02DC: // SMALL TILDE.
|
||||
codePoint = 0x007E; // TILDE (~).
|
||||
break;
|
||||
case 0x02CB: // MODIFIER LETTER GRAVE ACCENT.
|
||||
codePoint = 0x0060; // GRAVE ACCENT (`).
|
||||
break;
|
||||
case 0x02C6: // MODIFIER LETTER CIRCUMFLEX ACCENT.
|
||||
codePoint = 0x005E; // CIRCUMFLEX ACCENT (^).
|
||||
break;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/** Input the specified keyCode if applicable and return if the input was consumed. */
|
||||
public boolean handleKeyCode(int keyCode, int keyMod) {
|
||||
TerminalEmulator term = mTermSession.getEmulator();
|
||||
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
|
||||
if (code == null) return false;
|
||||
mTermSession.write(code);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a key is released in the view.
|
||||
*
|
||||
* @param keyCode The keycode of the key which was released.
|
||||
* @param event A {@link KeyEvent} describing the event.
|
||||
* @return Whether the event was handled.
|
||||
*/
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (LOG_KEY_EVENTS)
|
||||
Log.i(EmulatorDebug.LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
|
||||
if (mEmulator == null) return true;
|
||||
|
||||
if (mOnKeyListener.onKeyUp(keyCode, event)) {
|
||||
invalidate();
|
||||
return true;
|
||||
} else if (event.isSystem()) {
|
||||
// Let system key events through.
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called during layout when the size of this view has changed. If you were just added to the view
|
||||
* hierarchy, you're called with the old values of 0.
|
||||
*/
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
updateSize();
|
||||
}
|
||||
|
||||
/** Check if the terminal size in rows and columns should be updated. */
|
||||
public void updateSize() {
|
||||
int viewWidth = getWidth();
|
||||
int viewHeight = getHeight();
|
||||
if (viewWidth == 0 || viewHeight == 0 || mTermSession == null) return;
|
||||
|
||||
// Set to 80 and 24 if you want to enable vttest.
|
||||
int newColumns = Math.max(4, (int) (viewWidth / mRenderer.mFontWidth));
|
||||
int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
|
||||
|
||||
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
|
||||
mTermSession.updateSize(newColumns, newRows);
|
||||
mEmulator = mTermSession.getEmulator();
|
||||
|
||||
mTopRow = 0;
|
||||
scrollTo(0, 0);
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (mEmulator == null) {
|
||||
canvas.drawColor(0XFF000000);
|
||||
} else {
|
||||
mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2);
|
||||
|
||||
if (mIsSelectingText) {
|
||||
final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth();
|
||||
final int gripHandleMargin = gripHandleWidth / 4; // See the png.
|
||||
|
||||
int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin;
|
||||
int top = (mSelY1 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
||||
mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight());
|
||||
mLeftSelectionHandle.draw(canvas);
|
||||
|
||||
int left = Math.round((mSelX2 + 1) * mRenderer.mFontWidth) - gripHandleMargin;
|
||||
top = (mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent;
|
||||
mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight());
|
||||
mRightSelectionHandle.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggle text selection mode in the view. */
|
||||
@TargetApi(23)
|
||||
public void toggleSelectingText(MotionEvent ev) {
|
||||
mIsSelectingText = !mIsSelectingText;
|
||||
mOnKeyListener.copyModeChanged(mIsSelectingText);
|
||||
|
||||
if (mIsSelectingText) {
|
||||
if (mLeftSelectionHandle == null) {
|
||||
mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material);
|
||||
mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material);
|
||||
}
|
||||
|
||||
int cx = (int) (ev.getX() / mRenderer.mFontWidth);
|
||||
final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE);
|
||||
// Offset for finger:
|
||||
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
|
||||
int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow;
|
||||
|
||||
mSelX1 = mSelX2 = cx;
|
||||
mSelY1 = mSelY2 = cy;
|
||||
|
||||
TerminalBuffer screen = mEmulator.getScreen();
|
||||
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
|
||||
// Selecting something other than whitespace. Expand to word.
|
||||
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) {
|
||||
mSelX1--;
|
||||
}
|
||||
while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
|
||||
mSelX2++;
|
||||
}
|
||||
}
|
||||
|
||||
mInitialTextSelection = true;
|
||||
mIsDraggingLeftSelection = true;
|
||||
mSelectionDownX = ev.getX();
|
||||
mSelectionDownY = ev.getY();
|
||||
|
||||
final ActionMode.Callback callback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
int show = MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case 1:
|
||||
String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||
mTermSession.clipboardText(selectedText);
|
||||
break;
|
||||
case 2:
|
||||
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
|
||||
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
showContextMenu();
|
||||
break;
|
||||
}
|
||||
toggleSelectingText(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mActionMode = startActionMode(new ActionMode.Callback2() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
return callback.onCreateActionMode(mode, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
return callback.onActionItemClicked(mode, item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
||||
int x1 = Math.round(mSelX1 * mRenderer.mFontWidth);
|
||||
int x2 = Math.round(mSelX2 * mRenderer.mFontWidth);
|
||||
int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||
int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing);
|
||||
outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2);
|
||||
}
|
||||
}, ActionMode.TYPE_FLOATING);
|
||||
} else {
|
||||
mActionMode = startActionMode(callback);
|
||||
}
|
||||
|
||||
|
||||
invalidate();
|
||||
} else {
|
||||
mActionMode.finish();
|
||||
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalSession getCurrentSession() {
|
||||
return mTermSession;
|
||||
}
|
||||
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 695 B |
|
Before Width: | Height: | Size: 786 B |
|
Before Width: | Height: | Size: 597 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 779 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 983 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#E0E0E0" />
|
||||
</shape>
|
||||
</shape>
|
||||
|
||||
4
app/src/main/res/drawable/current_session_black.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#212325" />
|
||||
</shape>
|
||||
28
app/src/main/res/drawable/ic_foreground.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<!-- Keep in sync with non-adaptive ic_launcher.xml -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M34,38
|
||||
h6
|
||||
l12,16
|
||||
l-12,16
|
||||
h-6
|
||||
l12,-16
|
||||
"
|
||||
/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M56,66
|
||||
h18
|
||||
v4
|
||||
h-18
|
||||
"
|
||||
/>
|
||||
|
||||
</vector>
|
||||
17
app/src/main/res/drawable/ic_new_session.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="M 12, 12
|
||||
m -10.5, 0
|
||||
a 10.5,10.5 0 1,0 21,0
|
||||
a 10.5,10.5 0 1,0 -21,0"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
|
||||
</vector>
|
||||
24
app/src/main/res/drawable/ic_service_notification.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!--
|
||||
Updated notification icon compliant with system icons guidelines
|
||||
https://material.io/design/iconography/system-icons.html
|
||||
-->
|
||||
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
|
||||
<path
|
||||
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
|
||||
<path
|
||||
android:pathData="M13,18H22V20H13V18Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
|
||||
</group>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_settings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FF000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_activated="true" android:drawable="@drawable/current_session_black"/>
|
||||
<item android:state_activated="false" android:drawable="@drawable/session_ripple_black"/>
|
||||
</selector>
|
||||
@@ -4,4 +4,4 @@
|
||||
<item>
|
||||
<color android:color="@android:color/white" />
|
||||
</item>
|
||||
</ripple>
|
||||
</ripple>
|
||||
|
||||
7
app/src/main/res/drawable/session_ripple_black.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@android:color/darker_gray" >
|
||||
<item>
|
||||
<color android:color="@android:color/background_dark" />
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/text_select_handle_left_mtrl_alpha"
|
||||
android:tint="#2196F3" />
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:src="@drawable/text_select_handle_right_mtrl_alpha"
|
||||
android:tint="#2196F3" />
|
||||
9
app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
109
app/src/main/res/layout/activity_termux.xml
Normal file
@@ -0,0 +1,109 @@
|
||||
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/activity_termux_root_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.termux.view.TerminalView
|
||||
android:id="@+id/terminal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginRight="3dp"
|
||||
android:layout_marginLeft="3dp"
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||
android:scrollbars="vertical"
|
||||
android:importantForAutofill="no"
|
||||
android:autofillHints="password" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/left_drawer"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:background="@android:color/white"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@android:color/transparent"
|
||||
android:dividerHeight="0dp"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<ImageButton
|
||||
android:id="@+id/settings_button"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_settings"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/action_open_settings" />
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/terminal_sessions_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_weight="1"
|
||||
android:choiceMode="singleChoice"
|
||||
android:longClickable="true" />
|
||||
|
||||
<LinearLayout
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggle_keyboard_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_toggle_soft_keyboard" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/new_session_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_new_session" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/terminal_toolbar_view_pager"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="37.5dp"
|
||||
android:background="@android:drawable/screen_background_dark_transparent"
|
||||
android:layout_alignParentBottom="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/activity_termux_bottom_space_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@android:color/transparent" />
|
||||
|
||||
</com.termux.app.terminal.TermuxActivityRootView>
|
||||
@@ -1,75 +0,0 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v4.widget.DrawerLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_above="@+id/viewpager"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.termux.view.TerminalView
|
||||
android:id="@+id/terminal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/left_drawer"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:background="@android:color/white"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@android:color/transparent"
|
||||
android:dividerHeight="0dp"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/left_drawer_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_weight="1"
|
||||
android:choiceMode="singleChoice"
|
||||
android:longClickable="true" />
|
||||
|
||||
<LinearLayout
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggle_keyboard_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/toggle_soft_keyboard" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/new_session_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/new_session" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.v4.widget.DrawerLayout>
|
||||
|
||||
<android.support.v4.view.ViewPager
|
||||
android:id="@+id/viewpager"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:background="@android:drawable/screen_background_dark_transparent"
|
||||
android:layout_alignParentBottom="true" />
|
||||
</RelativeLayout>
|
||||
@@ -1,9 +1,9 @@
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/row_line"
|
||||
android:id="@+id/session_title"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:background="@drawable/selected_session_background"
|
||||
android:background="@drawable/session_background_selected"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="start|center_vertical"
|
||||
android:padding="6dip"
|
||||
android:textSize="14sp" />
|
||||
android:textSize="14sp" />
|
||||
20
app/src/main/res/layout/preference_markdown_text.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-appcompat-release/preference/preference/res/layout/preference.xml
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:id="@android:id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<include android:id="@android:id/summary" layout="@layout/markdown_adapter_node_default" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.termux.app.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/extra_keys"
|
||||
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/terminal_toolbar_extra_keys"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -1,14 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/text_input"
|
||||
android:id="@+id/terminal_toolbar_text_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionSend|flagNoFullscreen"
|
||||
android:maxLines="1"
|
||||
android:inputType="text"
|
||||
android:singleLine="true"
|
||||
android:importantForAutofill="no"
|
||||
android:textColor="@android:color/white"
|
||||
android:textColorHighlight="@android:color/darker_gray"
|
||||
android:paddingTop="0dp"
|
||||
android:textCursorDrawable="@null"
|
||||
android:paddingBottom="0dp"
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/black"/>
|
||||
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/black"/>
|
||||
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |