mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-03-19 08:18:45 -07:00
Compare commits
1397 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eca248bcd5 | ||
|
|
9999cb8b5d | ||
|
|
652bf0edab | ||
|
|
9035a94804 | ||
|
|
59eca3fd0a | ||
|
|
2be3caa7c3 | ||
|
|
5cb025e7cc | ||
|
|
e4f4239f5c | ||
|
|
debf1800d8 | ||
|
|
7008c563e8 | ||
|
|
62c6ab8c0a | ||
|
|
6242a605f5 | ||
|
|
2a5ff26e22 | ||
|
|
4bfb9d820f | ||
|
|
b02f52f283 | ||
|
|
6771f1141b | ||
|
|
c4ddebfa73 | ||
|
|
a211541fc3 | ||
|
|
4d758f871b | ||
|
|
162b84661b | ||
|
|
b087cb2876 | ||
|
|
299350cb7b | ||
|
|
ab93c22750 | ||
|
|
24b16e3609 | ||
|
|
a6f0cb20af | ||
|
|
f4dd504eeb | ||
|
|
bcffc941bb | ||
|
|
41059e8d69 | ||
|
|
b89589ee0c | ||
|
|
741eff8c50 | ||
|
|
bc3f6b1a99 | ||
|
|
6a9bd4531f | ||
|
|
2ce0fdebe6 | ||
|
|
6e9e12e4b2 | ||
|
|
73c3cfc7f3 | ||
|
|
3b73a0ea2c | ||
|
|
d8d64c8a75 | ||
|
|
125d2caffd | ||
|
|
2fb7030527 | ||
|
|
08d412d43c | ||
|
|
fb79eddc73 | ||
|
|
0279882309 | ||
|
|
be63d2bb36 | ||
|
|
1f74953b84 | ||
|
|
5f01d29204 | ||
|
|
e12e3f907e | ||
|
|
8946938987 | ||
|
|
6f4e4743a1 | ||
|
|
3e3d45d2dc | ||
|
|
44a31234d3 | ||
|
|
d7301154a5 | ||
|
|
d7ec7f826b | ||
|
|
2cab7d6c7b | ||
|
|
e000607d71 | ||
|
|
08d0b9be27 | ||
|
|
45c7cf19c5 | ||
|
|
4204fc56d4 | ||
|
|
390d529168 | ||
|
|
a757758e10 | ||
|
|
7a6c8508ce | ||
|
|
ffdb5cb511 | ||
|
|
47f1a33585 | ||
|
|
20d6d70ff6 | ||
|
|
397a906905 | ||
|
|
a9ade79617 | ||
|
|
70a0dafb21 | ||
|
|
e6c4268694 | ||
|
|
9107bb6af3 | ||
|
|
925ed577e7 | ||
|
|
1d4fbd9e75 | ||
|
|
7845da2943 | ||
|
|
6bb48df712 | ||
|
|
e8a58776f1 | ||
|
|
07e92948c8 | ||
|
|
74ead75ce8 | ||
|
|
bb082e703c | ||
|
|
ee25db0627 | ||
|
|
9ee6a6b3d7 | ||
|
|
e872fafd8b | ||
|
|
dd3798905f | ||
|
|
c07eac118c | ||
|
|
4dd6e3c252 | ||
|
|
0d39b10889 | ||
|
|
628b4d4715 | ||
|
|
be5246aef5 | ||
|
|
b55f597ccf | ||
|
|
e729eec636 | ||
|
|
a657af5dc9 | ||
|
|
34137b112a | ||
|
|
5f13a8f8f7 | ||
|
|
8495a03947 | ||
|
|
ac8d4200e1 | ||
|
|
e590ffbdcb | ||
|
|
97138f1e91 | ||
|
|
bea50fafba | ||
|
|
0919db6b11 | ||
|
|
18c1347bd3 | ||
|
|
0bbc736051 | ||
|
|
01cb75eaef | ||
|
|
33eaa7c5eb | ||
|
|
8734423cb0 | ||
|
|
da9e72b2a2 | ||
|
|
ff4ca74cfe | ||
|
|
200665c48a | ||
|
|
dd42aa99ea | ||
|
|
0936cdb192 | ||
|
|
871643dce2 | ||
|
|
a510554b21 | ||
|
|
9cc830c565 | ||
|
|
ddbac50645 | ||
|
|
b5138a4af0 | ||
|
|
64752f38e8 | ||
|
|
9ac4b5ce7d | ||
|
|
505053f9b4 | ||
|
|
ccb264f33a | ||
|
|
84f7d75d30 | ||
|
|
9a776c22d9 | ||
|
|
363566d0d5 | ||
|
|
d9dc459bf4 | ||
|
|
5d6b703622 | ||
|
|
f7ce9c38e1 | ||
|
|
bdbfb40383 | ||
|
|
283fc0f46f | ||
|
|
2c24a41bf2 | ||
|
|
97c93a1f4d | ||
|
|
8d534e6de8 | ||
|
|
3a60ef2039 | ||
|
|
52d7eff03f | ||
|
|
020e23ea13 | ||
|
|
1599bfc2c5 | ||
|
|
c8d51b38ba | ||
|
|
f741a4aeb8 | ||
|
|
4ee2235961 | ||
|
|
536e50c6e0 | ||
|
|
57d9fc6099 | ||
|
|
52d8910bdd | ||
|
|
c94bd49a89 | ||
|
|
b72ba6759e | ||
|
|
5bcb55b7fc | ||
|
|
0dc8231585 | ||
|
|
470acc93c9 | ||
|
|
0edb80b10f | ||
|
|
bcc6296d94 | ||
|
|
c3db2e368d | ||
|
|
d37da5ca66 | ||
|
|
aac52176ed | ||
|
|
78e2fc37e5 | ||
|
|
ca2e40593f | ||
|
|
c07fdc87e3 | ||
|
|
7270f5e413 | ||
|
|
07cc85ccb1 | ||
|
|
d6f17c42d5 | ||
|
|
d60806f429 | ||
|
|
8836a09c8c | ||
|
|
f16e93c7db | ||
|
|
1b0ddec66e | ||
|
|
cd8820f563 | ||
|
|
b70192ca3e | ||
|
|
d42ec5da9a | ||
|
|
742913ebcb | ||
|
|
ed206c6480 | ||
|
|
f9a8052583 | ||
|
|
f4fdd516f9 | ||
|
|
5925a71f94 | ||
|
|
3cda9beb93 | ||
|
|
8b7d1ffcdd | ||
|
|
8d02d0632e | ||
|
|
dd743f6f7e | ||
|
|
cf483ad4d2 | ||
|
|
4aed644e08 | ||
|
|
0acc39cec0 | ||
|
|
8b3a44344f | ||
|
|
8b49eda85a | ||
|
|
7057d4c7f1 | ||
|
|
aab8344058 | ||
|
|
7cccf83b37 | ||
|
|
f10ad93c4e | ||
|
|
f143b5df15 | ||
|
|
71213cc6f4 | ||
|
|
e2a1774e5b | ||
|
|
0222527a1e | ||
|
|
312bfe1bab | ||
|
|
48c62a1dae | ||
|
|
cfc2bcb665 | ||
|
|
94b1ff674f | ||
|
|
111136733a | ||
|
|
c8caaa98f5 | ||
|
|
8d28f10a3f | ||
|
|
177a456d8b | ||
|
|
ef4e230258 | ||
|
|
17082af438 | ||
|
|
1df5b34175 | ||
|
|
ea5fe7525d | ||
|
|
a75c335261 | ||
|
|
3903f42cf6 | ||
|
|
fb0c4ea838 | ||
|
|
bc89c60977 | ||
|
|
bd657c354c | ||
|
|
675b5f9565 | ||
|
|
1b2c43268e | ||
|
|
653730d75e | ||
|
|
d472e9c36e | ||
|
|
484d53ef7e | ||
|
|
c4e2985677 | ||
|
|
42d9f87bc9 | ||
|
|
2e4fa6864c | ||
|
|
e2abb648ac | ||
|
|
3599dcedfb | ||
|
|
ea72666df8 | ||
|
|
bd2a47ba18 | ||
|
|
b861671391 | ||
|
|
e91fc75d86 | ||
|
|
78f5cd55c7 | ||
|
|
9787a69528 | ||
|
|
87b8fe374d | ||
|
|
7b706bb0cb | ||
|
|
c1491b8d2b | ||
|
|
5cbaf2ae11 | ||
|
|
8ebc6207b4 | ||
|
|
7848ee616b | ||
|
|
fd193c3cae | ||
|
|
36d33c7a85 | ||
|
|
5caf28d27c | ||
|
|
2c39d0234d | ||
|
|
c313812129 | ||
|
|
af51880a81 | ||
|
|
db8d832707 | ||
|
|
8dc23d0ead | ||
|
|
b4287700d5 | ||
|
|
8d10ab89f2 | ||
|
|
49fdc1addb | ||
|
|
1333d3b986 | ||
|
|
335146a6a2 | ||
|
|
eaf9527971 | ||
|
|
da937a88c8 | ||
|
|
9476e7282d | ||
|
|
251c3c3e0e | ||
|
|
cd0eca20b0 | ||
|
|
6839cb9ab2 | ||
|
|
d11a3397d8 | ||
|
|
975120d6a6 | ||
|
|
e489b3b6dd | ||
|
|
589a270b8d | ||
|
|
7961be5cfa | ||
|
|
959430e030 | ||
|
|
2923c8ccd1 | ||
|
|
7df4a9d74f | ||
|
|
bf4ed295da | ||
|
|
a5fca960dc | ||
|
|
f99912b9db | ||
|
|
a54bdb54e4 | ||
|
|
cd9851a1fe | ||
|
|
9ca469898c | ||
|
|
0665549473 | ||
|
|
9d7a14b335 | ||
|
|
62e29fee74 | ||
|
|
e472db552b | ||
|
|
466e4bd4e1 | ||
|
|
4cf525c588 | ||
|
|
c8aec2510d | ||
|
|
ccbfe0e66e | ||
|
|
23ea28de6f | ||
|
|
55c3ee3a6f | ||
|
|
2a42ca2b8f | ||
|
|
a897e82fa4 | ||
|
|
ffa15831d3 | ||
|
|
a344ebf28c | ||
|
|
78f7fa348e | ||
|
|
d8c448b99d | ||
|
|
d4b83b6a44 | ||
|
|
e5d36d1d24 | ||
|
|
ff18cb8e70 | ||
|
|
37a9724a54 | ||
|
|
d660401063 | ||
|
|
88541d6f49 | ||
|
|
ecd6129fe5 | ||
|
|
6dfe9df9e2 | ||
|
|
e81de7ec36 | ||
|
|
c78da1ce24 | ||
|
|
7b2d40987c | ||
|
|
3a37e8c9c5 | ||
|
|
58b405bce1 | ||
|
|
810174ef73 | ||
|
|
690a5ac033 | ||
|
|
89aad31f7e | ||
|
|
7124db98e3 | ||
|
|
0860e859f7 | ||
|
|
04008949b8 | ||
|
|
39f2940bd1 | ||
|
|
1460317ebd | ||
|
|
12340c9bd5 | ||
|
|
c4590fe2ba | ||
|
|
b36066bbcd | ||
|
|
65d1c5827c | ||
|
|
1d675c8b2e | ||
|
|
0b494ed7df | ||
|
|
48d9fc24eb | ||
|
|
83426f7f36 | ||
|
|
0e86d4dbcb | ||
|
|
5e050d7456 | ||
|
|
898580bf90 | ||
|
|
86621a4c46 | ||
|
|
a834e72b71 | ||
|
|
d8cf42af16 | ||
|
|
8c79d66b7b | ||
|
|
fada8b148a | ||
|
|
dc0acea47c | ||
|
|
78d1200608 | ||
|
|
527bbc0368 | ||
|
|
4c89c7e2b3 | ||
|
|
adbea7e313 | ||
|
|
76962f965e | ||
|
|
a4b8c5e46b | ||
|
|
83c707439c | ||
|
|
25dd6121f4 | ||
|
|
67f35ad027 | ||
|
|
0c4b8afbc5 | ||
|
|
34b30d7ce1 | ||
|
|
2215088973 | ||
|
|
8b7fb6cdde | ||
|
|
94c7dbedf2 | ||
|
|
b1dc47a047 | ||
|
|
62b1310d97 | ||
|
|
0a86916d3a | ||
|
|
9907ce57aa | ||
|
|
b92626cacc | ||
|
|
5a762f0a8e | ||
|
|
5dd7a7d804 | ||
|
|
7831f40691 | ||
|
|
4f4b1ff885 | ||
|
|
97901979dd | ||
|
|
287316842c | ||
|
|
608786e8f3 | ||
|
|
9684a35cab | ||
|
|
e3e4202954 | ||
|
|
23c2054d46 | ||
|
|
a20a2a8fa0 | ||
|
|
a2896be4a6 | ||
|
|
e9220a28d9 | ||
|
|
cf12087e21 | ||
|
|
00c1b36837 | ||
|
|
03e034795d | ||
|
|
79c0fafe43 | ||
|
|
d499819ba0 | ||
|
|
86da917174 | ||
|
|
30bd7d6555 | ||
|
|
e5a12f0f5f | ||
|
|
c85a8434c6 | ||
|
|
427a1ca4e5 | ||
|
|
22884e173a | ||
|
|
d1829308e9 | ||
|
|
73840f8721 | ||
|
|
c7d1af9805 | ||
|
|
4ad26d3dfb | ||
|
|
0c70b7670c | ||
|
|
f44d044095 | ||
|
|
5c1cb13472 | ||
|
|
3327fc668e | ||
|
|
610945ac54 | ||
|
|
ddf5474917 | ||
|
|
6ba1685ade | ||
|
|
e02b5f7868 | ||
|
|
ab2e5d1e7e | ||
|
|
f3fef7bfe4 | ||
|
|
c34c7838bb | ||
|
|
c8a16b0e0c | ||
|
|
14f9ed91a1 | ||
|
|
7a207d4ccf | ||
|
|
92a42d901f | ||
|
|
084d89fcce | ||
|
|
55b036c071 | ||
|
|
30e79310ab | ||
|
|
f063fa5054 | ||
|
|
7bd901273c | ||
|
|
c1e061603b | ||
|
|
cb08504fe5 | ||
|
|
c0a1fb77be | ||
|
|
4864c1112a | ||
|
|
9ddeab034b | ||
|
|
c4847ed288 | ||
|
|
b8f1523fb2 | ||
|
|
fb7fa8a6b3 | ||
|
|
9c7d359093 | ||
|
|
eb54bc1fd7 | ||
|
|
d4a0286e13 | ||
|
|
83e66767ff | ||
|
|
7dc010749b | ||
|
|
8e8d013b1b | ||
|
|
bba0373808 | ||
|
|
1fa318dc8c | ||
|
|
6edc5e2037 | ||
|
|
1523ed9f78 | ||
|
|
8e604d2ab8 | ||
|
|
2aba7247a9 | ||
|
|
e66fe8533e | ||
|
|
b03fbb3917 | ||
|
|
c2ece62e4c | ||
|
|
8c972dcf34 | ||
|
|
50af14f2a3 | ||
|
|
e0a356b319 | ||
|
|
c09a792958 | ||
|
|
0bbfe7f44d | ||
|
|
a396abf565 | ||
|
|
1e3edb8883 | ||
|
|
3b8b61bf35 | ||
|
|
6f90456036 | ||
|
|
f56fd4e215 | ||
|
|
aa35aac5d5 | ||
|
|
1f162b819d | ||
|
|
52ef1d1cb2 | ||
|
|
f14e3a89cc | ||
|
|
95d3eac2e0 | ||
|
|
8e73536e02 | ||
|
|
12a0870bc9 | ||
|
|
6ff82c4e86 | ||
|
|
c64de35375 | ||
|
|
ee5283f4e8 | ||
|
|
bd0e954fea | ||
|
|
675471a49e | ||
|
|
c90e73ccec | ||
|
|
a43c1267d8 | ||
|
|
e8958c6b5c | ||
|
|
e8a3bf82c6 | ||
|
|
27fd79176a | ||
|
|
28d86a3454 | ||
|
|
c6c1a17ae6 | ||
|
|
2b47d47215 | ||
|
|
0e82df9e10 | ||
|
|
893821ad88 | ||
|
|
6b80fbfa99 | ||
|
|
8c3c7d0194 | ||
|
|
b94a3d9f2f | ||
|
|
442d0b5ddc | ||
|
|
494615d9a0 | ||
|
|
afbfb81837 | ||
|
|
3ed4e258a3 | ||
|
|
dddd41c95b | ||
|
|
5f2ca81e86 | ||
|
|
c9eac0c438 | ||
|
|
b6b34f7612 | ||
|
|
e55c413261 | ||
|
|
0399cde50a | ||
|
|
019eb03823 | ||
|
|
363410e1c0 | ||
|
|
fc2ef21660 | ||
|
|
18cb659ff3 | ||
|
|
63231d97ce | ||
|
|
9ac81a8a25 | ||
|
|
79af2787ae | ||
|
|
f5f9b285c0 | ||
|
|
6c05f2ae85 | ||
|
|
29043e1684 | ||
|
|
b73d4a7022 | ||
|
|
ad95e8951b | ||
|
|
bf591fca12 | ||
|
|
dcf027884d | ||
|
|
584f3820fe | ||
|
|
3c7c46307a | ||
|
|
4d80361805 | ||
|
|
9a74e19117 | ||
|
|
b1e17706a4 | ||
|
|
caad129d69 | ||
|
|
da58571ce5 | ||
|
|
2aa7f1c094 | ||
|
|
823e31a91b | ||
|
|
fb926ae302 | ||
|
|
e0489eeffd | ||
|
|
dc9d5a4cac | ||
|
|
143743d0b0 | ||
|
|
563f0d5ad5 | ||
|
|
c99f4a591b | ||
|
|
449204e380 | ||
|
|
a85c4c6528 | ||
|
|
d203a6fff6 | ||
|
|
6c612d66d7 | ||
|
|
540253a55b | ||
|
|
15b7c4ccd1 | ||
|
|
442d5335ea | ||
|
|
8a80eea597 | ||
|
|
5e35703091 | ||
|
|
b7ca73f431 | ||
|
|
a14fc90f07 | ||
|
|
c913f7ec74 | ||
|
|
7f6c9e8411 | ||
|
|
bb02ea3a20 | ||
|
|
3981c9665e | ||
|
|
88628fdf3c | ||
|
|
0469817781 | ||
|
|
a786801141 | ||
|
|
ab86732c89 | ||
|
|
59622d1688 | ||
|
|
58a25a3e2b | ||
|
|
15dca29a87 | ||
|
|
46980819c0 | ||
|
|
4fb6a7268c | ||
|
|
c05e963f37 | ||
|
|
7f7f625864 | ||
|
|
b25aa8295a | ||
|
|
15a605765c | ||
|
|
b575c95710 | ||
|
|
a48a9c858a | ||
|
|
0d8d6290a3 | ||
|
|
4dcd733ddd | ||
|
|
b62835cbeb | ||
|
|
6ea740b5ab | ||
|
|
7ab98dd5ac | ||
|
|
fc8b3400fc | ||
|
|
54428ba415 | ||
|
|
95d1e69d8e | ||
|
|
a0f13ab49f | ||
|
|
c3e8405020 | ||
|
|
a93593ea66 | ||
|
|
23eff70883 | ||
|
|
110dd4a8b9 | ||
|
|
d9c2bffc9f | ||
|
|
049db49dc8 | ||
|
|
7c1d2ec61e | ||
|
|
a1b2830c06 | ||
|
|
82d1d19267 | ||
|
|
4d4195c02d | ||
|
|
5637a258fc | ||
|
|
ee6810f417 | ||
|
|
7098248c64 | ||
|
|
0d31d356ef | ||
|
|
b782e7dcb7 | ||
|
|
a4671b4698 | ||
|
|
7edd8be169 | ||
|
|
24650eefe4 | ||
|
|
8e1a44e7eb | ||
|
|
2722875190 | ||
|
|
3ca6d06f69 | ||
|
|
10e47248de | ||
|
|
e73ff679ac | ||
|
|
53e401fa2d | ||
|
|
d2768357da | ||
|
|
a6c2ba7c1e | ||
|
|
aae5b466fb | ||
|
|
2b7be8b949 | ||
|
|
b6511a510d | ||
|
|
704541aef2 | ||
|
|
005560a4c5 | ||
|
|
231a5d1853 | ||
|
|
9e2b59060d | ||
|
|
08ea937f7c | ||
|
|
2baedf74d1 | ||
|
|
32faa4ced6 | ||
|
|
ccdb0b5d13 | ||
|
|
8506b672ad | ||
|
|
ce2e33bb20 | ||
|
|
6707b72260 | ||
|
|
5885b8c20d | ||
|
|
820710c086 | ||
|
|
51cf196bf7 | ||
|
|
dadba44cf9 | ||
|
|
2ce4a5543b | ||
|
|
9112a3a4f5 | ||
|
|
24615afda1 | ||
|
|
c5778f398b | ||
|
|
4eb4097b9b | ||
|
|
c512496847 | ||
|
|
506961a10d | ||
|
|
3414415907 | ||
|
|
dc2ae7cfd1 | ||
|
|
2e86d21c29 | ||
|
|
2654382c43 | ||
|
|
9e26b73813 | ||
|
|
10cd13bf80 | ||
|
|
f10ee5f887 | ||
|
|
47cc532d96 | ||
|
|
218327f92b | ||
|
|
4eae66a1a7 | ||
|
|
b09ceeb43c | ||
|
|
4fb539c110 | ||
|
|
849b284da5 | ||
|
|
895b5f6cbf | ||
|
|
cb3d4ea514 | ||
|
|
0d89a2a97d | ||
|
|
3ca5913055 | ||
|
|
df6b808f49 | ||
|
|
09c7ac754b | ||
|
|
805da67c23 | ||
|
|
3c6889505b | ||
|
|
c8e9ce7627 | ||
|
|
837c679a31 | ||
|
|
06616659b8 | ||
|
|
a34c04f999 | ||
|
|
da43ac89a0 | ||
|
|
830fc758b9 | ||
|
|
0f3cfef278 | ||
|
|
b32d7bfafd | ||
|
|
c0899f2939 | ||
|
|
082330808f | ||
|
|
024da05888 | ||
|
|
377b6d0cc2 | ||
|
|
c661009b31 | ||
|
|
613f2d31c5 | ||
|
|
7dbb973db5 | ||
|
|
f4502f8be8 | ||
|
|
455b13b83c | ||
|
|
8b98709743 | ||
|
|
1b12f45f39 | ||
|
|
a5cad532ff | ||
|
|
070719db50 | ||
|
|
28cccdf7aa | ||
|
|
b7e0986a5c | ||
|
|
da709745dd | ||
|
|
8b6771d487 | ||
|
|
e1b847fbc5 | ||
|
|
7188de1205 | ||
|
|
44fb7dbcbe | ||
|
|
8086b5933c | ||
|
|
7f675f4bf7 | ||
|
|
5e6b53e0da | ||
|
|
5b29fefc65 | ||
|
|
16a168535d | ||
|
|
33f70f8f6d | ||
|
|
4f18a66d73 | ||
|
|
250dc16007 | ||
|
|
7af273e047 | ||
|
|
e381aebaa0 | ||
|
|
45d91c9658 | ||
|
|
4a9158f667 | ||
|
|
0d9ee89e7f | ||
|
|
abaff72304 | ||
|
|
b828e2d0b2 | ||
|
|
53d7cbc11b | ||
|
|
310be7ab47 | ||
|
|
60894e458f | ||
|
|
fbebb6ac10 | ||
|
|
a9f8c20703 | ||
|
|
ae0b15d197 | ||
|
|
869aa62328 | ||
|
|
dcd3bc58a3 | ||
|
|
a82f17c594 | ||
|
|
b38fd1ca5f | ||
|
|
8e82113bce | ||
|
|
f723ef153b | ||
|
|
1dc723fb6d | ||
|
|
8f271c2575 | ||
|
|
7cf56b4406 | ||
|
|
c2eb603957 | ||
|
|
e6bd2ff60f | ||
|
|
5604074eba | ||
|
|
3f061c1a1e | ||
|
|
55e78a7b1a | ||
|
|
000f1d6041 | ||
|
|
2cbec20238 | ||
|
|
4b724c7257 | ||
|
|
ab04c6ab39 | ||
|
|
821a6c6954 | ||
|
|
5f27a62221 | ||
|
|
c76cc4c6bd | ||
|
|
52b75c53b6 | ||
|
|
9db2e99086 | ||
|
|
e9e2ecf2dd | ||
|
|
9a9e617c35 | ||
|
|
3a0becc783 | ||
|
|
1f974cb220 | ||
|
|
1db80228e8 | ||
|
|
838e1e254d | ||
|
|
554eda8fe1 | ||
|
|
2bdc047c4d | ||
|
|
e64f59ce5b | ||
|
|
b8140ad4e6 | ||
|
|
5a55483698 | ||
|
|
2d341863f5 | ||
|
|
278046becb | ||
|
|
5c0497354f | ||
|
|
98c258df93 | ||
|
|
c578cccfd5 | ||
|
|
07835a3e0e | ||
|
|
09131aca89 | ||
|
|
9ce998a6df | ||
|
|
ca36b42d79 | ||
|
|
37df39ec37 | ||
|
|
1701361a73 | ||
|
|
4c14ae33f5 | ||
|
|
d4a9ef7b7f | ||
|
|
1539cfe888 | ||
|
|
9093be1329 | ||
|
|
606d076251 | ||
|
|
46a34e19bc | ||
|
|
5ac7dc0b37 | ||
|
|
3b27de3715 | ||
|
|
939bfac920 | ||
|
|
f601bf12d5 | ||
|
|
0495468d02 | ||
|
|
300a2a242c | ||
|
|
33aebb5976 | ||
|
|
b3d6809c0b | ||
|
|
461f7e9f89 | ||
|
|
9cc50b20d8 | ||
|
|
f488e9df8f | ||
|
|
0dc596e206 | ||
|
|
c3bf03190b | ||
|
|
021ae891a9 | ||
|
|
9c03514eb1 | ||
|
|
eb74b266e1 | ||
|
|
80eb6ff25a | ||
|
|
7b81e2d2d1 | ||
|
|
a8789073f1 | ||
|
|
c8fe0f5524 | ||
|
|
d33b077a13 | ||
|
|
2282365cf8 | ||
|
|
9a00b7b942 | ||
|
|
d54baadbed | ||
|
|
0869a90fe3 | ||
|
|
2754b1dcf8 | ||
|
|
0db6314661 | ||
|
|
b5d2ef18e8 | ||
|
|
6e22476acc | ||
|
|
b26db8cee6 | ||
|
|
33cb39c8af | ||
|
|
f247759a6e | ||
|
|
de7e5bdfe7 | ||
|
|
53a8ba8cfe | ||
|
|
f2d057baba | ||
|
|
93bcf2cd25 | ||
|
|
6d82515cfc | ||
|
|
a177d3b022 | ||
|
|
92b2e06e57 | ||
|
|
f919bb0e99 | ||
|
|
054971e899 | ||
|
|
93c3d36452 | ||
|
|
4c38af994d | ||
|
|
bbb8efe92c | ||
|
|
659dd09723 | ||
|
|
4931825912 | ||
|
|
ef81cdab4f | ||
|
|
7c0b25cad9 | ||
|
|
b38ab2a7d6 | ||
|
|
a97191052b | ||
|
|
2963d4ca9e | ||
|
|
6aab856de7 | ||
|
|
94d1c66f8a | ||
|
|
7ff4d7608e | ||
|
|
46ef915c83 | ||
|
|
63b0a0d96b | ||
|
|
ea4cabdfc5 | ||
|
|
0185ddf577 | ||
|
|
ddae568741 | ||
|
|
fcb7ebb090 | ||
|
|
8d446fcc16 | ||
|
|
881d3b5221 | ||
|
|
fe9ec3bc6d | ||
|
|
480198dcd0 | ||
|
|
4ab7bc0d97 | ||
|
|
7173693d1b | ||
|
|
6b81716440 | ||
|
|
88e8e15607 | ||
|
|
69181a6b72 | ||
|
|
b11b81122a | ||
|
|
648e3ee36b | ||
|
|
724b94f320 | ||
|
|
a6e65f9a7e | ||
|
|
af5c4d09c4 | ||
|
|
872394cb58 | ||
|
|
fcbbe9a22e | ||
|
|
b168163ef0 | ||
|
|
3e38b8fed1 | ||
|
|
f90c548f27 | ||
|
|
c981c40218 | ||
|
|
dcbf37c5e8 | ||
|
|
300b233a27 | ||
|
|
e32cd03d0b | ||
|
|
a07b9315a5 | ||
|
|
e9694c6195 | ||
|
|
4a2a37c87a | ||
|
|
7dca5b831a | ||
|
|
be5ff68140 | ||
|
|
59f40d5fe5 | ||
|
|
1fbd053a42 | ||
|
|
966c6314f8 | ||
|
|
c92204c724 | ||
|
|
bb9947d4d2 | ||
|
|
7c8cdb4ad6 | ||
|
|
bd7f9c9e46 | ||
|
|
9a33a4dfe2 | ||
|
|
47e918bc92 | ||
|
|
c194168d9b | ||
|
|
cacc60b1ac | ||
|
|
52063b3652 | ||
|
|
85a4eaff59 | ||
|
|
45fa1fce70 | ||
|
|
2112c916f5 | ||
|
|
d6e159bff9 | ||
|
|
2f710a564f | ||
|
|
27cfc4945c | ||
|
|
7cdada92c8 | ||
|
|
8f1e57d4f9 | ||
|
|
8178666b49 | ||
|
|
313532dcaa | ||
|
|
2f8f3dc266 | ||
|
|
df6ada5ce3 | ||
|
|
a89b9e6af1 | ||
|
|
23ed275614 | ||
|
|
cfd1e0cf22 | ||
|
|
eb400f19b1 | ||
|
|
19f15f16f6 | ||
|
|
e158cfddfa | ||
|
|
d0cf93a08d | ||
|
|
08ad0e74dd | ||
|
|
722374a024 | ||
|
|
c6f0762510 | ||
|
|
941a363c5a | ||
|
|
2afcdc64a0 | ||
|
|
3c66c4bbc5 | ||
|
|
9f5cd5e1cc | ||
|
|
a35f2bb73b | ||
|
|
6cf00130f4 | ||
|
|
6c27ba6b88 | ||
|
|
dd3b9980e7 | ||
|
|
02e189a029 | ||
|
|
72b8d12ee4 | ||
|
|
eed03080c1 | ||
|
|
090cb4b0f9 | ||
|
|
6f2c76b898 | ||
|
|
f61827cbec | ||
|
|
3f2264f2c7 | ||
|
|
c1cadf4bdc | ||
|
|
0e56991369 | ||
|
|
4dc1c59040 | ||
|
|
33b7b8b297 | ||
|
|
e6af5ed460 | ||
|
|
b678afa4b6 | ||
|
|
4bac2df4e7 | ||
|
|
50416eee09 | ||
|
|
73cf501d33 | ||
|
|
d2b7907bed | ||
|
|
99d5dd5ea8 | ||
|
|
5fdb841fa8 | ||
|
|
7c88484d64 | ||
|
|
b22b6a4204 | ||
|
|
2a3d34c812 | ||
|
|
c50ee722a1 | ||
|
|
ffc1e38e48 | ||
|
|
6219d5fcbf | ||
|
|
2e4440b702 | ||
|
|
0d9ec0931b | ||
|
|
60e8415369 | ||
|
|
652a26d5d9 | ||
|
|
f57839379a | ||
|
|
36bd00a046 | ||
|
|
fb5ee86615 | ||
|
|
30bf5c8448 | ||
|
|
2051836a73 | ||
|
|
2cb0af1ff3 | ||
|
|
a1b6568226 | ||
|
|
1eddbfd72c | ||
|
|
21ed095601 | ||
|
|
000a2e4d59 | ||
|
|
7abe635de9 | ||
|
|
9a008c17ba | ||
|
|
08dbf728a4 | ||
|
|
4670f762d3 | ||
|
|
efa49567fa | ||
|
|
0ffc4527a7 | ||
|
|
dd9d43be96 | ||
|
|
865fca71a5 | ||
|
|
6b4baa3bcd | ||
|
|
a9ee2d7d18 | ||
|
|
d654b9cb97 | ||
|
|
4d2921e742 | ||
|
|
ecc74d45d1 | ||
|
|
5de597f079 | ||
|
|
156b0e67ca | ||
|
|
10069215f4 | ||
|
|
92b305a389 | ||
|
|
d20b30c771 | ||
|
|
83209b21ff | ||
|
|
81658d45f7 | ||
|
|
c951b208a1 | ||
|
|
050a073771 | ||
|
|
21d374214f | ||
|
|
19ea25a9d0 | ||
|
|
dbf6e40dfe | ||
|
|
d56f4fbc90 | ||
|
|
73c3d741a7 | ||
|
|
2b5fc75127 | ||
|
|
991802ab82 | ||
|
|
7f6b5305ba | ||
|
|
825c6c4316 | ||
|
|
f00408c793 | ||
|
|
a6ff3672af | ||
|
|
2290ddeb89 | ||
|
|
74af79ad03 | ||
|
|
b6c24a3a8a | ||
|
|
a8c2ae223a | ||
|
|
953d44302c | ||
|
|
24e46a5971 | ||
|
|
b1297c4192 | ||
|
|
9ae328fd84 | ||
|
|
625a1d6f44 | ||
|
|
987e5f5413 | ||
|
|
715284b70d | ||
|
|
62fc7868ac | ||
|
|
1a70796339 | ||
|
|
af6965eefa | ||
|
|
8f7d2e38f7 | ||
|
|
be433fa667 | ||
|
|
0ccd6e7381 | ||
|
|
907bbbda41 | ||
|
|
4393bc077d | ||
|
|
365b373480 | ||
|
|
47e6dd286d | ||
|
|
0dbaf52566 | ||
|
|
66f49dfab5 | ||
|
|
f8967e9274 | ||
|
|
a4f008fde5 | ||
|
|
e9980c778b | ||
|
|
06b6fb0c33 | ||
|
|
38cb3d4105 | ||
|
|
db99caf258 | ||
|
|
39dbffadfe | ||
|
|
b7505c3c9c | ||
|
|
3185e5a7ca | ||
|
|
e0cbe28711 | ||
|
|
66cee19cea | ||
|
|
2ec29ade79 | ||
|
|
c865d4e187 | ||
|
|
a42a0a53ce | ||
|
|
6d79de7d71 | ||
|
|
7e9abe6e90 | ||
|
|
4d5510be4f | ||
|
|
b04e1394c0 | ||
|
|
2aa923191e | ||
|
|
4bf1c74164 | ||
|
|
472c7878b2 | ||
|
|
38ad871e33 | ||
|
|
c5d34670c4 | ||
|
|
154121f3dd | ||
|
|
3d91a561fe | ||
|
|
2c6adbc69b | ||
|
|
5280982363 | ||
|
|
18c45ae289 | ||
|
|
41fbd2a7be | ||
|
|
5e45884af4 | ||
|
|
d78ee171bc | ||
|
|
356ee1febd | ||
|
|
cc044ccc4c | ||
|
|
9c638cc463 | ||
|
|
df786eb2b6 | ||
|
|
8e7186eebb | ||
|
|
74b7b84561 | ||
|
|
308c9999fa | ||
|
|
930bb8687f | ||
|
|
f2c4288d2d | ||
|
|
b44141ae39 | ||
|
|
86e0020964 | ||
|
|
94d3daeadf | ||
|
|
79334b7702 | ||
|
|
df66458db6 | ||
|
|
97705704e2 | ||
|
|
1206179580 | ||
|
|
a0b8aa4da6 | ||
|
|
65207f96c8 | ||
|
|
062e498bdd | ||
|
|
1057cb3e3c | ||
|
|
2dd23b2518 | ||
|
|
8cab12998c | ||
|
|
48b1c26dc8 | ||
|
|
f1e0bc3e4a | ||
|
|
38527cd58f | ||
|
|
e94d65b4b2 | ||
|
|
27ece3c7df | ||
|
|
06687abffc | ||
|
|
deedb462a0 | ||
|
|
c48962bdf7 | ||
|
|
1ef3f6e13b | ||
|
|
83a34a9004 | ||
|
|
e30bda6c8d | ||
|
|
00e9d76a5a | ||
|
|
6cda6c2fae | ||
|
|
6dfda6dc39 | ||
|
|
f41994cb52 | ||
|
|
a003336497 | ||
|
|
401090d6fe | ||
|
|
90dcc1cd30 | ||
|
|
2ac464b186 | ||
|
|
8b7fae278b | ||
|
|
d73c2daf6d | ||
|
|
ca25935de3 | ||
|
|
d7750b7220 | ||
|
|
98861f0b5a | ||
|
|
e35925d520 | ||
|
|
685a2d2101 | ||
|
|
f7e471616d | ||
|
|
c013a349af | ||
|
|
61ea59a27b | ||
|
|
e55f338367 | ||
|
|
1425cf4105 | ||
|
|
b493a985b0 | ||
|
|
1fe9ede940 | ||
|
|
1fd49e4987 | ||
|
|
d49b02b274 | ||
|
|
d47e70cfaa | ||
|
|
40cb031af5 | ||
|
|
1dcf325547 | ||
|
|
4e99997013 | ||
|
|
334554697d | ||
|
|
e77cbd0c15 | ||
|
|
46ba008b9d | ||
|
|
58aded31c2 | ||
|
|
6f6b0ade06 | ||
|
|
d9b67a207b | ||
|
|
c7083659aa | ||
|
|
a6d1803105 | ||
|
|
66eef75673 | ||
|
|
367f5f7b44 | ||
|
|
0edcb03c45 | ||
|
|
63eef153de | ||
|
|
68442f38ac | ||
|
|
8d5b9e5329 | ||
|
|
6c0966b795 | ||
|
|
7c2e93d266 | ||
|
|
1ff7b9055f | ||
|
|
49f241b77c | ||
|
|
cfb20b0f86 | ||
|
|
6d6f14fcb3 | ||
|
|
977c981265 | ||
|
|
ef48abf19d | ||
|
|
65c18f9c09 | ||
|
|
ecb31eed40 | ||
|
|
a80cadf587 | ||
|
|
fce1bf2365 | ||
|
|
cbc6d40b2c | ||
|
|
9fbd079560 | ||
|
|
42eb928054 | ||
|
|
577483fde1 | ||
|
|
aa6c7c15cc | ||
|
|
6c807d35b2 | ||
|
|
8ca8cdae97 | ||
|
|
75e37be6f3 | ||
|
|
4985314ca6 | ||
|
|
ac5ceb18c8 | ||
|
|
72b39594d3 | ||
|
|
16ae4aedf1 | ||
|
|
3ba00858e6 | ||
|
|
489100c755 | ||
|
|
da766f2a4e | ||
|
|
c81d7ff76c | ||
|
|
a6e50d3648 | ||
|
|
a177846044 | ||
|
|
19a4e11645 | ||
|
|
67cc36268e | ||
|
|
28770b9a32 | ||
|
|
9f92e1bf15 | ||
|
|
23fe5d5a19 | ||
|
|
9088b584f6 | ||
|
|
beaf636415 | ||
|
|
09bb2fe8dc | ||
|
|
1d6747d90e | ||
|
|
efadd94de3 | ||
|
|
8c0b4e444a | ||
|
|
32c7106e40 | ||
|
|
d2f2a9e4c8 | ||
|
|
985454afd4 | ||
|
|
9e1322de25 | ||
|
|
4e4ec73d94 | ||
|
|
bb39a524d0 | ||
|
|
196d9af099 | ||
|
|
1eeb2a34a1 | ||
|
|
cf43c56218 | ||
|
|
e6c1aec443 | ||
|
|
43fd1c4c1b | ||
|
|
022caca979 | ||
|
|
0352ea2cca | ||
|
|
e483d6befe | ||
|
|
678c07fff5 | ||
|
|
91c92051f1 | ||
|
|
4b8a0388e7 | ||
|
|
66788dc58c | ||
|
|
dd8c28b1cb | ||
|
|
32c5153e8e | ||
|
|
36de62873a | ||
|
|
51e37880c6 | ||
|
|
4b83c1e76c | ||
|
|
b0b04690d5 | ||
|
|
6d1e8d86cb | ||
|
|
eda8c70a80 | ||
|
|
587b6cfd41 | ||
|
|
e774408782 | ||
|
|
187f583c95 | ||
|
|
f5d3a71478 | ||
|
|
d868ff3080 | ||
|
|
f80198a669 | ||
|
|
6076b52c48 | ||
|
|
79a1c39b30 | ||
|
|
5c92d39498 | ||
|
|
6e7a995716 | ||
|
|
a55d570213 | ||
|
|
5d07d0b964 | ||
|
|
ec115cd7e3 | ||
|
|
9b3896fd3d | ||
|
|
a3f5918d25 | ||
|
|
b28326198c | ||
|
|
46275b90c2 | ||
|
|
15e13a8d8b | ||
|
|
b750c89c87 | ||
|
|
8d7c7c3dfb | ||
|
|
8e1a91509c | ||
|
|
927cd571f8 | ||
|
|
5fbd3e5c65 | ||
|
|
877aeb66cb | ||
|
|
8a88d8465a | ||
|
|
dda8cc85c9 | ||
|
|
6a59939d9a | ||
|
|
4745e86c1b | ||
|
|
9aa466c773 | ||
|
|
0243610c09 | ||
|
|
0a2a590ab7 | ||
|
|
89aee6ffa7 | ||
|
|
4eaf701cb7 | ||
|
|
4fff2aa7d8 | ||
|
|
35b3c8ba5c | ||
|
|
1d7cff7703 | ||
|
|
8d81bd0e33 | ||
|
|
7826d7527f | ||
|
|
d4e552d08b | ||
|
|
5a16418543 | ||
|
|
7297aba15a | ||
|
|
bc5d5f9502 | ||
|
|
1761986c1b | ||
|
|
1e034e3e0e | ||
|
|
bbf9756bfa | ||
|
|
96e559fb0e | ||
|
|
4c45775131 | ||
|
|
c072b4254d | ||
|
|
2dbb812126 | ||
|
|
be50f17f55 | ||
|
|
6f77f190f2 | ||
|
|
6bdc57cbe4 | ||
|
|
de00f1d5a9 | ||
|
|
e9b9bf987b | ||
|
|
f4b6385f9f | ||
|
|
75d905a56d | ||
|
|
b1363ee479 | ||
|
|
51afe43a30 | ||
|
|
189c03c047 | ||
|
|
ae9d270a32 | ||
|
|
e47e869f6b | ||
|
|
c39038a439 | ||
|
|
69174e2c13 | ||
|
|
474268a0af | ||
|
|
eadb0307fa | ||
|
|
5a5d0d5d72 | ||
|
|
a1273bc467 | ||
|
|
0c28a916be | ||
|
|
0ba573b789 | ||
|
|
ec42ee152c | ||
|
|
abcb487361 | ||
|
|
d12d9e82f1 | ||
|
|
275208e81b | ||
|
|
41226c12b8 | ||
|
|
f86c66c99d | ||
|
|
93876b5fd3 | ||
|
|
b5b14ce343 | ||
|
|
350d0d600c | ||
|
|
f924ffcbf3 | ||
|
|
0f5963f231 | ||
|
|
1961ff2c40 | ||
|
|
40003691d6 | ||
|
|
8290358241 | ||
|
|
ee34f775c3 | ||
|
|
feb47cd88c | ||
|
|
c6efb51f61 | ||
|
|
a5acf33ccd | ||
|
|
ab9ee449e4 | ||
|
|
9571b6f9be | ||
|
|
207d7fd3f6 | ||
|
|
bcdcfa1104 | ||
|
|
e0a4230dac | ||
|
|
17ba5cba3e | ||
|
|
f2e109ad7d | ||
|
|
c83e141a1c | ||
|
|
6089cc36de | ||
|
|
9638dc0a66 | ||
|
|
b191a14a23 | ||
|
|
cf1bc82537 | ||
|
|
6141bb5bb3 | ||
|
|
4d2b62da0d | ||
|
|
39383229d1 | ||
|
|
08bfbb154a | ||
|
|
d390ca2fdf | ||
|
|
7ad77a14ae | ||
|
|
f33343b4e6 | ||
|
|
af65d07456 | ||
|
|
16d728f379 | ||
|
|
c97ab690b6 | ||
|
|
4caed73fe0 | ||
|
|
4856da1584 | ||
|
|
0a07018fec | ||
|
|
64c82e1f2c | ||
|
|
e8e8afa6c2 | ||
|
|
af2207433d | ||
|
|
75ba62d588 | ||
|
|
606d97ae4d | ||
|
|
d778b0b0a7 | ||
|
|
5ee6daf126 | ||
|
|
43b9a09c9b | ||
|
|
8475a2bb94 | ||
|
|
d8692de2f4 | ||
|
|
33a9abc946 | ||
|
|
ee943afbc9 | ||
|
|
1f7c3e9f14 | ||
|
|
46770db18b | ||
|
|
92f980601c | ||
|
|
d0b8c16651 | ||
|
|
a470ee6f93 | ||
|
|
ff1c56683d | ||
|
|
4ee4cbada6 | ||
|
|
dbc2236dd2 | ||
|
|
a8c4a33e91 | ||
|
|
279f955a84 | ||
|
|
fbd1dbb20c | ||
|
|
6c09fc2e64 | ||
|
|
f3304b482c | ||
|
|
0a85ef61c3 | ||
|
|
dc26ad7125 | ||
|
|
24b1c607f3 | ||
|
|
732a161b67 | ||
|
|
9c7cf340a1 | ||
|
|
399b9e5eba | ||
|
|
5805573625 | ||
|
|
a6b1149b9f | ||
|
|
51e985ae7f | ||
|
|
9929b25339 | ||
|
|
2359cfc480 | ||
|
|
81493475f9 | ||
|
|
0493829231 | ||
|
|
e2d1952ad9 | ||
|
|
7450965458 | ||
|
|
f45384685b | ||
|
|
8abcccc262 | ||
|
|
a9c89cbbbb | ||
|
|
d2eaa6e6c1 | ||
|
|
53257b6ea1 | ||
|
|
c874391be4 | ||
|
|
7e8e013832 | ||
|
|
037f46f7f0 | ||
|
|
d3e1c496ca | ||
|
|
d7d0a44693 | ||
|
|
9d6f6764cb | ||
|
|
cb3ab63815 | ||
|
|
caae932117 | ||
|
|
e9cf27eb5a | ||
|
|
6ee6685f4c | ||
|
|
d15017b777 | ||
|
|
a9387e63e1 | ||
|
|
23c1f0111b | ||
|
|
866386e21f | ||
|
|
bf10496fa9 | ||
|
|
607e6547a7 | ||
|
|
6b21091fe2 | ||
|
|
e58f98e844 | ||
|
|
b8cb9cd84d | ||
|
|
c1038ac6f9 | ||
|
|
c556dd0aac | ||
|
|
d2fbcd07b7 | ||
|
|
bf6359abaa | ||
|
|
d1621845b8 | ||
|
|
f33f1d25d0 | ||
|
|
40f25f4d56 | ||
|
|
e13775ec2c | ||
|
|
ee4dad7a13 | ||
|
|
5e2ef1b7f4 | ||
|
|
f8c38eab74 | ||
|
|
305e8b3d14 | ||
|
|
2a654e5d7f | ||
|
|
57afae3425 | ||
|
|
feb44f875e | ||
|
|
7eebe62bb6 | ||
|
|
9ea9f01933 | ||
|
|
665c6bdc4b | ||
|
|
c79bc83275 | ||
|
|
c30fbdf145 | ||
|
|
f12951bd1d | ||
|
|
52f2e8c4a0 | ||
|
|
1b2af1ed6d | ||
|
|
0f9b2a7df8 | ||
|
|
f2846694e1 | ||
|
|
e668dbf6f7 | ||
|
|
d77a368176 | ||
|
|
ad0da08610 | ||
|
|
0c52385ad4 | ||
|
|
5b8b48ccc1 | ||
|
|
659b9c6fee | ||
|
|
ec31cab5a7 | ||
|
|
dd93556ad8 | ||
|
|
533aeadd38 | ||
|
|
18d0cedbe2 | ||
|
|
5a94ef9106 | ||
|
|
8e8f01f8b5 | ||
|
|
7087badf87 | ||
|
|
47d2d4e3a5 | ||
|
|
6c47d8f556 | ||
|
|
8c9d0314fb | ||
|
|
69144942e3 | ||
|
|
5627053b74 | ||
|
|
0f666de5e6 | ||
|
|
eddc862fa3 | ||
|
|
4327682120 | ||
|
|
af5bdee78f | ||
|
|
0e36e86dbf | ||
|
|
f95478f1f1 | ||
|
|
9fe8741a02 | ||
|
|
a5768e02ea | ||
|
|
f5aaff2b1e | ||
|
|
655f778171 | ||
|
|
2e77a426b2 | ||
|
|
2bcf2e76f1 | ||
|
|
57bd450798 | ||
|
|
582cad1b8d | ||
|
|
6ca2a3d841 | ||
|
|
91773c3311 | ||
|
|
dc61033b2c | ||
|
|
f8d62a4b6c | ||
|
|
1d2145b1b7 | ||
|
|
1f7f84b74a | ||
|
|
cd7a335d0f | ||
|
|
17569005a4 | ||
|
|
f36b21bae5 | ||
|
|
fe1ca52f6d | ||
|
|
1be647a279 | ||
|
|
2ea1a47bec | ||
|
|
2d799dae0d | ||
|
|
c6408babac | ||
|
|
a8c1ed8795 | ||
|
|
e3cb5f8ddd | ||
|
|
e160e211dd | ||
|
|
22d05ca399 | ||
|
|
bd2651057d | ||
|
|
1610092ec4 | ||
|
|
b9e6937996 | ||
|
|
a207f03952 | ||
|
|
851153dd7c | ||
|
|
583ffc8177 | ||
|
|
7518092ad2 | ||
|
|
8c2ad3883a | ||
|
|
d364554425 | ||
|
|
726ffdcd98 | ||
|
|
f9d22cf8ee | ||
|
|
ee50da566f | ||
|
|
9f7d410959 | ||
|
|
bc94ea4334 | ||
|
|
c0c9204848 | ||
|
|
c0d1bf63bc | ||
|
|
bbda0cdffe | ||
|
|
7b5ff99cd1 | ||
|
|
21ddb26db8 | ||
|
|
7bf2e3875f | ||
|
|
b136aba1e2 | ||
|
|
0d84f80b3c | ||
|
|
af45aeb771 | ||
|
|
1c5a435e1f | ||
|
|
0ea1257dcd | ||
|
|
4c92677b5a | ||
|
|
979260bd62 | ||
|
|
f7de649a36 | ||
|
|
0cf0d2b821 | ||
|
|
3733c9a091 | ||
|
|
e9f32e4f68 | ||
|
|
68c2817d40 | ||
|
|
83d837d868 | ||
|
|
093eb15ee1 | ||
|
|
c6412c1b1b | ||
|
|
1151393d74 | ||
|
|
468f3efb13 | ||
|
|
d6b19b9d4c | ||
|
|
709f25f600 | ||
|
|
4b16e4b026 | ||
|
|
cdfbc02922 | ||
|
|
d0c9384233 | ||
|
|
2488668b06 | ||
|
|
52a98cbd51 | ||
|
|
1840c4c486 | ||
|
|
34080f3958 | ||
|
|
e9b76b6aa5 | ||
|
|
b7799b53d9 | ||
|
|
1e206515c7 | ||
|
|
6bb313184d | ||
|
|
2763992434 | ||
|
|
18fe0e6442 | ||
|
|
a70c73bffd | ||
|
|
b4ae3493a6 | ||
|
|
1a16004b20 | ||
|
|
56707b8119 | ||
|
|
c3f9533ddc | ||
|
|
3b3abd63cc | ||
|
|
411d3ed4e9 | ||
|
|
f29cc26103 | ||
|
|
1cd595a598 | ||
|
|
22e023b58d | ||
|
|
7be958e35d | ||
|
|
69b66ef637 | ||
|
|
daf8653c38 | ||
|
|
e2545e57cf |
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -12,13 +12,11 @@
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
tools/** binary
|
||||
tools/rustup-wrapper/** -binary
|
||||
tools/elf-cleaner/** -binary
|
||||
*.jar binary
|
||||
*.exe binary
|
||||
*.apk binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ttf binary
|
||||
|
||||
# Help GitHub detect languages
|
||||
native/jni/external/** linguist-vendored
|
||||
native/jni/systemproperties/** linguist-language=C++
|
||||
|
||||
108
.github/actions/setup/action.yml
vendored
Normal file
108
.github/actions/setup/action.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: Magisk Setup
|
||||
description: Set up the build environment for Magisk
|
||||
inputs:
|
||||
is-asset-build:
|
||||
required: false
|
||||
default: false
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "21"
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Install GNU make
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
brew install make
|
||||
echo 'GNUMAKE=gmake' >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache sccache
|
||||
uses: actions/cache@v5
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
path: .sccache
|
||||
key: sccache-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: sccache-${{ runner.os }}-
|
||||
|
||||
- name: Restore sccache
|
||||
uses: actions/cache/restore@v5
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
path: .sccache
|
||||
key: sccache-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: sccache-${{ runner.os }}-
|
||||
|
||||
- name: Set up sccache
|
||||
shell: bash
|
||||
env:
|
||||
SCCACHE_DIRECT: false
|
||||
SCCACHE_DIR: ${{ github.workspace }}/.sccache
|
||||
SCCACHE_CACHE_SIZE: ${{ inputs.is-asset-build == 'true' && '2G' || '300M' }}
|
||||
SCCACHE_IDLE_TIMEOUT: 0
|
||||
run: |
|
||||
bash $GITHUB_ACTION_PATH/sccache.sh
|
||||
sccache --start-server
|
||||
sccache -z
|
||||
|
||||
- name: Show sccache stats
|
||||
uses: gacts/run-and-post-run@v1
|
||||
with:
|
||||
run: sccache -s
|
||||
post: sccache -s
|
||||
|
||||
- name: Set GRADLE_USER_HOME
|
||||
shell: bash
|
||||
run: echo "GRADLE_USER_HOME=$GITHUB_WORKSPACE/.gradle" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v5
|
||||
if: ${{ inputs.is-asset-build == 'true' && github.event_name != 'pull_request' }}
|
||||
with:
|
||||
path: |
|
||||
.gradle/caches
|
||||
.gradle/wrapper
|
||||
!.gradle/caches/build-cache-*
|
||||
key: gradle-cache-${{ hashFiles('app/gradle/**') }}
|
||||
restore-keys: gradle-cache-
|
||||
|
||||
- name: Restore Gradle dependencies
|
||||
uses: actions/cache/restore@v5
|
||||
if: ${{ inputs.is-asset-build == 'false' || github.event_name == 'pull_request' }}
|
||||
with:
|
||||
path: |
|
||||
.gradle/caches
|
||||
.gradle/wrapper
|
||||
!.gradle/caches/build-cache-*
|
||||
key: gradle-cache-${{ hashFiles('gradle/**') }}
|
||||
restore-keys: gradle-cache-
|
||||
enableCrossOsArchive: true
|
||||
|
||||
- name: Cache Gradle build cache
|
||||
uses: actions/cache@v5
|
||||
if: ${{ inputs.is-asset-build == 'true' && github.event_name != 'pull_request' }}
|
||||
with:
|
||||
path: .gradle/caches/build-cache-*
|
||||
key: gradle-build-cache-${{ github.sha }}
|
||||
restore-keys: gradle-build-cache-
|
||||
|
||||
- name: Restore Gradle build cache
|
||||
uses: actions/cache/restore@v5
|
||||
if: ${{ inputs.is-asset-build == 'false' || github.event_name == 'pull_request' }}
|
||||
with:
|
||||
path: .gradle/caches/build-cache-*
|
||||
key: gradle-build-cache-${{ github.sha }}
|
||||
restore-keys: gradle-build-cache-
|
||||
enableCrossOsArchive: true
|
||||
|
||||
- name: Set up NDK
|
||||
shell: bash
|
||||
run: python build.py -v ndk
|
||||
25
.github/actions/setup/sccache.sh
vendored
Executable file
25
.github/actions/setup/sccache.sh
vendored
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Get latest sccache version
|
||||
get_sccache_ver() {
|
||||
curl -sL 'https://api.github.com/repos/mozilla/sccache/releases/latest' | jq -r .name
|
||||
}
|
||||
|
||||
# $1=variant
|
||||
# $2=install_dir
|
||||
# $3=exe
|
||||
install_from_gh() {
|
||||
local ver=$(curl -sL 'https://api.github.com/repos/mozilla/sccache/releases/latest' | jq -r .name)
|
||||
local url="https://github.com/mozilla/sccache/releases/download/${ver}/sccache-${ver}-$1.tar.gz"
|
||||
local dest="$2/$3"
|
||||
curl -L "$url" | tar xz -O --wildcards "*/$3" > $dest
|
||||
chmod +x $dest
|
||||
}
|
||||
|
||||
if [ $RUNNER_OS = "macOS" ]; then
|
||||
brew install sccache
|
||||
elif [ $RUNNER_OS = "Linux" ]; then
|
||||
install_from_gh x86_64-unknown-linux-musl /usr/local/bin sccache
|
||||
elif [ $RUNNER_OS = "Windows" ]; then
|
||||
install_from_gh x86_64-pc-windows-msvc $USERPROFILE/.cargo/bin sccache.exe
|
||||
fi
|
||||
1
.github/ci.prop
vendored
Normal file
1
.github/ci.prop
vendored
Normal file
@@ -0,0 +1 @@
|
||||
abiList=arm64-v8a
|
||||
5
.github/kvm.sh
vendored
Executable file
5
.github/kvm.sh
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
260
.github/workflows/build.yml
vendored
260
.github/workflows/build.yml
vendored
@@ -2,107 +2,197 @@ name: Magisk Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'native/**'
|
||||
- 'stub/**'
|
||||
- 'buildSrc/**'
|
||||
- 'build.py'
|
||||
- 'gradle.properties'
|
||||
- '.github/workflows/build.yml'
|
||||
- "app/**"
|
||||
- "native/**"
|
||||
- "build.py"
|
||||
- ".github/workflows/build.yml"
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
name: Build Magisk artifacts
|
||||
runs-on: macos-26
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
is-asset-build: true
|
||||
|
||||
- name: Build release
|
||||
run: ./build.py -vr all
|
||||
|
||||
- name: Build debug
|
||||
run: ./build.py -v all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./app/gradlew --stop
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
compression-level: 9
|
||||
|
||||
- name: Upload mapping and native debug symbols
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ github.sha }}-symbols
|
||||
path: app/apk/build/outputs
|
||||
compression-level: 9
|
||||
|
||||
test-build:
|
||||
name: Test building on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
env:
|
||||
NDK_CCACHE: ccache
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||
RUSTC_WRAPPER: sccache
|
||||
os: [windows-2025, ubuntu-24.04]
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Test build
|
||||
run: python build.py -v -c .github/ci.prop all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./app/gradlew --stop
|
||||
|
||||
avd-test:
|
||||
name: Test API ${{ matrix.version }} (x86_64)
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 36.1, "CANARY"]
|
||||
type: [""]
|
||||
include:
|
||||
- version: "CinnamonBun"
|
||||
type: "google_apis_ps16k"
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Set up ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: ${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}
|
||||
|
||||
- name: Set up sccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
variant: sccache
|
||||
key: ${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
!~/.gradle/caches/build-cache-*
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Cache build cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches/build-cache-*
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}-build-cache-
|
||||
|
||||
- name: Set up NDK
|
||||
run: python build.py -v ndk
|
||||
|
||||
- name: Build release
|
||||
run: |
|
||||
python build.py -vr all
|
||||
|
||||
- name: Build debug
|
||||
run: |
|
||||
python build.py -v all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
|
||||
# Only upload artifacts built on Linux
|
||||
- name: Upload build artifact
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
|
||||
- name: Upload mapping and native debug symbols
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Enable KVM group perms
|
||||
run: .github/kvm.sh
|
||||
|
||||
- name: Run AVD test
|
||||
timeout-minutes: 18
|
||||
run: scripts/avd.sh test -l -v ${{ matrix.version }} -t ${{ matrix.type }}
|
||||
|
||||
- name: Upload logs on error
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ github.sha }}-symbols
|
||||
path: app/build/outputs
|
||||
name: "avd-logs-${{ matrix.version }}"
|
||||
path: |
|
||||
kernel.log
|
||||
logcat.log
|
||||
|
||||
avd-test-32:
|
||||
name: Test API ${{ matrix.version }} (x86)
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: [23, 24, 25, 26, 27, 28, 29, 30]
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
|
||||
- name: Enable KVM group perms
|
||||
run: .github/kvm.sh
|
||||
|
||||
- name: Run AVD test
|
||||
timeout-minutes: 18
|
||||
env:
|
||||
FORCE_32_BIT: 1
|
||||
run: scripts/avd.sh test -l -v ${{ matrix.version }}
|
||||
|
||||
- name: Upload logs on error
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: "avd32-logs-${{ matrix.version }}"
|
||||
path: |
|
||||
kernel.log
|
||||
logcat.log
|
||||
|
||||
cf-test:
|
||||
name: Test ${{ matrix.device }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
env:
|
||||
CF_HOME: /home/runner/aosp_cf_phone
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- branch: "aosp-android-latest-release"
|
||||
device: "aosp_cf_x86_64_only_phone"
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
|
||||
- name: Enable KVM group perms
|
||||
run: .github/kvm.sh
|
||||
|
||||
- name: Setup Cuttlefish environment
|
||||
run: |
|
||||
scripts/cuttlefish.sh setup
|
||||
scripts/cuttlefish.sh download ${{ matrix.branch }} ${{ matrix.device }}
|
||||
|
||||
- name: Run Cuttlefish test
|
||||
timeout-minutes: 18
|
||||
run: sudo -E -u $USER scripts/cuttlefish.sh test
|
||||
|
||||
- name: Upload logs on error
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: "cvd-logs-${{ matrix.device }}"
|
||||
path: |
|
||||
/home/runner/aosp_cf_phone/cuttlefish/instances/cvd-1/logs
|
||||
/home/runner/aosp_cf_phone/cuttlefish/instances/cvd-1/cuttlefish_config.json
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -2,17 +2,17 @@ out
|
||||
*.zip
|
||||
*.jks
|
||||
*.apk
|
||||
*.log
|
||||
/config.prop
|
||||
/update.sh
|
||||
/dict.txt
|
||||
/notes.md
|
||||
|
||||
# Built binaries
|
||||
native/out
|
||||
|
||||
# Android Studio / Gradle
|
||||
# Android Studio
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
/build
|
||||
/captures
|
||||
.idea
|
||||
.cursor
|
||||
ramdisk.img
|
||||
app/core/src/debug
|
||||
app/core/src/release
|
||||
|
||||
39
.gitmodules
vendored
39
.gitmodules
vendored
@@ -1,48 +1,21 @@
|
||||
[submodule "selinux"]
|
||||
path = native/src/external/selinux
|
||||
url = https://github.com/topjohnwu/selinux.git
|
||||
[submodule "busybox"]
|
||||
path = native/src/external/busybox
|
||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||
[submodule "dtc"]
|
||||
path = native/src/external/dtc
|
||||
url = https://github.com/dgibson/dtc.git
|
||||
[submodule "lz4"]
|
||||
path = native/src/external/lz4
|
||||
url = https://github.com/lz4/lz4.git
|
||||
[submodule "bzip2"]
|
||||
path = native/src/external/bzip2
|
||||
url = https://github.com/nemequ/bzip2.git
|
||||
[submodule "xz"]
|
||||
path = native/src/external/xz
|
||||
url = https://github.com/xz-mirror/xz.git
|
||||
[submodule "nanopb"]
|
||||
path = native/src/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/src/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "pcre"]
|
||||
path = native/src/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
[submodule "libcxx"]
|
||||
path = native/src/external/libcxx
|
||||
url = https://github.com/topjohnwu/libcxx.git
|
||||
[submodule "zlib"]
|
||||
path = native/src/external/zlib
|
||||
url = https://android.googlesource.com/platform/external/zlib
|
||||
[submodule "parallel-hashmap"]
|
||||
path = native/src/external/parallel-hashmap
|
||||
url = https://github.com/greg7mdp/parallel-hashmap.git
|
||||
[submodule "zopfli"]
|
||||
path = native/src/external/zopfli
|
||||
url = https://github.com/google/zopfli.git
|
||||
[submodule "cxx-rs"]
|
||||
path = native/src/external/cxx-rs
|
||||
url = https://github.com/topjohnwu/cxx.git
|
||||
[submodule "lsplt"]
|
||||
path = native/src/external/lsplt
|
||||
url = https://github.com/LSPosed/LSPlt.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
[submodule "system_properties"]
|
||||
path = native/src/external/system_properties
|
||||
url = https://github.com/topjohnwu/system_properties.git
|
||||
[submodule "crt0"]
|
||||
path = native/src/external/crt0
|
||||
url = https://github.com/topjohnwu/crt0.git
|
||||
|
||||
37
README.MD
37
README.MD
@@ -16,17 +16,14 @@ Some highlight features:
|
||||
|
||||
## Downloads
|
||||
|
||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.2)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.2)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
[Github](https://github.com/topjohnwu/Magisk/releases) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||
- [Building and Development](https://topjohnwu.github.io/Magisk/build.html)
|
||||
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||
- [Zygisk module sample](https://github.com/topjohnwu/zygisk-module-sample)
|
||||
|
||||
## Bug Reports
|
||||
|
||||
@@ -36,36 +33,12 @@ For installation issues, upload both boot image and install logs.<br>
|
||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||
|
||||
## Building and Development
|
||||
|
||||
- Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups.
|
||||
- Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
||||
- Install Python 3.8+ \
|
||||
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
|
||||
- Configure to use the JDK bundled in Android Studio:
|
||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
|
||||
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
||||
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||
- To start building, run `build.py` to see your options. \
|
||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native sources.
|
||||
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
||||
|
||||
## Signing and Distribution
|
||||
|
||||
- The certificate of the key used to sign the final Magisk APK product is also directly embedded into some executables. In release builds, Magisk's root daemon will enforce this certificate check and reject and forcefully uninstall any non-matching Magisk apps to protect users from malicious and unverified Magisk APKs.
|
||||
- To do any development on Magisk itself, switch to an **official debug build and reinstall Magisk** to bypass the signature check.
|
||||
- To distribute your own Magisk builds signed with your own keys, set your signing configs in `config.prop`.
|
||||
- Check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key) for more details on generating your own key.
|
||||
|
||||
## Translation Contributions
|
||||
|
||||
Default string resources for the Magisk app and its stub APK are located here:
|
||||
|
||||
- `app/src/main/res/values/strings.xml`
|
||||
- `stub/src/main/res/values/strings.xml`
|
||||
- `app/core/src/main/res/values/strings.xml`
|
||||
- `app/stub/src/main/res/values/strings.xml`
|
||||
|
||||
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
|
||||
|
||||
|
||||
14
app/.gitignore
vendored
14
app/.gitignore
vendored
@@ -1,11 +1,7 @@
|
||||
*.iml
|
||||
/dict.txt
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
.kotlin
|
||||
build
|
||||
/local.properties
|
||||
.idea/
|
||||
/build
|
||||
*.hprof
|
||||
.externalNativeBuild/
|
||||
*.apk
|
||||
src/*/assets
|
||||
src/*/jniLibs
|
||||
src/*/resources
|
||||
|
||||
61
app/apk-ng/build.gradle.kts
Normal file
61
app/apk-ng/build.gradle.kts
Normal file
@@ -0,0 +1,61 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("plugin.compose")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
setupMainApk()
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
excludes += "lib/*/libandroidx.graphics.path.so"
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
proguardFile("proguard-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
coreLibraryDesugaring(libs.jdk.libs)
|
||||
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
|
||||
// Compose
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.material.icons.extended)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.lifecycle.viewmodel.compose)
|
||||
implementation(libs.compose.material3)
|
||||
|
||||
// Navigation3
|
||||
implementation(libs.navigation3.runtime)
|
||||
implementation(libs.navigationevent.compose)
|
||||
implementation(libs.lifecycle.viewmodel.navigation3)
|
||||
implementation(libs.navigation3.ui)
|
||||
|
||||
}
|
||||
3
app/apk-ng/proguard-rules.pro
vendored
Normal file
3
app/apk-ng/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Excessive obfuscation
|
||||
-flattenpackagehierarchy
|
||||
-allowaccessmodification
|
||||
34
app/apk-ng/src/main/AndroidManifest.xml
Normal file
34
app/apk-ng/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application android:localeConfig="@xml/locale_config">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/SplashTheme">
|
||||
<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.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.surequest.SuRequestActivity"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:taskAffinity=""
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class AsyncLoadViewModel : BaseViewModel() {
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
|
||||
@MainThread
|
||||
fun startLoading() {
|
||||
if (loadingJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun reload() {
|
||||
loadingJob?.cancel()
|
||||
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||
}
|
||||
|
||||
protected abstract suspend fun doLoadWork()
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
private val _navEvents = MutableSharedFlow<Route>(extraBufferCapacity = 1)
|
||||
val navEvents: SharedFlow<Route> = _navEvents
|
||||
|
||||
open fun onSaveState(state: Bundle) {}
|
||||
open fun onRestoreState(state: Bundle) {}
|
||||
|
||||
fun showSnackbar(@StringRes resId: Int) {
|
||||
AppContext.toast(resId, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
fun showSnackbar(msg: String) {
|
||||
AppContext.toast(msg, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
fun navigateTo(route: Route) {
|
||||
_navEvents.tryEmit(route)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
|
||||
object VMFactory : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when (modelClass) {
|
||||
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||
InstallViewModel::class.java ->
|
||||
InstallViewModel(ServiceLocator.networkService)
|
||||
SuRequestViewModel::class.java ->
|
||||
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||
else -> modelClass.newInstance()
|
||||
} as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* A circular buffer of [TerminalRow]s which keeps notes about what is visible on a logical screen and the scroll
|
||||
* history.
|
||||
*
|
||||
* See [externalToInternalRow] for how to map from logical screen rows to array indices.
|
||||
*/
|
||||
class TerminalBuffer(columns: Int, totalRows: Int, screenRows: Int) {
|
||||
|
||||
var lines: Array<TerminalRow?>
|
||||
|
||||
/** The length of [lines]. */
|
||||
var totalRows: Int = totalRows
|
||||
private set
|
||||
|
||||
/** The number of rows and columns visible on the screen. */
|
||||
var screenRows: Int = screenRows
|
||||
|
||||
var columns: Int = columns
|
||||
|
||||
/** The number of rows kept in history. */
|
||||
var activeTranscriptRows: Int = 0
|
||||
private set
|
||||
|
||||
/** The index in the circular buffer where the visible screen starts. */
|
||||
private var screenFirstRow = 0
|
||||
|
||||
init {
|
||||
lines = arrayOfNulls(totalRows)
|
||||
blockSet(0, 0, columns, screenRows, ' '.code, TextStyle.NORMAL)
|
||||
}
|
||||
|
||||
val transcriptText: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows).trim()
|
||||
|
||||
val transcriptTextWithoutJoinedLines: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows, false).trim()
|
||||
|
||||
val transcriptTextWithFullLinesJoined: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows, joinBackLines = true, joinFullLines = true).trim()
|
||||
|
||||
fun getSelectedText(selX1: Int, selY1: Int, selX2: Int, selY2: Int, joinBackLines: Boolean = true, joinFullLines: Boolean = false): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
var y1 = selY1
|
||||
var y2 = selY2
|
||||
if (y1 < -activeTranscriptRows) y1 = -activeTranscriptRows
|
||||
if (y2 >= screenRows) y2 = screenRows - 1
|
||||
|
||||
for (row in y1..y2) {
|
||||
val x1 = if (row == y1) selX1 else 0
|
||||
var x2: Int
|
||||
if (row == y2) {
|
||||
x2 = selX2 + 1
|
||||
if (x2 > columns) x2 = columns
|
||||
} else {
|
||||
x2 = columns
|
||||
}
|
||||
val lineObject = lines[externalToInternalRow(row)]!!
|
||||
val x1Index = lineObject.findStartOfColumn(x1)
|
||||
var x2Index = if (x2 < columns) lineObject.findStartOfColumn(x2) else lineObject.spaceUsed
|
||||
if (x2Index == x1Index) {
|
||||
x2Index = lineObject.findStartOfColumn(x2 + 1)
|
||||
}
|
||||
val line = lineObject.text
|
||||
var lastPrintingCharIndex = -1
|
||||
val rowLineWrap = getLineWrap(row)
|
||||
if (rowLineWrap && x2 == columns) {
|
||||
lastPrintingCharIndex = x2Index - 1
|
||||
} else {
|
||||
for (i in x1Index until x2Index) {
|
||||
val c = line[i]
|
||||
if (c != ' ') lastPrintingCharIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
val len = lastPrintingCharIndex - x1Index + 1
|
||||
if (lastPrintingCharIndex != -1 && len > 0)
|
||||
builder.append(line, x1Index, len)
|
||||
|
||||
val lineFillsWidth = lastPrintingCharIndex == x2Index - 1
|
||||
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||
&& row < y2 && row < screenRows - 1) builder.append('\n')
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun getWordAtLocation(x: Int, y: Int): String {
|
||||
var y1 = y
|
||||
var y2 = y
|
||||
while (y1 > 0 && !getSelectedText(0, y1 - 1, columns, y, joinBackLines = true, joinFullLines = true).contains("\n")) {
|
||||
y1--
|
||||
}
|
||||
while (y2 < screenRows && !getSelectedText(0, y, columns, y2 + 1, joinBackLines = true, joinFullLines = true).contains("\n")) {
|
||||
y2++
|
||||
}
|
||||
|
||||
val text = getSelectedText(0, y1, columns, y2, joinBackLines = true, joinFullLines = true)
|
||||
val textOffset = (y - y1) * columns + x
|
||||
|
||||
if (textOffset >= text.length) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val x1 = text.lastIndexOf(' ', textOffset)
|
||||
var x2 = text.indexOf(' ', textOffset)
|
||||
if (x2 == -1) {
|
||||
x2 = text.length
|
||||
}
|
||||
|
||||
if (x1 == x2) {
|
||||
return ""
|
||||
}
|
||||
return text.substring(x1 + 1, x2)
|
||||
}
|
||||
|
||||
val activeRows: Int get() = activeTranscriptRows + screenRows
|
||||
|
||||
/**
|
||||
* Convert a row value from the public external coordinate system to our internal private coordinate system.
|
||||
*
|
||||
* ```
|
||||
* - External coordinate system: -activeTranscriptRows to screenRows-1, with the screen being 0..screenRows-1.
|
||||
* - Internal coordinate system: the screenRows lines starting at screenFirstRow comprise the screen, while the
|
||||
* activeTranscriptRows lines ending at screenFirstRow-1 form the transcript (as a circular buffer).
|
||||
*
|
||||
* External <-> Internal:
|
||||
*
|
||||
* [ ... ] [ ... ]
|
||||
* [ -activeTranscriptRows ] [ screenFirstRow - activeTranscriptRows ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ 0 (visible screen starts here) ] <-> [ screenFirstRow ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ screenRows-1 ] [ screenFirstRow + screenRows-1 ]
|
||||
* ```
|
||||
*
|
||||
* @param externalRow a row in the external coordinate system.
|
||||
* @return The row corresponding to the input argument in the private coordinate system.
|
||||
*/
|
||||
fun externalToInternalRow(externalRow: Int): Int {
|
||||
if (externalRow < -activeTranscriptRows || externalRow > screenRows)
|
||||
throw IllegalArgumentException("extRow=$externalRow, screenRows=$screenRows, activeTranscriptRows=$activeTranscriptRows")
|
||||
val internalRow = screenFirstRow + externalRow
|
||||
return if (internalRow < 0) (totalRows + internalRow) else (internalRow % totalRows)
|
||||
}
|
||||
|
||||
fun setLineWrap(row: Int) {
|
||||
lines[externalToInternalRow(row)]!!.lineWrap = true
|
||||
}
|
||||
|
||||
fun getLineWrap(row: Int): Boolean {
|
||||
return lines[externalToInternalRow(row)]!!.lineWrap
|
||||
}
|
||||
|
||||
fun clearLineWrap(row: Int) {
|
||||
lines[externalToInternalRow(row)]!!.lineWrap = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
||||
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
||||
*
|
||||
* @param newColumns The number of columns the screen should have.
|
||||
* @param newRows The number of rows the screen should have.
|
||||
* @param cursor An int[2] containing the (column, row) cursor location.
|
||||
*/
|
||||
fun resize(newColumns: Int, newRows: Int, newTotalRows: Int, cursor: IntArray, currentStyle: Long, altScreen: Boolean) {
|
||||
// newRows > totalRows should not normally happen since totalRows is TRANSCRIPT_ROWS (10000):
|
||||
if (newColumns == columns && newRows <= totalRows) {
|
||||
// Fast resize where just the rows changed.
|
||||
var shiftDownOfTopRow = screenRows - newRows
|
||||
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < screenRows) {
|
||||
// Shrinking. Check if we can skip blank rows at bottom below cursor.
|
||||
for (i in screenRows - 1 downTo 1) {
|
||||
if (cursor[1] >= i) break
|
||||
val r = externalToInternalRow(i)
|
||||
if (lines[r] == null || lines[r]!!.isBlank()) {
|
||||
if (--shiftDownOfTopRow == 0) break
|
||||
}
|
||||
}
|
||||
} else if (shiftDownOfTopRow < 0) {
|
||||
// Negative shift down = expanding. Only move screen up if there is transcript to show:
|
||||
val actualShift = maxOf(shiftDownOfTopRow, -activeTranscriptRows)
|
||||
if (shiftDownOfTopRow != actualShift) {
|
||||
for (i in 0 until actualShift - shiftDownOfTopRow)
|
||||
allocateFullLineIfNecessary((screenFirstRow + screenRows + i) % totalRows).clear(currentStyle)
|
||||
shiftDownOfTopRow = actualShift
|
||||
}
|
||||
}
|
||||
screenFirstRow += shiftDownOfTopRow
|
||||
screenFirstRow = if (screenFirstRow < 0) (screenFirstRow + totalRows) else (screenFirstRow % totalRows)
|
||||
totalRows = newTotalRows
|
||||
activeTranscriptRows = if (altScreen) 0 else maxOf(0, activeTranscriptRows + shiftDownOfTopRow)
|
||||
cursor[1] -= shiftDownOfTopRow
|
||||
screenRows = newRows
|
||||
} else {
|
||||
// Copy away old state and update new:
|
||||
val oldLines = lines
|
||||
lines = arrayOfNulls(newTotalRows)
|
||||
for (i in 0 until newTotalRows)
|
||||
lines[i] = TerminalRow(newColumns, currentStyle)
|
||||
|
||||
val oldActiveTranscriptRows = activeTranscriptRows
|
||||
val oldScreenFirstRow = screenFirstRow
|
||||
val oldScreenRows = screenRows
|
||||
val oldTotalRows = totalRows
|
||||
totalRows = newTotalRows
|
||||
screenRows = newRows
|
||||
activeTranscriptRows = 0
|
||||
screenFirstRow = 0
|
||||
columns = newColumns
|
||||
|
||||
var newCursorRow = -1
|
||||
var newCursorColumn = -1
|
||||
val oldCursorRow = cursor[1]
|
||||
val oldCursorColumn = cursor[0]
|
||||
var newCursorPlaced = false
|
||||
|
||||
var currentOutputExternalRow = 0
|
||||
var currentOutputExternalColumn = 0
|
||||
|
||||
var skippedBlankLines = 0
|
||||
for (externalOldRow in -oldActiveTranscriptRows until oldScreenRows) {
|
||||
var internalOldRow = oldScreenFirstRow + externalOldRow
|
||||
internalOldRow = if (internalOldRow < 0) (oldTotalRows + internalOldRow) else (internalOldRow % oldTotalRows)
|
||||
|
||||
val oldLine = oldLines[internalOldRow]
|
||||
val cursorAtThisRow = externalOldRow == oldCursorRow
|
||||
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
|
||||
skippedBlankLines++
|
||||
continue
|
||||
} else if (skippedBlankLines > 0) {
|
||||
for (i in 0 until skippedBlankLines) {
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
skippedBlankLines = 0
|
||||
}
|
||||
|
||||
var lastNonSpaceIndex = 0
|
||||
var justToCursor = false
|
||||
if (cursorAtThisRow || oldLine.lineWrap) {
|
||||
lastNonSpaceIndex = oldLine.spaceUsed
|
||||
if (cursorAtThisRow) justToCursor = true
|
||||
} else {
|
||||
for (i in 0 until oldLine.spaceUsed)
|
||||
// NEWLY INTRODUCED BUG! Should not index oldLine.styles with char indices
|
||||
if (oldLine.text[i] != ' '/* || oldLine.styles[i] != currentStyle */)
|
||||
lastNonSpaceIndex = i + 1
|
||||
}
|
||||
|
||||
var currentOldCol = 0
|
||||
var styleAtCol = 0L
|
||||
var i = 0
|
||||
while (i < lastNonSpaceIndex) {
|
||||
val c = oldLine.text[i]
|
||||
val codePoint: Int
|
||||
if (Character.isHighSurrogate(c)) {
|
||||
i++
|
||||
codePoint = Character.toCodePoint(c, oldLine.text[i])
|
||||
} else {
|
||||
codePoint = c.code
|
||||
}
|
||||
val displayWidth = WcWidth.width(codePoint)
|
||||
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol)
|
||||
|
||||
if (currentOutputExternalColumn + displayWidth > columns) {
|
||||
setLineWrap(currentOutputExternalRow)
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
|
||||
val offsetDueToCombiningChar = if (displayWidth <= 0 && currentOutputExternalColumn > 0) 1 else 0
|
||||
val outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar
|
||||
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol)
|
||||
|
||||
if (displayWidth > 0) {
|
||||
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
|
||||
newCursorColumn = currentOutputExternalColumn
|
||||
newCursorRow = currentOutputExternalRow
|
||||
newCursorPlaced = true
|
||||
}
|
||||
currentOldCol += displayWidth
|
||||
currentOutputExternalColumn += displayWidth
|
||||
if (justToCursor && newCursorPlaced) break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (externalOldRow != (oldScreenRows - 1) && !oldLine.lineWrap) {
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
}
|
||||
|
||||
cursor[0] = newCursorColumn
|
||||
cursor[1] = newCursorRow
|
||||
}
|
||||
|
||||
// Handle cursor scrolling off screen:
|
||||
if (cursor[0] < 0 || cursor[1] < 0) {
|
||||
cursor[0] = 0
|
||||
cursor[1] = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
|
||||
* into account.
|
||||
*
|
||||
* @param srcInternal The first line to be copied.
|
||||
* @param len The number of lines to be copied.
|
||||
*/
|
||||
private fun blockCopyLinesDown(srcInternal: Int, len: Int) {
|
||||
if (len == 0) return
|
||||
|
||||
val start = len - 1
|
||||
val lineToBeOverWritten = lines[(srcInternal + start + 1) % totalRows]
|
||||
for (i in start downTo 0)
|
||||
lines[(srcInternal + i + 1) % totalRows] = lines[(srcInternal + i) % totalRows]
|
||||
lines[srcInternal % totalRows] = lineToBeOverWritten
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
|
||||
*
|
||||
* @param topMargin First line that is scrolled.
|
||||
* @param bottomMargin One line after the last line that is scrolled.
|
||||
* @param style the style for the newly exposed line.
|
||||
*/
|
||||
fun scrollDownOneLine(topMargin: Int, bottomMargin: Int, style: Long) {
|
||||
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > screenRows)
|
||||
throw IllegalArgumentException("topMargin=$topMargin, bottomMargin=$bottomMargin, screenRows=$screenRows")
|
||||
|
||||
blockCopyLinesDown(screenFirstRow, topMargin)
|
||||
blockCopyLinesDown(externalToInternalRow(bottomMargin), screenRows - bottomMargin)
|
||||
|
||||
screenFirstRow = (screenFirstRow + 1) % totalRows
|
||||
if (activeTranscriptRows < totalRows - screenRows) activeTranscriptRows++
|
||||
|
||||
val blankRow = externalToInternalRow(bottomMargin - 1)
|
||||
if (lines[blankRow] == null) {
|
||||
lines[blankRow] = TerminalRow(columns, style)
|
||||
} else {
|
||||
lines[blankRow]!!.clear(style)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters
|
||||
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
|
||||
* be thrown.
|
||||
*
|
||||
* @param sx source X coordinate
|
||||
* @param sy source Y coordinate
|
||||
* @param w width
|
||||
* @param h height
|
||||
* @param dx destination X coordinate
|
||||
* @param dy destination Y coordinate
|
||||
*/
|
||||
fun blockCopy(sx: Int, sy: Int, w: Int, h: Int, dx: Int, dy: Int) {
|
||||
if (w == 0) return
|
||||
if (sx < 0 || sx + w > columns || sy < 0 || sy + h > screenRows || dx < 0 || dx + w > columns || dy < 0 || dy + h > screenRows)
|
||||
throw IllegalArgumentException()
|
||||
val copyingUp = sy > dy
|
||||
for (y in 0 until h) {
|
||||
val y2 = if (copyingUp) y else (h - (y + 1))
|
||||
val sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2))
|
||||
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block set characters. All characters must be within the bounds of the screen, or else an
|
||||
* InvalidParameterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
|
||||
* of characters.
|
||||
*/
|
||||
fun blockSet(sx: Int, sy: Int, w: Int, h: Int, `val`: Int, style: Long) {
|
||||
if (sx < 0 || sx + w > columns || sy < 0 || sy + h > screenRows) {
|
||||
throw IllegalArgumentException(
|
||||
"Illegal arguments! blockSet($sx, $sy, $w, $h, $`val`, $columns, $screenRows)")
|
||||
}
|
||||
for (y in 0 until h)
|
||||
for (x in 0 until w)
|
||||
setChar(sx + x, sy + y, `val`, style)
|
||||
}
|
||||
|
||||
fun allocateFullLineIfNecessary(row: Int): TerminalRow {
|
||||
return lines[row] ?: TerminalRow(columns, 0).also { lines[row] = it }
|
||||
}
|
||||
|
||||
fun setChar(column: Int, row: Int, codePoint: Int, style: Long) {
|
||||
if (row < 0 || row >= screenRows || column < 0 || column >= columns)
|
||||
throw IllegalArgumentException("TerminalBuffer.setChar(): row=$row, column=$column, screenRows=$screenRows, columns=$columns")
|
||||
val internalRow = externalToInternalRow(row)
|
||||
allocateFullLineIfNecessary(internalRow).setChar(column, codePoint, style)
|
||||
}
|
||||
|
||||
fun getStyleAt(externalRow: Int, column: Int): Long {
|
||||
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column)
|
||||
}
|
||||
|
||||
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
|
||||
fun setOrClearEffect(bits: Int, setOrClear: Boolean, reverse: Boolean, rectangular: Boolean, leftMargin: Int, rightMargin: Int, top: Int, left: Int,
|
||||
bottom: Int, right: Int) {
|
||||
for (y in top until bottom) {
|
||||
val line = lines[externalToInternalRow(y)]!!
|
||||
val startOfLine = if (rectangular || y == top) left else leftMargin
|
||||
val endOfLine = if (rectangular || y + 1 == bottom) right else rightMargin
|
||||
for (x in startOfLine until endOfLine) {
|
||||
val currentStyle = line.getStyle(x)
|
||||
val foreColor = TextStyle.decodeForeColor(currentStyle)
|
||||
val backColor = TextStyle.decodeBackColor(currentStyle)
|
||||
var effect = TextStyle.decodeEffect(currentStyle)
|
||||
if (reverse) {
|
||||
effect = (effect and bits.inv()) or (bits and effect.inv())
|
||||
} else if (setOrClear) {
|
||||
effect = effect or bits
|
||||
} else {
|
||||
effect = effect and bits.inv()
|
||||
}
|
||||
line.styles[x] = TextStyle.encode(foreColor, backColor, effect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearTranscript() {
|
||||
if (screenFirstRow < activeTranscriptRows) {
|
||||
Arrays.fill(lines, totalRows + screenFirstRow - activeTranscriptRows, totalRows, null)
|
||||
Arrays.fill(lines, 0, screenFirstRow, null)
|
||||
} else {
|
||||
Arrays.fill(lines, screenFirstRow - activeTranscriptRows, screenFirstRow, null)
|
||||
}
|
||||
activeTranscriptRows = 0
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
|
||||
private val busyboxPath: String by lazy {
|
||||
Shell.cmd("readlink /proc/self/exe").exec().out.firstOrNull()
|
||||
?: "/data/adb/magisk/busybox"
|
||||
}
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun TerminalEmulator.appendOnMain(bytes: ByteArray, len: Int) {
|
||||
mainHandler.post {
|
||||
append(bytes, len)
|
||||
onScreenUpdate?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun TerminalEmulator.appendLineOnMain(line: String) {
|
||||
val bytes = "$line\r\n".toByteArray(Charsets.UTF_8)
|
||||
appendOnMain(bytes, bytes.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command as root inside a PTY (via busybox script).
|
||||
* Reads raw bytes from the process and feeds them to the terminal emulator.
|
||||
* Must be called from a background thread.
|
||||
* Returns true if the process exits with code 0.
|
||||
*/
|
||||
fun runSuCommand(emulator: TerminalEmulator, command: String): Boolean {
|
||||
return try {
|
||||
val cols = emulator.mColumns
|
||||
val rows = emulator.mRows
|
||||
val wrappedCmd = "export TERM=xterm-256color; stty cols $cols rows $rows 2>/dev/null; $command"
|
||||
val escapedCmd = wrappedCmd.replace("'", "'\\''")
|
||||
|
||||
val process = ProcessBuilder(
|
||||
"su", "-c",
|
||||
"$busyboxPath script -q -c '$escapedCmd' /dev/null"
|
||||
).redirectErrorStream(true).start()
|
||||
|
||||
process.outputStream.close()
|
||||
|
||||
val buffer = ByteArray(4096)
|
||||
process.inputStream.use { input ->
|
||||
while (true) {
|
||||
val n = input.read(buffer)
|
||||
if (n == -1) break
|
||||
emulator.appendOnMain(buffer.copyOf(n), n)
|
||||
}
|
||||
}
|
||||
|
||||
process.waitFor() == 0
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "runSuCommand failed")
|
||||
emulator.appendLineOnMain("! Error: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* A row in a terminal, composed of a fixed number of cells.
|
||||
*
|
||||
* The text in the row is stored in a char[] array, [text], for quick access during rendering.
|
||||
*/
|
||||
class TerminalRow(private val columns: Int, style: Long) {
|
||||
|
||||
/**
|
||||
* Max combining characters that can exist in a column, that are separate from the base character
|
||||
* itself. Any additional combining characters will be ignored and not added to the column.
|
||||
*
|
||||
* There does not seem to be limit in unicode standard for max number of combination characters
|
||||
* that can be combined but such characters are primarily under 10.
|
||||
*
|
||||
* "Section 3.6 Combination" of unicode standard contains combining characters info.
|
||||
* - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
|
||||
* - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
|
||||
* - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
|
||||
*
|
||||
* UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
|
||||
* > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
|
||||
* > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
|
||||
* > yet is well within the buffer size limits of practical implementations.
|
||||
* - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
|
||||
* - https://stackoverflow.com/a/11983435/14686958
|
||||
*
|
||||
* We choose the value 15 because it should be enough for terminal based applications and keep
|
||||
* the memory usage low for a terminal row, won't affect performance or cause terminal to
|
||||
* lag or hang, and will keep malicious applications from causing harm. The value can be
|
||||
* increased if ever needed for legitimate applications.
|
||||
*/
|
||||
companion object {
|
||||
private const val SPARE_CAPACITY_FACTOR = 1.5f
|
||||
private const val MAX_COMBINING_CHARACTERS_PER_COLUMN = 15
|
||||
}
|
||||
|
||||
/** The text filling this terminal row. */
|
||||
var text: CharArray = CharArray((SPARE_CAPACITY_FACTOR * columns).toInt())
|
||||
|
||||
/** The number of java chars used in [text]. */
|
||||
private var _spaceUsed: Short = 0
|
||||
|
||||
/** If this row has been line wrapped due to text output at the end of line. */
|
||||
var lineWrap: Boolean = false
|
||||
|
||||
/** The style bits of each cell in the row. See [TextStyle]. */
|
||||
val styles: LongArray = LongArray(columns)
|
||||
|
||||
/** If this row might contain chars with width != 1, used for deactivating fast path */
|
||||
var hasNonOneWidthOrSurrogateChars: Boolean = false
|
||||
|
||||
init {
|
||||
clear(style)
|
||||
}
|
||||
|
||||
/** NOTE: The sourceX2 is exclusive. */
|
||||
fun copyInterval(line: TerminalRow, sourceX1: Int, sourceX2: Int, destinationX: Int) {
|
||||
hasNonOneWidthOrSurrogateChars = hasNonOneWidthOrSurrogateChars or line.hasNonOneWidthOrSurrogateChars
|
||||
val x1 = line.findStartOfColumn(sourceX1)
|
||||
val x2 = line.findStartOfColumn(sourceX2)
|
||||
var startingFromSecondHalfOfWideChar = sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1)
|
||||
val sourceChars = if (this === line) line.text.copyOf() else line.text
|
||||
var latestNonCombiningWidth = 0
|
||||
var destX = destinationX
|
||||
var srcX1 = sourceX1
|
||||
var i = x1
|
||||
while (i < x2) {
|
||||
val sourceChar = sourceChars[i]
|
||||
var codePoint: Int
|
||||
if (Character.isHighSurrogate(sourceChar)) {
|
||||
i++
|
||||
codePoint = Character.toCodePoint(sourceChar, sourceChars[i])
|
||||
} else {
|
||||
codePoint = sourceChar.code
|
||||
}
|
||||
if (startingFromSecondHalfOfWideChar) {
|
||||
codePoint = ' '.code
|
||||
startingFromSecondHalfOfWideChar = false
|
||||
}
|
||||
val w = WcWidth.width(codePoint)
|
||||
if (w > 0) {
|
||||
destX += latestNonCombiningWidth
|
||||
srcX1 += latestNonCombiningWidth
|
||||
latestNonCombiningWidth = w
|
||||
}
|
||||
setChar(destX, codePoint, line.getStyle(srcX1))
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
val spaceUsed: Int get() = _spaceUsed.toInt()
|
||||
|
||||
/** Note that the column may end of second half of wide character. */
|
||||
fun findStartOfColumn(column: Int): Int {
|
||||
if (column == columns) return spaceUsed
|
||||
|
||||
var currentColumn = 0
|
||||
var currentCharIndex = 0
|
||||
while (true) {
|
||||
var newCharIndex = currentCharIndex
|
||||
val c = text[newCharIndex++]
|
||||
val isHigh = Character.isHighSurrogate(c)
|
||||
val codePoint = if (isHigh) Character.toCodePoint(c, text[newCharIndex++]) else c.code
|
||||
val wcwidth = WcWidth.width(codePoint)
|
||||
if (wcwidth > 0) {
|
||||
currentColumn += wcwidth
|
||||
if (currentColumn == column) {
|
||||
while (newCharIndex < _spaceUsed) {
|
||||
if (Character.isHighSurrogate(text[newCharIndex])) {
|
||||
if (WcWidth.width(Character.toCodePoint(text[newCharIndex], text[newCharIndex + 1])) <= 0) {
|
||||
newCharIndex += 2
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if (WcWidth.width(text[newCharIndex].code) <= 0) {
|
||||
newCharIndex++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return newCharIndex
|
||||
} else if (currentColumn > column) {
|
||||
return currentCharIndex
|
||||
}
|
||||
}
|
||||
currentCharIndex = newCharIndex
|
||||
}
|
||||
}
|
||||
|
||||
private fun wideDisplayCharacterStartingAt(column: Int): Boolean {
|
||||
var currentCharIndex = 0
|
||||
var currentColumn = 0
|
||||
while (currentCharIndex < _spaceUsed) {
|
||||
val c = text[currentCharIndex++]
|
||||
val codePoint = if (Character.isHighSurrogate(c)) Character.toCodePoint(c, text[currentCharIndex++]) else c.code
|
||||
val wcwidth = WcWidth.width(codePoint)
|
||||
if (wcwidth > 0) {
|
||||
if (currentColumn == column && wcwidth == 2) return true
|
||||
currentColumn += wcwidth
|
||||
if (currentColumn > column) return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun clear(style: Long) {
|
||||
Arrays.fill(text, ' ')
|
||||
Arrays.fill(styles, style)
|
||||
_spaceUsed = columns.toShort()
|
||||
hasNonOneWidthOrSurrogateChars = false
|
||||
}
|
||||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
fun setChar(columnToSet: Int, codePoint: Int, style: Long) {
|
||||
if (columnToSet < 0 || columnToSet >= styles.size)
|
||||
throw IllegalArgumentException("TerminalRow.setChar(): columnToSet=$columnToSet, codePoint=$codePoint, style=$style")
|
||||
|
||||
styles[columnToSet] = style
|
||||
|
||||
val newCodePointDisplayWidth = WcWidth.width(codePoint)
|
||||
|
||||
// Fast path when we don't have any chars with width != 1
|
||||
if (!hasNonOneWidthOrSurrogateChars) {
|
||||
if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT || newCodePointDisplayWidth != 1) {
|
||||
hasNonOneWidthOrSurrogateChars = true
|
||||
} else {
|
||||
text[columnToSet] = codePoint.toChar()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val newIsCombining = newCodePointDisplayWidth <= 0
|
||||
|
||||
val wasExtraColForWideChar = columnToSet > 0 && wideDisplayCharacterStartingAt(columnToSet - 1)
|
||||
|
||||
var col = columnToSet
|
||||
if (newIsCombining) {
|
||||
if (wasExtraColForWideChar) col--
|
||||
} else {
|
||||
if (wasExtraColForWideChar) setChar(col - 1, ' '.code, style)
|
||||
val overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(col + 1)
|
||||
if (overwritingWideCharInNextColumn) setChar(col + 1, ' '.code, style)
|
||||
}
|
||||
|
||||
var textArray = text
|
||||
val oldStartOfColumnIndex = findStartOfColumn(col)
|
||||
val oldCodePointDisplayWidth = WcWidth.width(textArray, oldStartOfColumnIndex)
|
||||
|
||||
val oldCharactersUsedForColumn: Int
|
||||
if (col + oldCodePointDisplayWidth < columns) {
|
||||
val oldEndOfColumnIndex = findStartOfColumn(col + oldCodePointDisplayWidth)
|
||||
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex
|
||||
} else {
|
||||
oldCharactersUsedForColumn = _spaceUsed - oldStartOfColumnIndex
|
||||
}
|
||||
|
||||
if (newIsCombining) {
|
||||
val combiningCharsCount = WcWidth.zeroWidthCharsCount(textArray, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn)
|
||||
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
|
||||
return
|
||||
}
|
||||
|
||||
var newCharactersUsedForColumn = Character.charCount(codePoint)
|
||||
if (newIsCombining) {
|
||||
newCharactersUsedForColumn += oldCharactersUsedForColumn
|
||||
}
|
||||
|
||||
val oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn
|
||||
val newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn
|
||||
|
||||
val javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn
|
||||
if (javaCharDifference > 0) {
|
||||
val oldCharactersAfterColumn = _spaceUsed - oldNextColumnIndex
|
||||
if (_spaceUsed + javaCharDifference > textArray.size) {
|
||||
val newText = CharArray(textArray.size + columns)
|
||||
System.arraycopy(textArray, 0, newText, 0, oldNextColumnIndex)
|
||||
System.arraycopy(textArray, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn)
|
||||
text = newText
|
||||
textArray = newText
|
||||
} else {
|
||||
System.arraycopy(textArray, oldNextColumnIndex, textArray, newNextColumnIndex, oldCharactersAfterColumn)
|
||||
}
|
||||
} else if (javaCharDifference < 0) {
|
||||
System.arraycopy(textArray, oldNextColumnIndex, textArray, newNextColumnIndex, _spaceUsed - oldNextColumnIndex)
|
||||
}
|
||||
_spaceUsed = (_spaceUsed + javaCharDifference).toShort()
|
||||
|
||||
Character.toChars(codePoint, textArray, oldStartOfColumnIndex + if (newIsCombining) oldCharactersUsedForColumn else 0)
|
||||
|
||||
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||
if (_spaceUsed + 1 > textArray.size) {
|
||||
val newText = CharArray(textArray.size + columns)
|
||||
System.arraycopy(textArray, 0, newText, 0, newNextColumnIndex)
|
||||
System.arraycopy(textArray, newNextColumnIndex, newText, newNextColumnIndex + 1, _spaceUsed - newNextColumnIndex)
|
||||
text = newText
|
||||
textArray = newText
|
||||
} else {
|
||||
System.arraycopy(textArray, newNextColumnIndex, textArray, newNextColumnIndex + 1, _spaceUsed - newNextColumnIndex)
|
||||
}
|
||||
textArray[newNextColumnIndex] = ' '
|
||||
++_spaceUsed
|
||||
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
|
||||
if (col == columns - 1) {
|
||||
throw IllegalArgumentException("Cannot put wide character in last column")
|
||||
} else if (col == columns - 2) {
|
||||
_spaceUsed = newNextColumnIndex.toShort()
|
||||
} else {
|
||||
val newNextNextColumnIndex = newNextColumnIndex + if (Character.isHighSurrogate(textArray[newNextColumnIndex])) 2 else 1
|
||||
val nextLen = newNextNextColumnIndex - newNextColumnIndex
|
||||
System.arraycopy(textArray, newNextNextColumnIndex, textArray, newNextColumnIndex, _spaceUsed - newNextNextColumnIndex)
|
||||
_spaceUsed = (_spaceUsed - nextLen).toShort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isBlank(): Boolean {
|
||||
for (charIndex in 0 until spaceUsed)
|
||||
if (text[charIndex] != ' ') return false
|
||||
return true
|
||||
}
|
||||
|
||||
fun getStyle(column: Int): Long = styles[column]
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import android.graphics.Color
|
||||
import java.util.Properties
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
object TextStyle {
|
||||
|
||||
const val CHARACTER_ATTRIBUTE_BOLD = 1
|
||||
const val CHARACTER_ATTRIBUTE_ITALIC = 1 shl 1
|
||||
const val CHARACTER_ATTRIBUTE_UNDERLINE = 1 shl 2
|
||||
const val CHARACTER_ATTRIBUTE_BLINK = 1 shl 3
|
||||
const val CHARACTER_ATTRIBUTE_INVERSE = 1 shl 4
|
||||
const val CHARACTER_ATTRIBUTE_INVISIBLE = 1 shl 5
|
||||
const val CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 shl 6
|
||||
const val CHARACTER_ATTRIBUTE_PROTECTED = 1 shl 7
|
||||
const val CHARACTER_ATTRIBUTE_DIM = 1 shl 8
|
||||
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 shl 9
|
||||
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND = 1 shl 10
|
||||
|
||||
const val COLOR_INDEX_FOREGROUND = 256
|
||||
const val COLOR_INDEX_BACKGROUND = 257
|
||||
const val COLOR_INDEX_CURSOR = 258
|
||||
const val NUM_INDEXED_COLORS = 259
|
||||
|
||||
val NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0)
|
||||
|
||||
fun encode(foreColor: Int, backColor: Int, effect: Int): Long {
|
||||
var result = (effect and 0b111111111).toLong()
|
||||
if (foreColor and 0xff000000.toInt() == 0xff000000.toInt()) {
|
||||
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() or ((foreColor.toLong() and 0x00ffffffL) shl 40)
|
||||
} else {
|
||||
result = result or ((foreColor.toLong() and 0b111111111L) shl 40)
|
||||
}
|
||||
if (backColor and 0xff000000.toInt() == 0xff000000.toInt()) {
|
||||
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() or ((backColor.toLong() and 0x00ffffffL) shl 16)
|
||||
} else {
|
||||
result = result or ((backColor.toLong() and 0b111111111L) shl 16)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun decodeForeColor(style: Long): Int {
|
||||
return if (style and CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() == 0L) {
|
||||
((style ushr 40) and 0b111111111L).toInt()
|
||||
} else {
|
||||
0xff000000.toInt() or ((style ushr 40) and 0x00ffffffL).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeBackColor(style: Long): Int {
|
||||
return if (style and CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() == 0L) {
|
||||
((style ushr 16) and 0b111111111L).toInt()
|
||||
} else {
|
||||
0xff000000.toInt() or ((style ushr 16) and 0x00ffffffL).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeEffect(style: Long): Int {
|
||||
return (style and 0b11111111111L).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
|
||||
* Operating System Control (OSC) sequences.
|
||||
*/
|
||||
class TerminalColorScheme {
|
||||
|
||||
val defaultColors: IntArray = IntArray(TextStyle.NUM_INDEXED_COLORS)
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
fun updateWith(props: Properties) {
|
||||
reset()
|
||||
var cursorPropExists = false
|
||||
for ((keyObj, valueObj) in props) {
|
||||
val key = keyObj as String
|
||||
val value = valueObj as String
|
||||
val colorIndex: Int = when {
|
||||
key == "foreground" -> TextStyle.COLOR_INDEX_FOREGROUND
|
||||
key == "background" -> TextStyle.COLOR_INDEX_BACKGROUND
|
||||
key == "cursor" -> {
|
||||
cursorPropExists = true
|
||||
TextStyle.COLOR_INDEX_CURSOR
|
||||
}
|
||||
key.startsWith("color") -> {
|
||||
try {
|
||||
key.substring(5).toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
throw IllegalArgumentException("Invalid property: '$key'")
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Invalid property: '$key'")
|
||||
}
|
||||
|
||||
val colorValue = TerminalColors.parse(value)
|
||||
if (colorValue == 0) {
|
||||
throw IllegalArgumentException("Property '$key' has invalid color: '$value'")
|
||||
}
|
||||
|
||||
defaultColors[colorIndex] = colorValue
|
||||
}
|
||||
|
||||
if (!cursorPropExists) {
|
||||
setCursorColorForBackground()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCursorColorForBackground() {
|
||||
val backgroundColor = defaultColors[TextStyle.COLOR_INDEX_BACKGROUND]
|
||||
val brightness = TerminalColors.perceivedBrightness(backgroundColor)
|
||||
if (brightness > 0) {
|
||||
defaultColors[TextStyle.COLOR_INDEX_CURSOR] = if (brightness < 130) {
|
||||
0xffffffff.toInt()
|
||||
} else {
|
||||
0xff000000.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
System.arraycopy(DEFAULT_COLORSCHEME, 0, defaultColors, 0, TextStyle.NUM_INDEXED_COLORS)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_COLORSCHEME = longArrayOf(
|
||||
// 16 original colors. First 8 are dim.
|
||||
0xff000000, // black
|
||||
0xffcd0000, // dim red
|
||||
0xff00cd00, // dim green
|
||||
0xffcdcd00, // dim yellow
|
||||
0xff6495ed, // dim blue
|
||||
0xffcd00cd, // dim magenta
|
||||
0xff00cdcd, // dim cyan
|
||||
0xffe5e5e5, // dim white
|
||||
// Second 8 are bright:
|
||||
0xff7f7f7f, // medium grey
|
||||
0xffff0000, // bright red
|
||||
0xff00ff00, // bright green
|
||||
0xffffff00, // bright yellow
|
||||
0xff5c5cff, // light blue
|
||||
0xffff00ff, // bright magenta
|
||||
0xff00ffff, // bright cyan
|
||||
0xffffffffL, // bright white
|
||||
|
||||
// 216 color cube, six shades of each color:
|
||||
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
|
||||
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
|
||||
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
|
||||
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
|
||||
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
|
||||
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
|
||||
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
|
||||
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
|
||||
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
|
||||
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
|
||||
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
|
||||
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
|
||||
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
|
||||
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
|
||||
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
|
||||
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
|
||||
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
|
||||
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffffL,
|
||||
|
||||
// 24 grey scale ramp:
|
||||
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
|
||||
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
|
||||
|
||||
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
|
||||
0xffffffffL, 0xff000000L, 0xffffffffL
|
||||
).map { it.toInt() }.toIntArray()
|
||||
}
|
||||
}
|
||||
|
||||
/** Current terminal colors (if different from default). */
|
||||
class TerminalColors {
|
||||
|
||||
val currentColors: IntArray = IntArray(TextStyle.NUM_INDEXED_COLORS)
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
fun reset(index: Int) {
|
||||
currentColors[index] = COLOR_SCHEME.defaultColors[index]
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
System.arraycopy(COLOR_SCHEME.defaultColors, 0, currentColors, 0, TextStyle.NUM_INDEXED_COLORS)
|
||||
}
|
||||
|
||||
fun tryParseColor(intoIndex: Int, textParameter: String) {
|
||||
val c = parse(textParameter)
|
||||
if (c != 0) currentColors[intoIndex] = c
|
||||
}
|
||||
|
||||
companion object {
|
||||
val COLOR_SCHEME = TerminalColorScheme()
|
||||
|
||||
internal fun parse(c: String): Int {
|
||||
return try {
|
||||
val (skipInitial, skipBetween) = when {
|
||||
c[0] == '#' -> 1 to 0
|
||||
c.startsWith("rgb:") -> 4 to 1
|
||||
else -> return 0
|
||||
}
|
||||
val charsForColors = c.length - skipInitial - 2 * skipBetween
|
||||
if (charsForColors % 3 != 0) return 0
|
||||
val componentLength = charsForColors / 3
|
||||
val mult = 255.0 / (2.0.pow(componentLength * 4) - 1)
|
||||
|
||||
var currentPosition = skipInitial
|
||||
val rString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
currentPosition += componentLength + skipBetween
|
||||
val gString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
currentPosition += componentLength + skipBetween
|
||||
val bString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
|
||||
val r = (rString.toInt(16) * mult).toInt()
|
||||
val g = (gString.toInt(16) * mult).toInt()
|
||||
val b = (bString.toInt(16) * mult).toInt()
|
||||
(0xFF shl 24) or (r shl 16) or (g shl 8) or b
|
||||
} catch (_: NumberFormatException) {
|
||||
0
|
||||
} catch (_: IndexOutOfBoundsException) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun perceivedBrightness(color: Int): Int {
|
||||
return floor(
|
||||
sqrt(
|
||||
Color.red(color).toDouble().pow(2) * 0.241 +
|
||||
Color.green(color).toDouble().pow(2) * 0.691 +
|
||||
Color.blue(color).toDouble().pow(2) * 0.068
|
||||
)
|
||||
).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
/**
|
||||
* Implementation of wcwidth(3) for Unicode 15.
|
||||
*
|
||||
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* Must be kept in sync with the following:
|
||||
* https://github.com/termux/wcwidth
|
||||
* https://github.com/termux/libandroid-support
|
||||
* https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
|
||||
*/
|
||||
object WcWidth {
|
||||
|
||||
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private val ZERO_WIDTH = arrayOf(
|
||||
intArrayOf(0x00300, 0x0036f), // Combining Grave Accent ..Combining Latin Small Le
|
||||
intArrayOf(0x00483, 0x00489), // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
||||
intArrayOf(0x00591, 0x005bd), // Hebrew Accent Etnahta ..Hebrew Point Meteg
|
||||
intArrayOf(0x005bf, 0x005bf), // Hebrew Point Rafe ..Hebrew Point Rafe
|
||||
intArrayOf(0x005c1, 0x005c2), // Hebrew Point Shin Dot ..Hebrew Point Sin Dot
|
||||
intArrayOf(0x005c4, 0x005c5), // Hebrew Mark Upper Dot ..Hebrew Mark Lower Dot
|
||||
intArrayOf(0x005c7, 0x005c7), // Hebrew Point Qamats Qata..Hebrew Point Qamats Qata
|
||||
intArrayOf(0x00610, 0x0061a), // Arabic Sign Sallallahou ..Arabic Small Kasra
|
||||
intArrayOf(0x0064b, 0x0065f), // Arabic Fathatan ..Arabic Wavy Hamza Below
|
||||
intArrayOf(0x00670, 0x00670), // Arabic Letter Superscrip..Arabic Letter Superscrip
|
||||
intArrayOf(0x006d6, 0x006dc), // Arabic Small High Ligatu..Arabic Small High Seen
|
||||
intArrayOf(0x006df, 0x006e4), // Arabic Small High Rounde..Arabic Small High Madda
|
||||
intArrayOf(0x006e7, 0x006e8), // Arabic Small High Yeh ..Arabic Small High Noon
|
||||
intArrayOf(0x006ea, 0x006ed), // Arabic Empty Centre Low ..Arabic Small Low Meem
|
||||
intArrayOf(0x00711, 0x00711), // Syriac Letter Superscrip..Syriac Letter Superscrip
|
||||
intArrayOf(0x00730, 0x0074a), // Syriac Pthaha Above ..Syriac Barrekh
|
||||
intArrayOf(0x007a6, 0x007b0), // Thaana Abafili ..Thaana Sukun
|
||||
intArrayOf(0x007eb, 0x007f3), // Nko Combining Short High..Nko Combining Double Dot
|
||||
intArrayOf(0x007fd, 0x007fd), // Nko Dantayalan ..Nko Dantayalan
|
||||
intArrayOf(0x00816, 0x00819), // Samaritan Mark In ..Samaritan Mark Dagesh
|
||||
intArrayOf(0x0081b, 0x00823), // Samaritan Mark Epentheti..Samaritan Vowel Sign A
|
||||
intArrayOf(0x00825, 0x00827), // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||
intArrayOf(0x00829, 0x0082d), // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||
intArrayOf(0x00859, 0x0085b), // Mandaic Affrication Mark..Mandaic Gemination Mark
|
||||
intArrayOf(0x00898, 0x0089f), // Arabic Small High Word A..Arabic Half Madda Over M
|
||||
intArrayOf(0x008ca, 0x008e1), // Arabic Small High Farsi ..Arabic Small High Sign S
|
||||
intArrayOf(0x008e3, 0x00902), // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||
intArrayOf(0x0093a, 0x0093a), // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||
intArrayOf(0x0093c, 0x0093c), // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
||||
intArrayOf(0x00941, 0x00948), // Devanagari Vowel Sign U ..Devanagari Vowel Sign Ai
|
||||
intArrayOf(0x0094d, 0x0094d), // Devanagari Sign Virama ..Devanagari Sign Virama
|
||||
intArrayOf(0x00951, 0x00957), // Devanagari Stress Sign U..Devanagari Vowel Sign Uu
|
||||
intArrayOf(0x00962, 0x00963), // Devanagari Vowel Sign Vo..Devanagari Vowel Sign Vo
|
||||
intArrayOf(0x00981, 0x00981), // Bengali Sign Candrabindu..Bengali Sign Candrabindu
|
||||
intArrayOf(0x009bc, 0x009bc), // Bengali Sign Nukta ..Bengali Sign Nukta
|
||||
intArrayOf(0x009c1, 0x009c4), // Bengali Vowel Sign U ..Bengali Vowel Sign Vocal
|
||||
intArrayOf(0x009cd, 0x009cd), // Bengali Sign Virama ..Bengali Sign Virama
|
||||
intArrayOf(0x009e2, 0x009e3), // Bengali Vowel Sign Vocal..Bengali Vowel Sign Vocal
|
||||
intArrayOf(0x009fe, 0x009fe), // Bengali Sandhi Mark ..Bengali Sandhi Mark
|
||||
intArrayOf(0x00a01, 0x00a02), // Gurmukhi Sign Adak Bindi..Gurmukhi Sign Bindi
|
||||
intArrayOf(0x00a3c, 0x00a3c), // Gurmukhi Sign Nukta ..Gurmukhi Sign Nukta
|
||||
intArrayOf(0x00a41, 0x00a42), // Gurmukhi Vowel Sign U ..Gurmukhi Vowel Sign Uu
|
||||
intArrayOf(0x00a47, 0x00a48), // Gurmukhi Vowel Sign Ee ..Gurmukhi Vowel Sign Ai
|
||||
intArrayOf(0x00a4b, 0x00a4d), // Gurmukhi Vowel Sign Oo ..Gurmukhi Sign Virama
|
||||
intArrayOf(0x00a51, 0x00a51), // Gurmukhi Sign Udaat ..Gurmukhi Sign Udaat
|
||||
intArrayOf(0x00a70, 0x00a71), // Gurmukhi Tippi ..Gurmukhi Addak
|
||||
intArrayOf(0x00a75, 0x00a75), // Gurmukhi Sign Yakash ..Gurmukhi Sign Yakash
|
||||
intArrayOf(0x00a81, 0x00a82), // Gujarati Sign Candrabind..Gujarati Sign Anusvara
|
||||
intArrayOf(0x00abc, 0x00abc), // Gujarati Sign Nukta ..Gujarati Sign Nukta
|
||||
intArrayOf(0x00ac1, 0x00ac5), // Gujarati Vowel Sign U ..Gujarati Vowel Sign Cand
|
||||
intArrayOf(0x00ac7, 0x00ac8), // Gujarati Vowel Sign E ..Gujarati Vowel Sign Ai
|
||||
intArrayOf(0x00acd, 0x00acd), // Gujarati Sign Virama ..Gujarati Sign Virama
|
||||
intArrayOf(0x00ae2, 0x00ae3), // Gujarati Vowel Sign Voca..Gujarati Vowel Sign Voca
|
||||
intArrayOf(0x00afa, 0x00aff), // Gujarati Sign Sukun ..Gujarati Sign Two-circle
|
||||
intArrayOf(0x00b01, 0x00b01), // Oriya Sign Candrabindu ..Oriya Sign Candrabindu
|
||||
intArrayOf(0x00b3c, 0x00b3c), // Oriya Sign Nukta ..Oriya Sign Nukta
|
||||
intArrayOf(0x00b3f, 0x00b3f), // Oriya Vowel Sign I ..Oriya Vowel Sign I
|
||||
intArrayOf(0x00b41, 0x00b44), // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
|
||||
intArrayOf(0x00b4d, 0x00b4d), // Oriya Sign Virama ..Oriya Sign Virama
|
||||
intArrayOf(0x00b55, 0x00b56), // Oriya Sign Overline ..Oriya Ai Length Mark
|
||||
intArrayOf(0x00b62, 0x00b63), // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
|
||||
intArrayOf(0x00b82, 0x00b82), // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||
intArrayOf(0x00bc0, 0x00bc0), // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||
intArrayOf(0x00bcd, 0x00bcd), // Tamil Sign Virama ..Tamil Sign Virama
|
||||
intArrayOf(0x00c00, 0x00c00), // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||
intArrayOf(0x00c04, 0x00c04), // Telugu Sign Combining An..Telugu Sign Combining An
|
||||
intArrayOf(0x00c3c, 0x00c3c), // Telugu Sign Nukta ..Telugu Sign Nukta
|
||||
intArrayOf(0x00c3e, 0x00c40), // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||
intArrayOf(0x00c46, 0x00c48), // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||
intArrayOf(0x00c4a, 0x00c4d), // Telugu Vowel Sign O ..Telugu Sign Virama
|
||||
intArrayOf(0x00c55, 0x00c56), // Telugu Length Mark ..Telugu Ai Length Mark
|
||||
intArrayOf(0x00c62, 0x00c63), // Telugu Vowel Sign Vocali..Telugu Vowel Sign Vocali
|
||||
intArrayOf(0x00c81, 0x00c81), // Kannada Sign Candrabindu..Kannada Sign Candrabindu
|
||||
intArrayOf(0x00cbc, 0x00cbc), // Kannada Sign Nukta ..Kannada Sign Nukta
|
||||
intArrayOf(0x00cbf, 0x00cbf), // Kannada Vowel Sign I ..Kannada Vowel Sign I
|
||||
intArrayOf(0x00cc6, 0x00cc6), // Kannada Vowel Sign E ..Kannada Vowel Sign E
|
||||
intArrayOf(0x00ccc, 0x00ccd), // Kannada Vowel Sign Au ..Kannada Sign Virama
|
||||
intArrayOf(0x00ce2, 0x00ce3), // Kannada Vowel Sign Vocal..Kannada Vowel Sign Vocal
|
||||
intArrayOf(0x00d00, 0x00d01), // Malayalam Sign Combining..Malayalam Sign Candrabin
|
||||
intArrayOf(0x00d3b, 0x00d3c), // Malayalam Sign Vertical ..Malayalam Sign Circular
|
||||
intArrayOf(0x00d41, 0x00d44), // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||
intArrayOf(0x00d4d, 0x00d4d), // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||
intArrayOf(0x00d62, 0x00d63), // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
||||
intArrayOf(0x00d81, 0x00d81), // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
|
||||
intArrayOf(0x00dca, 0x00dca), // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||
intArrayOf(0x00dd2, 0x00dd4), // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||
intArrayOf(0x00dd6, 0x00dd6), // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
||||
intArrayOf(0x00e31, 0x00e31), // Thai Character Mai Han-a..Thai Character Mai Han-a
|
||||
intArrayOf(0x00e34, 0x00e3a), // Thai Character Sara I ..Thai Character Phinthu
|
||||
intArrayOf(0x00e47, 0x00e4e), // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||
intArrayOf(0x00eb1, 0x00eb1), // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||
intArrayOf(0x00eb4, 0x00ebc), // Lao Vowel Sign I ..Lao Semivowel Sign Lo
|
||||
intArrayOf(0x00ec8, 0x00ece), // Lao Tone Mai Ek ..(nil)
|
||||
intArrayOf(0x00f18, 0x00f19), // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||
intArrayOf(0x00f35, 0x00f35), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
intArrayOf(0x00f37, 0x00f37), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
intArrayOf(0x00f39, 0x00f39), // Tibetan Mark Tsa -phru ..Tibetan Mark Tsa -phru
|
||||
intArrayOf(0x00f71, 0x00f7e), // Tibetan Vowel Sign Aa ..Tibetan Sign Rjes Su Nga
|
||||
intArrayOf(0x00f80, 0x00f84), // Tibetan Vowel Sign Rever..Tibetan Mark Halanta
|
||||
intArrayOf(0x00f86, 0x00f87), // Tibetan Sign Lci Rtags ..Tibetan Sign Yang Rtags
|
||||
intArrayOf(0x00f8d, 0x00f97), // Tibetan Subjoined Sign L..Tibetan Subjoined Letter
|
||||
intArrayOf(0x00f99, 0x00fbc), // Tibetan Subjoined Letter..Tibetan Subjoined Letter
|
||||
intArrayOf(0x00fc6, 0x00fc6), // Tibetan Symbol Padma Gda..Tibetan Symbol Padma Gda
|
||||
intArrayOf(0x0102d, 0x01030), // Myanmar Vowel Sign I ..Myanmar Vowel Sign Uu
|
||||
intArrayOf(0x01032, 0x01037), // Myanmar Vowel Sign Ai ..Myanmar Sign Dot Below
|
||||
intArrayOf(0x01039, 0x0103a), // Myanmar Sign Virama ..Myanmar Sign Asat
|
||||
intArrayOf(0x0103d, 0x0103e), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
intArrayOf(0x01058, 0x01059), // Myanmar Vowel Sign Vocal..Myanmar Vowel Sign Vocal
|
||||
intArrayOf(0x0105e, 0x01060), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
intArrayOf(0x01071, 0x01074), // Myanmar Vowel Sign Geba ..Myanmar Vowel Sign Kayah
|
||||
intArrayOf(0x01082, 0x01082), // Myanmar Consonant Sign S..Myanmar Consonant Sign S
|
||||
intArrayOf(0x01085, 0x01086), // Myanmar Vowel Sign Shan ..Myanmar Vowel Sign Shan
|
||||
intArrayOf(0x0108d, 0x0108d), // Myanmar Sign Shan Counci..Myanmar Sign Shan Counci
|
||||
intArrayOf(0x0109d, 0x0109d), // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||
intArrayOf(0x0135d, 0x0135f), // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||
intArrayOf(0x01712, 0x01714), // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
||||
intArrayOf(0x01732, 0x01733), // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
|
||||
intArrayOf(0x01752, 0x01753), // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||
intArrayOf(0x01772, 0x01773), // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||
intArrayOf(0x017b4, 0x017b5), // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
||||
intArrayOf(0x017b7, 0x017bd), // Khmer Vowel Sign I ..Khmer Vowel Sign Ua
|
||||
intArrayOf(0x017c6, 0x017c6), // Khmer Sign Nikahit ..Khmer Sign Nikahit
|
||||
intArrayOf(0x017c9, 0x017d3), // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||
intArrayOf(0x017dd, 0x017dd), // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||
intArrayOf(0x0180b, 0x0180d), // Mongolian Free Variation..Mongolian Free Variation
|
||||
intArrayOf(0x0180f, 0x0180f), // Mongolian Free Variation..Mongolian Free Variation
|
||||
intArrayOf(0x01885, 0x01886), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
intArrayOf(0x018a9, 0x018a9), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
intArrayOf(0x01920, 0x01922), // Limbu Vowel Sign A ..Limbu Vowel Sign U
|
||||
intArrayOf(0x01927, 0x01928), // Limbu Vowel Sign E ..Limbu Vowel Sign O
|
||||
intArrayOf(0x01932, 0x01932), // Limbu Small Letter Anusv..Limbu Small Letter Anusv
|
||||
intArrayOf(0x01939, 0x0193b), // Limbu Sign Mukphreng ..Limbu Sign Sa-i
|
||||
intArrayOf(0x01a17, 0x01a18), // Buginese Vowel Sign I ..Buginese Vowel Sign U
|
||||
intArrayOf(0x01a1b, 0x01a1b), // Buginese Vowel Sign Ae ..Buginese Vowel Sign Ae
|
||||
intArrayOf(0x01a56, 0x01a56), // Tai Tham Consonant Sign ..Tai Tham Consonant Sign
|
||||
intArrayOf(0x01a58, 0x01a5e), // Tai Tham Sign Mai Kang L..Tai Tham Consonant Sign
|
||||
intArrayOf(0x01a60, 0x01a60), // Tai Tham Sign Sakot ..Tai Tham Sign Sakot
|
||||
intArrayOf(0x01a62, 0x01a62), // Tai Tham Vowel Sign Mai ..Tai Tham Vowel Sign Mai
|
||||
intArrayOf(0x01a65, 0x01a6c), // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
|
||||
intArrayOf(0x01a73, 0x01a7c), // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||
intArrayOf(0x01a7f, 0x01a7f), // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
||||
intArrayOf(0x01ab0, 0x01ace), // Combining Doubled Circum..Combining Latin Small Le
|
||||
intArrayOf(0x01b00, 0x01b03), // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||
intArrayOf(0x01b34, 0x01b34), // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||
intArrayOf(0x01b36, 0x01b3a), // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
||||
intArrayOf(0x01b3c, 0x01b3c), // Balinese Vowel Sign La L..Balinese Vowel Sign La L
|
||||
intArrayOf(0x01b42, 0x01b42), // Balinese Vowel Sign Pepe..Balinese Vowel Sign Pepe
|
||||
intArrayOf(0x01b6b, 0x01b73), // Balinese Musical Symbol ..Balinese Musical Symbol
|
||||
intArrayOf(0x01b80, 0x01b81), // Sundanese Sign Panyecek ..Sundanese Sign Panglayar
|
||||
intArrayOf(0x01ba2, 0x01ba5), // Sundanese Consonant Sign..Sundanese Vowel Sign Pan
|
||||
intArrayOf(0x01ba8, 0x01ba9), // Sundanese Vowel Sign Pam..Sundanese Vowel Sign Pan
|
||||
intArrayOf(0x01bab, 0x01bad), // Sundanese Sign Virama ..Sundanese Consonant Sign
|
||||
intArrayOf(0x01be6, 0x01be6), // Batak Sign Tompi ..Batak Sign Tompi
|
||||
intArrayOf(0x01be8, 0x01be9), // Batak Vowel Sign Pakpak ..Batak Vowel Sign Ee
|
||||
intArrayOf(0x01bed, 0x01bed), // Batak Vowel Sign Karo O ..Batak Vowel Sign Karo O
|
||||
intArrayOf(0x01bef, 0x01bf1), // Batak Vowel Sign U For S..Batak Consonant Sign H
|
||||
intArrayOf(0x01c2c, 0x01c33), // Lepcha Vowel Sign E ..Lepcha Consonant Sign T
|
||||
intArrayOf(0x01c36, 0x01c37), // Lepcha Sign Ran ..Lepcha Sign Nukta
|
||||
intArrayOf(0x01cd0, 0x01cd2), // Vedic Tone Karshana ..Vedic Tone Prenkha
|
||||
intArrayOf(0x01cd4, 0x01ce0), // Vedic Sign Yajurvedic Mi..Vedic Tone Rigvedic Kash
|
||||
intArrayOf(0x01ce2, 0x01ce8), // Vedic Sign Visarga Svari..Vedic Sign Visarga Anuda
|
||||
intArrayOf(0x01ced, 0x01ced), // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||
intArrayOf(0x01cf4, 0x01cf4), // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||
intArrayOf(0x01cf8, 0x01cf9), // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||
intArrayOf(0x01dc0, 0x01dff), // Combining Dotted Grave A..Combining Right Arrowhea
|
||||
intArrayOf(0x020d0, 0x020f0), // Combining Left Harpoon A..Combining Asterisk Above
|
||||
intArrayOf(0x02cef, 0x02cf1), // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||
intArrayOf(0x02d7f, 0x02d7f), // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
||||
intArrayOf(0x02de0, 0x02dff), // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
intArrayOf(0x0302a, 0x0302d), // Ideographic Level Tone M..Ideographic Entering Ton
|
||||
intArrayOf(0x03099, 0x0309a), // Combining Katakana-hirag..Combining Katakana-hirag
|
||||
intArrayOf(0x0a66f, 0x0a672), // Combining Cyrillic Vzmet..Combining Cyrillic Thous
|
||||
intArrayOf(0x0a674, 0x0a67d), // Combining Cyrillic Lette..Combining Cyrillic Payer
|
||||
intArrayOf(0x0a69e, 0x0a69f), // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
intArrayOf(0x0a6f0, 0x0a6f1), // Bamum Combining Mark Koq..Bamum Combining Mark Tuk
|
||||
intArrayOf(0x0a802, 0x0a802), // Syloti Nagri Sign Dvisva..Syloti Nagri Sign Dvisva
|
||||
intArrayOf(0x0a806, 0x0a806), // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||
intArrayOf(0x0a80b, 0x0a80b), // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||
intArrayOf(0x0a825, 0x0a826), // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
||||
intArrayOf(0x0a82c, 0x0a82c), // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
|
||||
intArrayOf(0x0a8c4, 0x0a8c5), // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
|
||||
intArrayOf(0x0a8e0, 0x0a8f1), // Combining Devanagari Dig..Combining Devanagari Sig
|
||||
intArrayOf(0x0a8ff, 0x0a8ff), // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
|
||||
intArrayOf(0x0a926, 0x0a92d), // Kayah Li Vowel Ue ..Kayah Li Tone Calya Plop
|
||||
intArrayOf(0x0a947, 0x0a951), // Rejang Vowel Sign I ..Rejang Consonant Sign R
|
||||
intArrayOf(0x0a980, 0x0a982), // Javanese Sign Panyangga ..Javanese Sign Layar
|
||||
intArrayOf(0x0a9b3, 0x0a9b3), // Javanese Sign Cecak Telu..Javanese Sign Cecak Telu
|
||||
intArrayOf(0x0a9b6, 0x0a9b9), // Javanese Vowel Sign Wulu..Javanese Vowel Sign Suku
|
||||
intArrayOf(0x0a9bc, 0x0a9bd), // Javanese Vowel Sign Pepe..Javanese Consonant Sign
|
||||
intArrayOf(0x0a9e5, 0x0a9e5), // Myanmar Sign Shan Saw ..Myanmar Sign Shan Saw
|
||||
intArrayOf(0x0aa29, 0x0aa2e), // Cham Vowel Sign Aa ..Cham Vowel Sign Oe
|
||||
intArrayOf(0x0aa31, 0x0aa32), // Cham Vowel Sign Au ..Cham Vowel Sign Ue
|
||||
intArrayOf(0x0aa35, 0x0aa36), // Cham Consonant Sign La ..Cham Consonant Sign Wa
|
||||
intArrayOf(0x0aa43, 0x0aa43), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
intArrayOf(0x0aa4c, 0x0aa4c), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
intArrayOf(0x0aa7c, 0x0aa7c), // Myanmar Sign Tai Laing T..Myanmar Sign Tai Laing T
|
||||
intArrayOf(0x0aab0, 0x0aab0), // Tai Viet Mai Kang ..Tai Viet Mai Kang
|
||||
intArrayOf(0x0aab2, 0x0aab4), // Tai Viet Vowel I ..Tai Viet Vowel U
|
||||
intArrayOf(0x0aab7, 0x0aab8), // Tai Viet Mai Khit ..Tai Viet Vowel Ia
|
||||
intArrayOf(0x0aabe, 0x0aabf), // Tai Viet Vowel Am ..Tai Viet Tone Mai Ek
|
||||
intArrayOf(0x0aac1, 0x0aac1), // Tai Viet Tone Mai Tho ..Tai Viet Tone Mai Tho
|
||||
intArrayOf(0x0aaec, 0x0aaed), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0aaf6, 0x0aaf6), // Meetei Mayek Virama ..Meetei Mayek Virama
|
||||
intArrayOf(0x0abe5, 0x0abe5), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0abe8, 0x0abe8), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0abed, 0x0abed), // Meetei Mayek Apun Iyek ..Meetei Mayek Apun Iyek
|
||||
intArrayOf(0x0fb1e, 0x0fb1e), // Hebrew Point Judeo-spani..Hebrew Point Judeo-spani
|
||||
intArrayOf(0x0fe00, 0x0fe0f), // Variation Selector-1 ..Variation Selector-16
|
||||
intArrayOf(0x0fe20, 0x0fe2f), // Combining Ligature Left ..Combining Cyrillic Titlo
|
||||
intArrayOf(0x101fd, 0x101fd), // Phaistos Disc Sign Combi..Phaistos Disc Sign Combi
|
||||
intArrayOf(0x102e0, 0x102e0), // Coptic Epact Thousands M..Coptic Epact Thousands M
|
||||
intArrayOf(0x10376, 0x1037a), // Combining Old Permic Let..Combining Old Permic Let
|
||||
intArrayOf(0x10a01, 0x10a03), // Kharoshthi Vowel Sign I ..Kharoshthi Vowel Sign Vo
|
||||
intArrayOf(0x10a05, 0x10a06), // Kharoshthi Vowel Sign E ..Kharoshthi Vowel Sign O
|
||||
intArrayOf(0x10a0c, 0x10a0f), // Kharoshthi Vowel Length ..Kharoshthi Sign Visarga
|
||||
intArrayOf(0x10a38, 0x10a3a), // Kharoshthi Sign Bar Abov..Kharoshthi Sign Dot Belo
|
||||
intArrayOf(0x10a3f, 0x10a3f), // Kharoshthi Virama ..Kharoshthi Virama
|
||||
intArrayOf(0x10ae5, 0x10ae6), // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||
intArrayOf(0x10d24, 0x10d27), // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
|
||||
intArrayOf(0x10eab, 0x10eac), // Yezidi Combining Hamza M..Yezidi Combining Madda M
|
||||
intArrayOf(0x10efd, 0x10eff), // (nil) ..(nil)
|
||||
intArrayOf(0x10f46, 0x10f50), // Sogdian Combining Dot Be..Sogdian Combining Stroke
|
||||
intArrayOf(0x10f82, 0x10f85), // Old Uyghur Combining Dot..Old Uyghur Combining Two
|
||||
intArrayOf(0x11001, 0x11001), // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||
intArrayOf(0x11038, 0x11046), // Brahmi Vowel Sign Aa ..Brahmi Virama
|
||||
intArrayOf(0x11070, 0x11070), // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
|
||||
intArrayOf(0x11073, 0x11074), // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
|
||||
intArrayOf(0x1107f, 0x11081), // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||
intArrayOf(0x110b3, 0x110b6), // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||
intArrayOf(0x110b9, 0x110ba), // Kaithi Sign Virama ..Kaithi Sign Nukta
|
||||
intArrayOf(0x110c2, 0x110c2), // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
|
||||
intArrayOf(0x11100, 0x11102), // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||
intArrayOf(0x11127, 0x1112b), // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||
intArrayOf(0x1112d, 0x11134), // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||
intArrayOf(0x11173, 0x11173), // Mahajani Sign Nukta ..Mahajani Sign Nukta
|
||||
intArrayOf(0x11180, 0x11181), // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||
intArrayOf(0x111b6, 0x111be), // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||
intArrayOf(0x111c9, 0x111cc), // Sharada Sandhi Mark ..Sharada Extra Short Vowe
|
||||
intArrayOf(0x111cf, 0x111cf), // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
|
||||
intArrayOf(0x1122f, 0x11231), // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||
intArrayOf(0x11234, 0x11234), // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||
intArrayOf(0x11236, 0x11237), // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||
intArrayOf(0x1123e, 0x1123e), // Khojki Sign Sukun ..Khojki Sign Sukun
|
||||
intArrayOf(0x11241, 0x11241), // (nil) ..(nil)
|
||||
intArrayOf(0x112df, 0x112df), // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||
intArrayOf(0x112e3, 0x112ea), // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||
intArrayOf(0x11300, 0x11301), // Grantha Sign Combining A..Grantha Sign Candrabindu
|
||||
intArrayOf(0x1133b, 0x1133c), // Combining Bindu Below ..Grantha Sign Nukta
|
||||
intArrayOf(0x11340, 0x11340), // Grantha Vowel Sign Ii ..Grantha Vowel Sign Ii
|
||||
intArrayOf(0x11366, 0x1136c), // Combining Grantha Digit ..Combining Grantha Digit
|
||||
intArrayOf(0x11370, 0x11374), // Combining Grantha Letter..Combining Grantha Letter
|
||||
intArrayOf(0x11438, 0x1143f), // Newa Vowel Sign U ..Newa Vowel Sign Ai
|
||||
intArrayOf(0x11442, 0x11444), // Newa Sign Virama ..Newa Sign Anusvara
|
||||
intArrayOf(0x11446, 0x11446), // Newa Sign Nukta ..Newa Sign Nukta
|
||||
intArrayOf(0x1145e, 0x1145e), // Newa Sandhi Mark ..Newa Sandhi Mark
|
||||
intArrayOf(0x114b3, 0x114b8), // Tirhuta Vowel Sign U ..Tirhuta Vowel Sign Vocal
|
||||
intArrayOf(0x114ba, 0x114ba), // Tirhuta Vowel Sign Short..Tirhuta Vowel Sign Short
|
||||
intArrayOf(0x114bf, 0x114c0), // Tirhuta Sign Candrabindu..Tirhuta Sign Anusvara
|
||||
intArrayOf(0x114c2, 0x114c3), // Tirhuta Sign Virama ..Tirhuta Sign Nukta
|
||||
intArrayOf(0x115b2, 0x115b5), // Siddham Vowel Sign U ..Siddham Vowel Sign Vocal
|
||||
intArrayOf(0x115bc, 0x115bd), // Siddham Sign Candrabindu..Siddham Sign Anusvara
|
||||
intArrayOf(0x115bf, 0x115c0), // Siddham Sign Virama ..Siddham Sign Nukta
|
||||
intArrayOf(0x115dc, 0x115dd), // Siddham Vowel Sign Alter..Siddham Vowel Sign Alter
|
||||
intArrayOf(0x11633, 0x1163a), // Modi Vowel Sign U ..Modi Vowel Sign Ai
|
||||
intArrayOf(0x1163d, 0x1163d), // Modi Sign Anusvara ..Modi Sign Anusvara
|
||||
intArrayOf(0x1163f, 0x11640), // Modi Sign Virama ..Modi Sign Ardhacandra
|
||||
intArrayOf(0x116ab, 0x116ab), // Takri Sign Anusvara ..Takri Sign Anusvara
|
||||
intArrayOf(0x116ad, 0x116ad), // Takri Vowel Sign Aa ..Takri Vowel Sign Aa
|
||||
intArrayOf(0x116b0, 0x116b5), // Takri Vowel Sign U ..Takri Vowel Sign Au
|
||||
intArrayOf(0x116b7, 0x116b7), // Takri Sign Nukta ..Takri Sign Nukta
|
||||
intArrayOf(0x1171d, 0x1171f), // Ahom Consonant Sign Medi..Ahom Consonant Sign Medi
|
||||
intArrayOf(0x11722, 0x11725), // Ahom Vowel Sign I ..Ahom Vowel Sign Uu
|
||||
intArrayOf(0x11727, 0x1172b), // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||
intArrayOf(0x1182f, 0x11837), // Dogra Vowel Sign U ..Dogra Sign Anusvara
|
||||
intArrayOf(0x11839, 0x1183a), // Dogra Sign Virama ..Dogra Sign Nukta
|
||||
intArrayOf(0x1193b, 0x1193c), // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
|
||||
intArrayOf(0x1193e, 0x1193e), // Dives Akuru Virama ..Dives Akuru Virama
|
||||
intArrayOf(0x11943, 0x11943), // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
|
||||
intArrayOf(0x119d4, 0x119d7), // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
|
||||
intArrayOf(0x119da, 0x119db), // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
|
||||
intArrayOf(0x119e0, 0x119e0), // Nandinagari Sign Virama ..Nandinagari Sign Virama
|
||||
intArrayOf(0x11a01, 0x11a0a), // Zanabazar Square Vowel S..Zanabazar Square Vowel L
|
||||
intArrayOf(0x11a33, 0x11a38), // Zanabazar Square Final C..Zanabazar Square Sign An
|
||||
intArrayOf(0x11a3b, 0x11a3e), // Zanabazar Square Cluster..Zanabazar Square Cluster
|
||||
intArrayOf(0x11a47, 0x11a47), // Zanabazar Square Subjoin..Zanabazar Square Subjoin
|
||||
intArrayOf(0x11a51, 0x11a56), // Soyombo Vowel Sign I ..Soyombo Vowel Sign Oe
|
||||
intArrayOf(0x11a59, 0x11a5b), // Soyombo Vowel Sign Vocal..Soyombo Vowel Length Mar
|
||||
intArrayOf(0x11a8a, 0x11a96), // Soyombo Final Consonant ..Soyombo Sign Anusvara
|
||||
intArrayOf(0x11a98, 0x11a99), // Soyombo Gemination Mark ..Soyombo Subjoiner
|
||||
intArrayOf(0x11c30, 0x11c36), // Bhaiksuki Vowel Sign I ..Bhaiksuki Vowel Sign Voc
|
||||
intArrayOf(0x11c38, 0x11c3d), // Bhaiksuki Vowel Sign E ..Bhaiksuki Sign Anusvara
|
||||
intArrayOf(0x11c3f, 0x11c3f), // Bhaiksuki Sign Virama ..Bhaiksuki Sign Virama
|
||||
intArrayOf(0x11c92, 0x11ca7), // Marchen Subjoined Letter..Marchen Subjoined Letter
|
||||
intArrayOf(0x11caa, 0x11cb0), // Marchen Subjoined Letter..Marchen Vowel Sign Aa
|
||||
intArrayOf(0x11cb2, 0x11cb3), // Marchen Vowel Sign U ..Marchen Vowel Sign E
|
||||
intArrayOf(0x11cb5, 0x11cb6), // Marchen Sign Anusvara ..Marchen Sign Candrabindu
|
||||
intArrayOf(0x11d31, 0x11d36), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3a, 0x11d3a), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3c, 0x11d3d), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3f, 0x11d45), // Masaram Gondi Vowel Sign..Masaram Gondi Virama
|
||||
intArrayOf(0x11d47, 0x11d47), // Masaram Gondi Ra-kara ..Masaram Gondi Ra-kara
|
||||
intArrayOf(0x11d90, 0x11d91), // Gunjala Gondi Vowel Sign..Gunjala Gondi Vowel Sign
|
||||
intArrayOf(0x11d95, 0x11d95), // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
|
||||
intArrayOf(0x11d97, 0x11d97), // Gunjala Gondi Virama ..Gunjala Gondi Virama
|
||||
intArrayOf(0x11ef3, 0x11ef4), // Makasar Vowel Sign I ..Makasar Vowel Sign U
|
||||
intArrayOf(0x11f00, 0x11f01), // (nil) ..(nil)
|
||||
intArrayOf(0x11f36, 0x11f3a), // (nil) ..(nil)
|
||||
intArrayOf(0x11f40, 0x11f40), // (nil) ..(nil)
|
||||
intArrayOf(0x11f42, 0x11f42), // (nil) ..(nil)
|
||||
intArrayOf(0x13440, 0x13440), // (nil) ..(nil)
|
||||
intArrayOf(0x13447, 0x13455), // (nil) ..(nil)
|
||||
intArrayOf(0x16af0, 0x16af4), // Bassa Vah Combining High..Bassa Vah Combining High
|
||||
intArrayOf(0x16b30, 0x16b36), // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||
intArrayOf(0x16f4f, 0x16f4f), // Miao Sign Consonant Modi..Miao Sign Consonant Modi
|
||||
intArrayOf(0x16f8f, 0x16f92), // Miao Tone Right ..Miao Tone Below
|
||||
intArrayOf(0x16fe4, 0x16fe4), // Khitan Small Script Fill..Khitan Small Script Fill
|
||||
intArrayOf(0x1bc9d, 0x1bc9e), // Duployan Thick Letter Se..Duployan Double Mark
|
||||
intArrayOf(0x1cf00, 0x1cf2d), // Znamenny Combining Mark ..Znamenny Combining Mark
|
||||
intArrayOf(0x1cf30, 0x1cf46), // Znamenny Combining Tonal..Znamenny Priznak Modifie
|
||||
intArrayOf(0x1d167, 0x1d169), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d17b, 0x1d182), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d185, 0x1d18b), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d1aa, 0x1d1ad), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d242, 0x1d244), // Combining Greek Musical ..Combining Greek Musical
|
||||
intArrayOf(0x1da00, 0x1da36), // Signwriting Head Rim ..Signwriting Air Sucking
|
||||
intArrayOf(0x1da3b, 0x1da6c), // Signwriting Mouth Closed..Signwriting Excitement
|
||||
intArrayOf(0x1da75, 0x1da75), // Signwriting Upper Body T..Signwriting Upper Body T
|
||||
intArrayOf(0x1da84, 0x1da84), // Signwriting Location Hea..Signwriting Location Hea
|
||||
intArrayOf(0x1da9b, 0x1da9f), // Signwriting Fill Modifie..Signwriting Fill Modifie
|
||||
intArrayOf(0x1daa1, 0x1daaf), // Signwriting Rotation Mod..Signwriting Rotation Mod
|
||||
intArrayOf(0x1e000, 0x1e006), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e008, 0x1e018), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e01b, 0x1e021), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e023, 0x1e024), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e026, 0x1e02a), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e08f, 0x1e08f), // (nil) ..(nil)
|
||||
intArrayOf(0x1e130, 0x1e136), // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
|
||||
intArrayOf(0x1e2ae, 0x1e2ae), // Toto Sign Rising Tone ..Toto Sign Rising Tone
|
||||
intArrayOf(0x1e2ec, 0x1e2ef), // Wancho Tone Tup ..Wancho Tone Koini
|
||||
intArrayOf(0x1e4ec, 0x1e4ef), // (nil) ..(nil)
|
||||
intArrayOf(0x1e8d0, 0x1e8d6), // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||
intArrayOf(0x1e944, 0x1e94a), // Adlam Alif Lengthener ..Adlam Nukta
|
||||
intArrayOf(0xe0100, 0xe01ef), // Variation Selector-17 ..Variation Selector-256
|
||||
)
|
||||
|
||||
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private val WIDE_EASTASIAN = arrayOf(
|
||||
intArrayOf(0x01100, 0x0115f), // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||
intArrayOf(0x0231a, 0x0231b), // Watch ..Hourglass
|
||||
intArrayOf(0x02329, 0x0232a), // Left-pointing Angle Brac..Right-pointing Angle Bra
|
||||
intArrayOf(0x023e9, 0x023ec), // Black Right-pointing Dou..Black Down-pointing Doub
|
||||
intArrayOf(0x023f0, 0x023f0), // Alarm Clock ..Alarm Clock
|
||||
intArrayOf(0x023f3, 0x023f3), // Hourglass With Flowing S..Hourglass With Flowing S
|
||||
intArrayOf(0x025fd, 0x025fe), // White Medium Small Squar..Black Medium Small Squar
|
||||
intArrayOf(0x02614, 0x02615), // Umbrella With Rain Drops..Hot Beverage
|
||||
intArrayOf(0x02648, 0x02653), // Aries ..Pisces
|
||||
intArrayOf(0x0267f, 0x0267f), // Wheelchair Symbol ..Wheelchair Symbol
|
||||
intArrayOf(0x02693, 0x02693), // Anchor ..Anchor
|
||||
intArrayOf(0x026a1, 0x026a1), // High Voltage Sign ..High Voltage Sign
|
||||
intArrayOf(0x026aa, 0x026ab), // Medium White Circle ..Medium Black Circle
|
||||
intArrayOf(0x026bd, 0x026be), // Soccer Ball ..Baseball
|
||||
intArrayOf(0x026c4, 0x026c5), // Snowman Without Snow ..Sun Behind Cloud
|
||||
intArrayOf(0x026ce, 0x026ce), // Ophiuchus ..Ophiuchus
|
||||
intArrayOf(0x026d4, 0x026d4), // No Entry ..No Entry
|
||||
intArrayOf(0x026ea, 0x026ea), // Church ..Church
|
||||
intArrayOf(0x026f2, 0x026f3), // Fountain ..Flag In Hole
|
||||
intArrayOf(0x026f5, 0x026f5), // Sailboat ..Sailboat
|
||||
intArrayOf(0x026fa, 0x026fa), // Tent ..Tent
|
||||
intArrayOf(0x026fd, 0x026fd), // Fuel Pump ..Fuel Pump
|
||||
intArrayOf(0x02705, 0x02705), // White Heavy Check Mark ..White Heavy Check Mark
|
||||
intArrayOf(0x0270a, 0x0270b), // Raised Fist ..Raised Hand
|
||||
intArrayOf(0x02728, 0x02728), // Sparkles ..Sparkles
|
||||
intArrayOf(0x0274c, 0x0274c), // Cross Mark ..Cross Mark
|
||||
intArrayOf(0x0274e, 0x0274e), // Negative Squared Cross M..Negative Squared Cross M
|
||||
intArrayOf(0x02753, 0x02755), // Black Question Mark Orna..White Exclamation Mark O
|
||||
intArrayOf(0x02757, 0x02757), // Heavy Exclamation Mark S..Heavy Exclamation Mark S
|
||||
intArrayOf(0x02795, 0x02797), // Heavy Plus Sign ..Heavy Division Sign
|
||||
intArrayOf(0x027b0, 0x027b0), // Curly Loop ..Curly Loop
|
||||
intArrayOf(0x027bf, 0x027bf), // Double Curly Loop ..Double Curly Loop
|
||||
intArrayOf(0x02b1b, 0x02b1c), // Black Large Square ..White Large Square
|
||||
intArrayOf(0x02b50, 0x02b50), // White Medium Star ..White Medium Star
|
||||
intArrayOf(0x02b55, 0x02b55), // Heavy Large Circle ..Heavy Large Circle
|
||||
intArrayOf(0x02e80, 0x02e99), // Cjk Radical Repeat ..Cjk Radical Rap
|
||||
intArrayOf(0x02e9b, 0x02ef3), // Cjk Radical Choke ..Cjk Radical C-simplified
|
||||
intArrayOf(0x02f00, 0x02fd5), // Kangxi Radical One ..Kangxi Radical Flute
|
||||
intArrayOf(0x02ff0, 0x02ffb), // Ideographic Description ..Ideographic Description
|
||||
intArrayOf(0x03000, 0x0303e), // Ideographic Space ..Ideographic Variation In
|
||||
intArrayOf(0x03041, 0x03096), // Hiragana Letter Small A ..Hiragana Letter Small Ke
|
||||
intArrayOf(0x03099, 0x030ff), // Combining Katakana-hirag..Katakana Digraph Koto
|
||||
intArrayOf(0x03105, 0x0312f), // Bopomofo Letter B ..Bopomofo Letter Nn
|
||||
intArrayOf(0x03131, 0x0318e), // Hangul Letter Kiyeok ..Hangul Letter Araeae
|
||||
intArrayOf(0x03190, 0x031e3), // Ideographic Annotation L..Cjk Stroke Q
|
||||
intArrayOf(0x031f0, 0x0321e), // Katakana Letter Small Ku..Parenthesized Korean Cha
|
||||
intArrayOf(0x03220, 0x03247), // Parenthesized Ideograph ..Circled Ideograph Koto
|
||||
intArrayOf(0x03250, 0x04dbf), // Partnership Sign ..Cjk Unified Ideograph-4d
|
||||
intArrayOf(0x04e00, 0x0a48c), // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||
intArrayOf(0x0a490, 0x0a4c6), // Yi Radical Qot ..Yi Radical Ke
|
||||
intArrayOf(0x0a960, 0x0a97c), // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
||||
intArrayOf(0x0ac00, 0x0d7a3), // Hangul Syllable Ga ..Hangul Syllable Hih
|
||||
intArrayOf(0x0f900, 0x0faff), // Cjk Compatibility Ideogr..(nil)
|
||||
intArrayOf(0x0fe10, 0x0fe19), // Presentation Form For Ve..Presentation Form For Ve
|
||||
intArrayOf(0x0fe30, 0x0fe52), // Presentation Form For Ve..Small Full Stop
|
||||
intArrayOf(0x0fe54, 0x0fe66), // Small Semicolon ..Small Equals Sign
|
||||
intArrayOf(0x0fe68, 0x0fe6b), // Small Reverse Solidus ..Small Commercial At
|
||||
intArrayOf(0x0ff01, 0x0ff60), // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||
intArrayOf(0x0ffe0, 0x0ffe6), // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||
intArrayOf(0x16fe0, 0x16fe4), // Tangut Iteration Mark ..Khitan Small Script Fill
|
||||
intArrayOf(0x16ff0, 0x16ff1), // Vietnamese Alternate Rea..Vietnamese Alternate Rea
|
||||
intArrayOf(0x17000, 0x187f7), // (nil) ..(nil)
|
||||
intArrayOf(0x18800, 0x18cd5), // Tangut Component-001 ..Khitan Small Script Char
|
||||
intArrayOf(0x18d00, 0x18d08), // (nil) ..(nil)
|
||||
intArrayOf(0x1aff0, 0x1aff3), // Katakana Letter Minnan T..Katakana Letter Minnan T
|
||||
intArrayOf(0x1aff5, 0x1affb), // Katakana Letter Minnan T..Katakana Letter Minnan N
|
||||
intArrayOf(0x1affd, 0x1affe), // Katakana Letter Minnan N..Katakana Letter Minnan N
|
||||
intArrayOf(0x1b000, 0x1b122), // Katakana Letter Archaic ..Katakana Letter Archaic
|
||||
intArrayOf(0x1b132, 0x1b132), // (nil) ..(nil)
|
||||
intArrayOf(0x1b150, 0x1b152), // Hiragana Letter Small Wi..Hiragana Letter Small Wo
|
||||
intArrayOf(0x1b155, 0x1b155), // (nil) ..(nil)
|
||||
intArrayOf(0x1b164, 0x1b167), // Katakana Letter Small Wi..Katakana Letter Small N
|
||||
intArrayOf(0x1b170, 0x1b2fb), // Nushu Character-1b170 ..Nushu Character-1b2fb
|
||||
intArrayOf(0x1f004, 0x1f004), // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||
intArrayOf(0x1f0cf, 0x1f0cf), // Playing Card Black Joker..Playing Card Black Joker
|
||||
intArrayOf(0x1f18e, 0x1f18e), // Negative Squared Ab ..Negative Squared Ab
|
||||
intArrayOf(0x1f191, 0x1f19a), // Squared Cl ..Squared Vs
|
||||
intArrayOf(0x1f200, 0x1f202), // Square Hiragana Hoka ..Squared Katakana Sa
|
||||
intArrayOf(0x1f210, 0x1f23b), // Squared Cjk Unified Ideo..Squared Cjk Unified Ideo
|
||||
intArrayOf(0x1f240, 0x1f248), // Tortoise Shell Bracketed..Tortoise Shell Bracketed
|
||||
intArrayOf(0x1f250, 0x1f251), // Circled Ideograph Advant..Circled Ideograph Accept
|
||||
intArrayOf(0x1f260, 0x1f265), // Rounded Symbol For Fu ..Rounded Symbol For Cai
|
||||
intArrayOf(0x1f300, 0x1f320), // Cyclone ..Shooting Star
|
||||
intArrayOf(0x1f32d, 0x1f335), // Hot Dog ..Cactus
|
||||
intArrayOf(0x1f337, 0x1f37c), // Tulip ..Baby Bottle
|
||||
intArrayOf(0x1f37e, 0x1f393), // Bottle With Popping Cork..Graduation Cap
|
||||
intArrayOf(0x1f3a0, 0x1f3ca), // Carousel Horse ..Swimmer
|
||||
intArrayOf(0x1f3cf, 0x1f3d3), // Cricket Bat And Ball ..Table Tennis Paddle And
|
||||
intArrayOf(0x1f3e0, 0x1f3f0), // House Building ..European Castle
|
||||
intArrayOf(0x1f3f4, 0x1f3f4), // Waving Black Flag ..Waving Black Flag
|
||||
intArrayOf(0x1f3f8, 0x1f43e), // Badminton Racquet And Sh..Paw Prints
|
||||
intArrayOf(0x1f440, 0x1f440), // Eyes ..Eyes
|
||||
intArrayOf(0x1f442, 0x1f4fc), // Ear ..Videocassette
|
||||
intArrayOf(0x1f4ff, 0x1f53d), // Prayer Beads ..Down-pointing Small Red
|
||||
intArrayOf(0x1f54b, 0x1f54e), // Kaaba ..Menorah With Nine Branch
|
||||
intArrayOf(0x1f550, 0x1f567), // Clock Face One Oclock ..Clock Face Twelve-thirty
|
||||
intArrayOf(0x1f57a, 0x1f57a), // Man Dancing ..Man Dancing
|
||||
intArrayOf(0x1f595, 0x1f596), // Reversed Hand With Middl..Raised Hand With Part Be
|
||||
intArrayOf(0x1f5a4, 0x1f5a4), // Black Heart ..Black Heart
|
||||
intArrayOf(0x1f5fb, 0x1f64f), // Mount Fuji ..Person With Folded Hands
|
||||
intArrayOf(0x1f680, 0x1f6c5), // Rocket ..Left Luggage
|
||||
intArrayOf(0x1f6cc, 0x1f6cc), // Sleeping Accommodation ..Sleeping Accommodation
|
||||
intArrayOf(0x1f6d0, 0x1f6d2), // Place Of Worship ..Shopping Trolley
|
||||
intArrayOf(0x1f6d5, 0x1f6d7), // Hindu Temple ..Elevator
|
||||
intArrayOf(0x1f6dc, 0x1f6df), // (nil) ..Ring Buoy
|
||||
intArrayOf(0x1f6eb, 0x1f6ec), // Airplane Departure ..Airplane Arriving
|
||||
intArrayOf(0x1f6f4, 0x1f6fc), // Scooter ..Roller Skate
|
||||
intArrayOf(0x1f7e0, 0x1f7eb), // Large Orange Circle ..Large Brown Square
|
||||
intArrayOf(0x1f7f0, 0x1f7f0), // Heavy Equals Sign ..Heavy Equals Sign
|
||||
intArrayOf(0x1f90c, 0x1f93a), // Pinched Fingers ..Fencer
|
||||
intArrayOf(0x1f93c, 0x1f945), // Wrestlers ..Goal Net
|
||||
intArrayOf(0x1f947, 0x1f9ff), // First Place Medal ..Nazar Amulet
|
||||
intArrayOf(0x1fa70, 0x1fa7c), // Ballet Shoes ..Crutch
|
||||
intArrayOf(0x1fa80, 0x1fa88), // Yo-yo ..(nil)
|
||||
intArrayOf(0x1fa90, 0x1fabd), // Ringed Planet ..(nil)
|
||||
intArrayOf(0x1fabf, 0x1fac5), // (nil) ..Person With Crown
|
||||
intArrayOf(0x1face, 0x1fadb), // (nil) ..(nil)
|
||||
intArrayOf(0x1fae0, 0x1fae8), // Melting Face ..(nil)
|
||||
intArrayOf(0x1faf0, 0x1faf8), // Hand With Index Finger A..(nil)
|
||||
intArrayOf(0x20000, 0x2fffd), // Cjk Unified Ideograph-20..(nil)
|
||||
intArrayOf(0x30000, 0x3fffd), // Cjk Unified Ideograph-30..(nil)
|
||||
)
|
||||
|
||||
private fun intable(table: Array<IntArray>, c: Int): Boolean {
|
||||
if (c < table[0][0]) return false
|
||||
var bot = 0
|
||||
var top = table.size - 1
|
||||
while (top >= bot) {
|
||||
val mid = (bot + top) / 2
|
||||
if (table[mid][1] < c) {
|
||||
bot = mid + 1
|
||||
} else if (table[mid][0] > c) {
|
||||
top = mid - 1
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Return the terminal display width of a code point: 0, 1 or 2. */
|
||||
fun width(ucs: Int): Int {
|
||||
if (ucs == 0 ||
|
||||
ucs == 0x034F ||
|
||||
(ucs in 0x200B..0x200F) ||
|
||||
ucs == 0x2028 ||
|
||||
ucs == 0x2029 ||
|
||||
(ucs in 0x202A..0x202E) ||
|
||||
(ucs in 0x2060..0x2063)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// C0/C1 control characters
|
||||
// Termux change: Return 0 instead of -1.
|
||||
if (ucs < 32 || (ucs in 0x07F until 0x0A0)) return 0
|
||||
|
||||
if (intable(ZERO_WIDTH, ucs)) return 0
|
||||
|
||||
return if (intable(WIDE_EASTASIAN, ucs)) 2 else 1
|
||||
}
|
||||
|
||||
/** The width at an index position in a java char array. */
|
||||
fun width(chars: CharArray, index: Int): Int {
|
||||
val c = chars[index]
|
||||
return if (Character.isHighSurrogate(c)) width(Character.toCodePoint(c, chars[index + 1])) else width(c.code)
|
||||
}
|
||||
|
||||
/**
|
||||
* The zero width characters count like combining characters in the `chars` array from start
|
||||
* index to end index (exclusive).
|
||||
*/
|
||||
fun zeroWidthCharsCount(chars: CharArray, start: Int, end: Int): Int {
|
||||
if (start < 0 || start >= chars.size) return 0
|
||||
var count = 0
|
||||
var i = start
|
||||
while (i < end && i < chars.size) {
|
||||
if (Character.isHighSurrogate(chars[i])) {
|
||||
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
|
||||
count++
|
||||
}
|
||||
i += 2
|
||||
} else {
|
||||
if (width(chars[i].code) <= 0) {
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
}
|
||||
361
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
361
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
@@ -0,0 +1,361 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.topjohnwu.magisk.arch.VMFactory
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.ActivityExtension
|
||||
import com.topjohnwu.magisk.core.base.SplashController
|
||||
import com.topjohnwu.magisk.core.base.SplashScreenHost
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ui.deny.DenyListScreen
|
||||
import com.topjohnwu.magisk.ui.deny.DenyListViewModel
|
||||
import com.topjohnwu.magisk.ui.flash.FlashScreen
|
||||
import com.topjohnwu.magisk.ui.flash.FlashUtils
|
||||
import com.topjohnwu.magisk.ui.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ActionScreen
|
||||
import com.topjohnwu.magisk.ui.module.ActionViewModel
|
||||
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
|
||||
import com.topjohnwu.magisk.ui.navigation.Navigator
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.ui.navigation.rememberNavigator
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserDetailScreen
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.theme.MagiskTheme
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class MainActivity : AppCompatActivity(), SplashScreenHost {
|
||||
|
||||
override val extension = ActivityExtension(this)
|
||||
override val splashController = SplashController(this)
|
||||
|
||||
private val intentState = MutableStateFlow(0)
|
||||
internal val showInvalidState = MutableStateFlow(false)
|
||||
internal val showUnsupported = MutableStateFlow<List<Pair<Int, Int>>>(emptyList())
|
||||
internal val showShortcutPrompt = MutableStateFlow(false)
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
extension.onCreate(savedInstanceState)
|
||||
if (isRunningAsStub) {
|
||||
val delegate = delegate
|
||||
val clz = delegate.javaClass
|
||||
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||
}
|
||||
|
||||
setTheme(Theme.selected.themeRes)
|
||||
splashController.preOnCreate()
|
||||
super.onCreate(savedInstanceState)
|
||||
splashController.onCreate(savedInstanceState)
|
||||
|
||||
setupWindow()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
splashController.onResume()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
extension.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun setupWindow() {
|
||||
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||
.use { it.getDrawable(0) }
|
||||
.also { window.setBackgroundDrawable(it) }
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
window?.decorView?.post {
|
||||
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
|
||||
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
window.isStatusBarContrastEnforced = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun onCreateUi(savedInstanceState: Bundle?) {
|
||||
showUnsupportedMessage()
|
||||
askForHomeShortcut()
|
||||
|
||||
if (Config.checkUpdate) {
|
||||
extension.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||
Config.checkUpdate = it
|
||||
}
|
||||
}
|
||||
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
val initialTab = getInitialTab(intent)
|
||||
|
||||
setContent {
|
||||
MagiskTheme {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
val navigator = rememberNavigator(Route.Main)
|
||||
CompositionLocalProvider(LocalNavigator provides navigator) {
|
||||
HandleFlashIntent(navigator)
|
||||
|
||||
NavDisplay(
|
||||
backStack = navigator.backStack,
|
||||
onBack = { navigator.pop() },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator<Any>()
|
||||
),
|
||||
entryProvider = entryProvider {
|
||||
entry<Route.Main> {
|
||||
MainScreen(initialTab = initialTab)
|
||||
}
|
||||
entry<Route.DenyList> { _ ->
|
||||
val vm: DenyListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
DenyListScreen(vm, onBack = { navigator.pop() })
|
||||
}
|
||||
entry<Route.Flash> { key ->
|
||||
val vm: FlashViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
|
||||
LaunchedEffect(key) {
|
||||
if (vm.flashAction.isEmpty()) {
|
||||
vm.flashAction = key.action
|
||||
vm.flashUri = key.additionalData?.let { Uri.parse(it) }
|
||||
vm.startFlashing()
|
||||
}
|
||||
}
|
||||
FlashScreen(vm, action = key.action, onBack = { navigator.pop() })
|
||||
}
|
||||
entry<Route.SuperuserDetail> { key ->
|
||||
val vm: SuperuserViewModel = androidx.lifecycle.viewmodel.compose.viewModel(
|
||||
viewModelStoreOwner = this@MainActivity, factory = VMFactory
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
vm.authenticate = { onSuccess ->
|
||||
extension.withAuthentication { if (it) onSuccess() }
|
||||
}
|
||||
}
|
||||
SuperuserDetailScreen(uid = key.uid, viewModel = vm, onBack = { navigator.pop() })
|
||||
}
|
||||
entry<Route.Action> { key ->
|
||||
val vm: ActionViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
|
||||
LaunchedEffect(key) {
|
||||
if (vm.actionId.isEmpty()) {
|
||||
vm.actionId = key.id
|
||||
vm.actionName = key.name
|
||||
vm.startRunAction()
|
||||
}
|
||||
}
|
||||
ActionScreen(vm, actionName = key.name, onBack = { navigator.pop() })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
MainActivityDialogs(activity = this@MainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HandleFlashIntent(navigator: Navigator) {
|
||||
val intentVersion by intentState.collectAsState()
|
||||
LaunchedEffect(intentVersion) {
|
||||
val currentIntent = intent ?: return@LaunchedEffect
|
||||
if (currentIntent.action == FlashUtils.INTENT_FLASH) {
|
||||
val action = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_ACTION)
|
||||
?: return@LaunchedEffect
|
||||
val uri = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_URI)
|
||||
navigator.push(Route.Flash(action, uri))
|
||||
currentIntent.action = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
intentState.value += 1
|
||||
}
|
||||
|
||||
private fun getInitialTab(intent: Intent?): Int {
|
||||
val section = if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) {
|
||||
Const.Nav.SETTINGS
|
||||
} else {
|
||||
intent?.getStringExtra(Const.Key.OPEN_SECTION)
|
||||
}
|
||||
return when (section) {
|
||||
Const.Nav.SUPERUSER -> Tab.SUPERUSER.ordinal
|
||||
Const.Nav.MODULES -> Tab.MODULES.ordinal
|
||||
Const.Nav.SETTINGS -> Tab.SETTINGS.ordinal
|
||||
else -> Tab.HOME.ordinal
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun showInvalidStateMessage() {
|
||||
showInvalidState.value = true
|
||||
}
|
||||
|
||||
internal fun handleInvalidStateInstall() {
|
||||
extension.withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||
if (!it) {
|
||||
toast(CoreR.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
||||
showInvalidState.value = true
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
if (!AppMigration.restoreApp(this@MainActivity)) {
|
||||
toast(CoreR.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsupportedMessage() {
|
||||
val messages = mutableListOf<Pair<Int, Int>>()
|
||||
|
||||
if (Info.env.isUnsupported) {
|
||||
messages.add(CoreR.string.unsupport_magisk_title to CoreR.string.unsupport_magisk_msg)
|
||||
}
|
||||
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
|
||||
?.split(':')
|
||||
?.filterNot { java.io.File("$it/magisk").exists() }
|
||||
?.any { java.io.File("$it/su").exists() } == true) {
|
||||
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_other_su_msg)
|
||||
}
|
||||
if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
|
||||
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_system_app_msg)
|
||||
}
|
||||
if (applicationInfo.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0) {
|
||||
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_external_storage_msg)
|
||||
}
|
||||
|
||||
if (messages.isNotEmpty()) {
|
||||
showUnsupported.value = messages
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForHomeShortcut() {
|
||||
if (isRunningAsStub && !Config.askedHome &&
|
||||
ShortcutManagerCompat.isRequestPinShortcutSupported(this)) {
|
||||
Config.askedHome = true
|
||||
showShortcutPrompt.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainActivityDialogs(activity: MainActivity) {
|
||||
val showInvalid by activity.showInvalidState.collectAsState()
|
||||
val unsupportedMessages by activity.showUnsupported.collectAsState()
|
||||
val showShortcut by activity.showShortcutPrompt.collectAsState()
|
||||
|
||||
val invalidDialog = com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
activity.showInvalidState.value = false
|
||||
activity.handleInvalidStateInstall()
|
||||
},
|
||||
onDismiss = {}
|
||||
)
|
||||
|
||||
LaunchedEffect(showInvalid) {
|
||||
if (showInvalid) {
|
||||
invalidDialog.showConfirm(
|
||||
title = activity.getString(CoreR.string.unsupport_nonroot_stub_title),
|
||||
content = activity.getString(CoreR.string.unsupport_nonroot_stub_msg),
|
||||
confirm = activity.getString(CoreR.string.install),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for ((index, pair) in unsupportedMessages.withIndex()) {
|
||||
val (titleRes, msgRes) = pair
|
||||
val show = rememberSaveable { androidx.compose.runtime.mutableStateOf(true) }
|
||||
com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
|
||||
onConfirm = { show.value = false },
|
||||
).also { dialog ->
|
||||
LaunchedEffect(Unit) {
|
||||
dialog.showConfirm(
|
||||
title = activity.getString(titleRes),
|
||||
content = activity.getString(msgRes),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val shortcutDialog = com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
activity.showShortcutPrompt.value = false
|
||||
Shortcuts.addHomeIcon(activity)
|
||||
},
|
||||
onDismiss = { activity.showShortcutPrompt.value = false }
|
||||
)
|
||||
|
||||
LaunchedEffect(showShortcut) {
|
||||
if (showShortcut) {
|
||||
shortcutDialog.showConfirm(
|
||||
title = activity.getString(CoreR.string.add_shortcut_title),
|
||||
content = activity.getString(CoreR.string.add_shortcut_msg),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
227
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt
Normal file
227
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt
Normal file
@@ -0,0 +1,227 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.VMFactory
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.ui.home.HomeScreen
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogScreen
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ModuleScreen
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.ui.navigation.CollectNavEvents
|
||||
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsScreen
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserScreen
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
enum class Tab(val titleRes: Int, val iconRes: Int) {
|
||||
MODULES(CoreR.string.modules, R.drawable.ic_module_outlined_md2),
|
||||
SUPERUSER(CoreR.string.superuser, R.drawable.ic_superuser_outlined_md2),
|
||||
HOME(CoreR.string.section_home, R.drawable.ic_home_outlined_md2),
|
||||
LOG(CoreR.string.logs, R.drawable.ic_bug_outlined_md2),
|
||||
SETTINGS(CoreR.string.settings, R.drawable.ic_settings_outlined_md2);
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(initialTab: Int = Tab.HOME.ordinal) {
|
||||
val navigator = LocalNavigator.current
|
||||
val visibleTabs = remember {
|
||||
Tab.entries.filter { tab ->
|
||||
when (tab) {
|
||||
Tab.SUPERUSER -> Info.showSuperUser
|
||||
Tab.MODULES -> Info.env.isActive && LocalModule.loaded()
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
val initialPage = visibleTabs.indexOf(Tab.entries[initialTab]).coerceAtLeast(0)
|
||||
val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { visibleTabs.size })
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
beyondViewportPageCount = visibleTabs.size - 1,
|
||||
userScrollEnabled = true,
|
||||
) { page ->
|
||||
when (visibleTabs[page]) {
|
||||
Tab.HOME -> {
|
||||
val vm: HomeViewModel = viewModel(factory = VMFactory)
|
||||
val installVm: InstallViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
CollectNavEvents(vm, navigator)
|
||||
CollectNavEvents(installVm, navigator)
|
||||
HomeScreen(vm, installVm)
|
||||
}
|
||||
Tab.SUPERUSER -> {
|
||||
val activity = LocalContext.current as MainActivity
|
||||
val vm: SuperuserViewModel = viewModel(viewModelStoreOwner = activity, factory = VMFactory)
|
||||
LaunchedEffect(Unit) {
|
||||
vm.authenticate = { onSuccess ->
|
||||
activity.extension.withAuthentication { if (it) onSuccess() }
|
||||
}
|
||||
vm.startLoading()
|
||||
}
|
||||
SuperuserScreen(vm)
|
||||
}
|
||||
Tab.LOG -> {
|
||||
val vm: LogViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
LogScreen(vm)
|
||||
}
|
||||
Tab.MODULES -> {
|
||||
val vm: ModuleViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
CollectNavEvents(vm, navigator)
|
||||
ModuleScreen(vm)
|
||||
}
|
||||
Tab.SETTINGS -> {
|
||||
val activity = LocalContext.current as MainActivity
|
||||
val vm: SettingsViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) {
|
||||
vm.authenticate = { onSuccess ->
|
||||
activity.extension.withAuthentication { if (it) onSuccess() }
|
||||
}
|
||||
}
|
||||
CollectNavEvents(vm, navigator)
|
||||
SettingsScreen(vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingNavigationBar(
|
||||
pagerState = pagerState,
|
||||
visibleTabs = visibleTabs,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingNavigationBar(
|
||||
pagerState: PagerState,
|
||||
visibleTabs: List<Tab>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val shape = RoundedCornerShape(28.dp)
|
||||
val navBarInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(bottom = navBarInset + 12.dp, start = 24.dp, end = 24.dp)
|
||||
.shadow(elevation = 6.dp, shape = shape)
|
||||
.clip(shape)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
visibleTabs.forEachIndexed { index, tab ->
|
||||
FloatingNavItem(
|
||||
icon = ImageVector.vectorResource(tab.iconRes),
|
||||
label = stringResource(tab.titleRes),
|
||||
selected = pagerState.currentPage == index,
|
||||
enabled = true,
|
||||
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingNavItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
selected -> MaterialTheme.colorScheme.primary
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
animationSpec = tween(200),
|
||||
label = "navItemColor"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
role = Role.Tab,
|
||||
onClick = onClick,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = contentColor,
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 11.sp,
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package com.topjohnwu.magisk.ui.component
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
sealed interface ConfirmResult {
|
||||
data object Confirmed : ConfirmResult
|
||||
data object Canceled : ConfirmResult
|
||||
}
|
||||
|
||||
data class DialogVisuals(
|
||||
val title: String = "",
|
||||
val content: String? = null,
|
||||
val markdown: Boolean = false,
|
||||
val confirm: String? = null,
|
||||
val dismiss: String? = null,
|
||||
)
|
||||
|
||||
interface LoadingDialogHandle {
|
||||
suspend fun <R> withLoading(block: suspend () -> R): R
|
||||
}
|
||||
|
||||
interface ConfirmDialogHandle {
|
||||
fun showConfirm(
|
||||
title: String,
|
||||
content: String? = null,
|
||||
markdown: Boolean = false,
|
||||
confirm: String? = null,
|
||||
dismiss: String? = null
|
||||
)
|
||||
|
||||
suspend fun awaitConfirm(
|
||||
title: String,
|
||||
content: String? = null,
|
||||
markdown: Boolean = false,
|
||||
confirm: String? = null,
|
||||
dismiss: String? = null
|
||||
): ConfirmResult
|
||||
}
|
||||
|
||||
private class LoadingDialogHandleImpl(
|
||||
private val visible: MutableState<Boolean>,
|
||||
private val coroutineScope: CoroutineScope
|
||||
) : LoadingDialogHandle {
|
||||
override suspend fun <R> withLoading(block: suspend () -> R): R {
|
||||
return coroutineScope.async {
|
||||
try {
|
||||
visible.value = true
|
||||
block()
|
||||
} finally {
|
||||
visible.value = false
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfirmDialogHandleImpl(
|
||||
private val visible: MutableState<Boolean>,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val callback: ConfirmCallback,
|
||||
private val resultChannel: Channel<ConfirmResult>
|
||||
) : ConfirmDialogHandle {
|
||||
|
||||
var visuals by mutableStateOf(DialogVisuals())
|
||||
private set
|
||||
|
||||
private var awaitContinuation: CancellableContinuation<ConfirmResult>? = null
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
resultChannel
|
||||
.consumeAsFlow()
|
||||
.onEach { result ->
|
||||
awaitContinuation?.let {
|
||||
awaitContinuation = null
|
||||
if (it.isActive) it.resume(result)
|
||||
}
|
||||
}
|
||||
.onEach { visible.value = false }
|
||||
.collect { result ->
|
||||
when (result) {
|
||||
ConfirmResult.Confirmed -> callback.onConfirm?.invoke()
|
||||
ConfirmResult.Canceled -> callback.onDismiss?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun showConfirm(
|
||||
title: String,
|
||||
content: String?,
|
||||
markdown: Boolean,
|
||||
confirm: String?,
|
||||
dismiss: String?
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
visuals = DialogVisuals(title, content, markdown, confirm, dismiss)
|
||||
visible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun awaitConfirm(
|
||||
title: String,
|
||||
content: String?,
|
||||
markdown: Boolean,
|
||||
confirm: String?,
|
||||
dismiss: String?
|
||||
): ConfirmResult {
|
||||
coroutineScope.launch {
|
||||
visuals = DialogVisuals(title, content, markdown, confirm, dismiss)
|
||||
visible.value = true
|
||||
}
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
awaitContinuation = cont.apply {
|
||||
invokeOnCancellation { visible.value = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ConfirmCallback {
|
||||
val onConfirm: (() -> Unit)?
|
||||
val onDismiss: (() -> Unit)?
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmCallback(
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
onDismiss: (() -> Unit)? = null
|
||||
): ConfirmCallback {
|
||||
val currentOnConfirm by rememberUpdatedState(onConfirm)
|
||||
val currentOnDismiss by rememberUpdatedState(onDismiss)
|
||||
return remember {
|
||||
object : ConfirmCallback {
|
||||
override val onConfirm get() = currentOnConfirm
|
||||
override val onDismiss get() = currentOnDismiss
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberLoadingDialog(): LoadingDialogHandle {
|
||||
val visible = remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
LoadingDialog(visible)
|
||||
return remember { LoadingDialogHandleImpl(visible, scope) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmDialog(
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
onDismiss: (() -> Unit)? = null
|
||||
): ConfirmDialogHandle {
|
||||
return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle {
|
||||
val visible = rememberSaveable { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val resultChannel = remember { Channel<ConfirmResult>() }
|
||||
|
||||
val handle = remember {
|
||||
ConfirmDialogHandleImpl(visible, scope, callback, resultChannel)
|
||||
}
|
||||
|
||||
if (visible.value) {
|
||||
ConfirmDialogContent(
|
||||
visuals = handle.visuals,
|
||||
confirm = { scope.launch { resultChannel.send(ConfirmResult.Confirmed) } },
|
||||
dismiss = { scope.launch { resultChannel.send(ConfirmResult.Canceled) } },
|
||||
showDialog = visible
|
||||
)
|
||||
}
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingDialog(showDialog: MutableState<Boolean>) {
|
||||
if (showDialog.value) {
|
||||
Dialog(
|
||||
onDismissRequest = {},
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
|
||||
) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 6.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(24.dp).fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
text = stringResource(com.topjohnwu.magisk.core.R.string.loading),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmDialogContent(
|
||||
visuals: DialogVisuals,
|
||||
confirm: () -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
showDialog: MutableState<Boolean>
|
||||
) {
|
||||
if (showDialog.value) {
|
||||
AlertDialog(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
showDialog.value = false
|
||||
},
|
||||
title = if (visuals.title.isNotEmpty()) {
|
||||
{ Text(text = visuals.title) }
|
||||
} else null,
|
||||
text = {
|
||||
visuals.content?.let { content ->
|
||||
if (visuals.markdown) {
|
||||
MarkdownText(content)
|
||||
} else {
|
||||
Text(
|
||||
text = content,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
confirm()
|
||||
showDialog.value = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = visuals.confirm ?: stringResource(android.R.string.ok)
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
showDialog.value = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = visuals.dismiss ?: stringResource(android.R.string.cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkdownText(text: String) {
|
||||
val contentColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
TextView(context).apply {
|
||||
setTextColor(contentColor)
|
||||
ServiceLocator.markwon.setMarkdown(this, text)
|
||||
}
|
||||
},
|
||||
update = { textView ->
|
||||
textView.setTextColor(contentColor)
|
||||
ServiceLocator.markwon.setMarkdown(textView, text)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 300.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkdownTextAsync(getMarkdownText: suspend () -> String) {
|
||||
var mdText by remember { mutableStateOf<String?>(null) }
|
||||
var error by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
mdText = withContext(Dispatchers.IO) { getMarkdownText() }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
error = true
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
error -> Text(stringResource(com.topjohnwu.magisk.core.R.string.download_file_error))
|
||||
mdText != null -> MarkdownText(mdText!!)
|
||||
else -> Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(color = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.topjohnwu.magisk.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SettingsArrow(
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
leadingContent: @Composable (() -> Unit)? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = summary?.let { { Text(it) } },
|
||||
leadingContent = leadingContent,
|
||||
trailingContent = { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null) },
|
||||
modifier = Modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsSwitch(
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = summary?.takeIf { it.isNotEmpty() }?.let { { Text(it) } },
|
||||
trailingContent = { Switch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled) },
|
||||
modifier = Modifier.clickable(enabled = enabled, onClick = { onCheckedChange(!checked) })
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsDropdown(
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
items: List<String>,
|
||||
selectedIndex: Int,
|
||||
enabled: Boolean = true,
|
||||
onSelectedIndexChange: (Int) -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
ListItem(
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = {
|
||||
val currentSummary = summary ?: items.getOrNull(selectedIndex) ?: ""
|
||||
if (currentSummary.isNotEmpty()) Text(currentSummary)
|
||||
},
|
||||
trailingContent = { Icon(Icons.Default.ArrowDropDown, contentDescription = null) },
|
||||
modifier = Modifier.clickable(enabled = enabled, onClick = { expanded = true })
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
items.forEachIndexed { index, item ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(item) },
|
||||
onClick = {
|
||||
onSelectedIndexChange(index)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SmallTitle(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.topjohnwu.magisk.ui.deny
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.GET_ACTIVITIES
|
||||
import android.content.pm.PackageManager.GET_PROVIDERS
|
||||
import android.content.pm.PackageManager.GET_RECEIVERS
|
||||
import android.content.pm.PackageManager.GET_SERVICES
|
||||
import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.os.ProcessCompat
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import java.util.Locale
|
||||
import java.util.TreeSet
|
||||
|
||||
class CmdlineListItem(line: String) {
|
||||
val packageName: String
|
||||
val process: String
|
||||
|
||||
init {
|
||||
val split = line.split(Regex("\\|"), 2)
|
||||
packageName = split[0]
|
||||
process = split.getOrElse(1) { packageName }
|
||||
}
|
||||
}
|
||||
|
||||
const val ISOLATED_MAGIC = "isolated"
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
class AppProcessInfo(
|
||||
private val info: ApplicationInfo,
|
||||
pm: PackageManager,
|
||||
denyList: List<CmdlineListItem>
|
||||
) : Comparable<AppProcessInfo> {
|
||||
|
||||
private val denyList = denyList.filter {
|
||||
it.packageName == info.packageName || it.packageName == ISOLATED_MAGIC
|
||||
}
|
||||
|
||||
val label = info.getLabel(pm)
|
||||
val iconImage: Drawable = runCatching { info.loadIcon(pm) }.getOrDefault(pm.defaultActivityIcon)
|
||||
val packageName: String get() = info.packageName
|
||||
var firstInstallTime: Long = 0L
|
||||
private set
|
||||
var lastUpdateTime: Long = 0L
|
||||
private set
|
||||
val processes = fetchProcesses(pm)
|
||||
|
||||
override fun compareTo(other: AppProcessInfo) = comparator.compare(this, other)
|
||||
|
||||
fun isSystemApp() = info.flags and ApplicationInfo.FLAG_SYSTEM != 0
|
||||
|
||||
fun isApp() = ProcessCompat.isApplicationUid(info.uid)
|
||||
|
||||
private fun createProcess(name: String, pkg: String = info.packageName) =
|
||||
ProcessInfo(name, pkg, denyList.any { it.process == name && it.packageName == pkg })
|
||||
|
||||
private fun ComponentInfo.getProcName(): String = processName
|
||||
?: applicationInfo.processName
|
||||
?: applicationInfo.packageName
|
||||
|
||||
private val ServiceInfo.isIsolated get() = (flags and ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
|
||||
private val ServiceInfo.useAppZygote get() = (flags and ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0
|
||||
|
||||
private fun Array<out ComponentInfo>?.toProcessList() =
|
||||
orEmpty().map { createProcess(it.getProcName()) }
|
||||
|
||||
private fun Array<ServiceInfo>?.toProcessList(): List<ProcessInfo> {
|
||||
if (this == null) return emptyList()
|
||||
val result = mutableListOf<ProcessInfo>()
|
||||
var hasIsolated = false
|
||||
for (si in this) {
|
||||
if (si.isIsolated) {
|
||||
if (si.useAppZygote) {
|
||||
val proc = info.processName ?: info.packageName
|
||||
result.add(createProcess("${proc}_zygote"))
|
||||
} else {
|
||||
hasIsolated = true
|
||||
}
|
||||
} else {
|
||||
result.add(createProcess(si.getProcName()))
|
||||
}
|
||||
}
|
||||
if (hasIsolated) {
|
||||
val prefix = "${info.processName ?: info.packageName}:"
|
||||
val isEnabled = denyList.any {
|
||||
it.packageName == ISOLATED_MAGIC && it.process.startsWith(prefix)
|
||||
}
|
||||
result.add(ProcessInfo(prefix, ISOLATED_MAGIC, isEnabled))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun fetchProcesses(pm: PackageManager): Collection<ProcessInfo> {
|
||||
val flag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES or
|
||||
GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
|
||||
val packageInfo = try {
|
||||
pm.getPackageInfo(info.packageName, flag)
|
||||
} catch (e: Exception) {
|
||||
// Exceed binder data transfer limit, parse the package locally
|
||||
pm.getPackageArchiveInfo(info.sourceDir, flag) ?: return emptyList()
|
||||
}
|
||||
|
||||
firstInstallTime = packageInfo.firstInstallTime
|
||||
lastUpdateTime = packageInfo.lastUpdateTime
|
||||
|
||||
val processSet = TreeSet<ProcessInfo>(compareBy({ it.name }, { it.isIsolated }))
|
||||
processSet += packageInfo.activities.toProcessList()
|
||||
processSet += packageInfo.services.toProcessList()
|
||||
processSet += packageInfo.receivers.toProcessList()
|
||||
processSet += packageInfo.providers.toProcessList()
|
||||
return processSet
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val comparator = compareBy<AppProcessInfo>(
|
||||
{ it.label.lowercase(Locale.ROOT) },
|
||||
{ it.info.packageName }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ProcessInfo(
|
||||
val name: String,
|
||||
val packageName: String,
|
||||
var isEnabled: Boolean
|
||||
) {
|
||||
val isIsolated = packageName == ISOLATED_MAGIC
|
||||
val isAppZygote = name.endsWith("_zygote")
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package com.topjohnwu.magisk.ui.deny
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TriStateCheckbox
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DenyListScreen(viewModel: DenyListViewModel, onBack: () -> Unit) {
|
||||
val loading by viewModel.loading.collectAsState()
|
||||
val apps by viewModel.filteredApps.collectAsState()
|
||||
val query by viewModel.query.collectAsState()
|
||||
val showSystem by viewModel.showSystem.collectAsState()
|
||||
val showOS by viewModel.showOS.collectAsState()
|
||||
val sortBy by viewModel.sortBy.collectAsState()
|
||||
val sortReverse by viewModel.sortReverse.collectAsState()
|
||||
|
||||
val showSortMenu = remember { mutableStateOf(false) }
|
||||
val showFilterMenu = remember { mutableStateOf(false) }
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(CoreR.string.denylist)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = { showSortMenu.value = true },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = stringResource(CoreR.string.menu_sort),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showSortMenu.value,
|
||||
onDismissRequest = { showSortMenu.value = false }
|
||||
) {
|
||||
val sortOptions = listOf(
|
||||
CoreR.string.sort_by_name to SortBy.NAME,
|
||||
CoreR.string.sort_by_package_name to SortBy.PACKAGE_NAME,
|
||||
CoreR.string.sort_by_install_time to SortBy.INSTALL_TIME,
|
||||
CoreR.string.sort_by_update_time to SortBy.UPDATE_TIME,
|
||||
)
|
||||
sortOptions.forEach { (resId, sort) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(resId)) },
|
||||
trailingIcon = if (sortBy == sort) {
|
||||
{ Icon(androidx.compose.material.icons.Icons.Default.Check, contentDescription = null) }
|
||||
} else null,
|
||||
onClick = {
|
||||
viewModel.setSortBy(sort)
|
||||
showSortMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(CoreR.string.sort_reverse)) },
|
||||
trailingIcon = if (sortReverse) {
|
||||
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||
} else null,
|
||||
onClick = {
|
||||
viewModel.toggleSortReverse()
|
||||
showSortMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { showFilterMenu.value = true },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Tune,
|
||||
contentDescription = stringResource(CoreR.string.hide_filter_hint),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showFilterMenu.value,
|
||||
onDismissRequest = { showFilterMenu.value = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(CoreR.string.show_system_app)) },
|
||||
trailingIcon = if (showSystem) {
|
||||
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||
} else null,
|
||||
onClick = {
|
||||
viewModel.setShowSystem(!showSystem)
|
||||
showFilterMenu.value = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(CoreR.string.show_os_app)) },
|
||||
trailingIcon = if (showOS) {
|
||||
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||
} else null,
|
||||
onClick = {
|
||||
if (!showOS && !showSystem) {
|
||||
viewModel.setShowSystem(true)
|
||||
}
|
||||
viewModel.setShowOS(!showOS)
|
||||
showFilterMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
SearchInput(
|
||||
query = query,
|
||||
onQueryChange = viewModel::setQuery,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.loading),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
items = apps,
|
||||
key = { it.info.packageName }
|
||||
) { app ->
|
||||
DenyAppCard(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchInput(query: String, onQueryChange: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
OutlinedTextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
modifier = modifier,
|
||||
label = { Text(stringResource(CoreR.string.hide_filter_hint)) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DenyAppCard(app: DenyAppState) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
if (app.checkedPercent > 0f) {
|
||||
LinearProgressIndicator(
|
||||
progress = app.checkedPercent,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { app.isExpanded = !app.isExpanded }
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(app.info.iconImage),
|
||||
contentDescription = app.info.label,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = app.info.label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
Text(
|
||||
text = app.info.packageName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
TriStateCheckbox(
|
||||
state = when {
|
||||
app.itemsChecked == 0 -> ToggleableState.Off
|
||||
app.checkedPercent < 1f -> ToggleableState.Indeterminate
|
||||
else -> ToggleableState.On
|
||||
},
|
||||
onClick = { app.toggleAll() }
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = app.isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 52.dp)
|
||||
) {
|
||||
app.processes.forEach { proc ->
|
||||
ProcessRow(proc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProcessRow(proc: DenyProcessState) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { proc.toggle() }
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = proc.displayName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (proc.isEnabled) MaterialTheme.colorScheme.onSurface
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Checkbox(
|
||||
checked = proc.isEnabled,
|
||||
onCheckedChange = { proc.toggle() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.topjohnwu.magisk.ui.deny
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.concurrentMap
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.toCollection
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
enum class SortBy { NAME, PACKAGE_NAME, INSTALL_TIME, UPDATE_TIME }
|
||||
|
||||
class DenyListViewModel : AsyncLoadViewModel() {
|
||||
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading: StateFlow<Boolean> = _loading.asStateFlow()
|
||||
|
||||
private val _allApps = MutableStateFlow<List<DenyAppState>>(emptyList())
|
||||
|
||||
private val _query = MutableStateFlow("")
|
||||
val query: StateFlow<String> = _query.asStateFlow()
|
||||
|
||||
private val _showSystem = MutableStateFlow(false)
|
||||
val showSystem: StateFlow<Boolean> = _showSystem.asStateFlow()
|
||||
|
||||
private val _showOS = MutableStateFlow(false)
|
||||
val showOS: StateFlow<Boolean> = _showOS.asStateFlow()
|
||||
|
||||
private val _sortBy = MutableStateFlow(SortBy.NAME)
|
||||
val sortBy: StateFlow<SortBy> = _sortBy.asStateFlow()
|
||||
|
||||
private val _sortReverse = MutableStateFlow(false)
|
||||
val sortReverse: StateFlow<Boolean> = _sortReverse.asStateFlow()
|
||||
|
||||
val filteredApps: StateFlow<List<DenyAppState>> = combine(
|
||||
_allApps, _query, _showSystem, _showOS, _sortBy, _sortReverse
|
||||
) { args ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val apps = args[0] as List<DenyAppState>
|
||||
val q = args[1] as String
|
||||
val showSys = args[2] as Boolean
|
||||
val showOS = args[3] as Boolean
|
||||
val sort = args[4] as SortBy
|
||||
val reverse = args[5] as Boolean
|
||||
|
||||
val filtered = apps.filter { app ->
|
||||
val passFilter = app.isChecked ||
|
||||
((showSys || !app.info.isSystemApp()) &&
|
||||
((showSys && showOS) || app.info.isApp()))
|
||||
val passQuery = q.isBlank() ||
|
||||
app.info.label.contains(q, true) ||
|
||||
app.info.packageName.contains(q, true) ||
|
||||
app.processes.any { it.process.name.contains(q, true) }
|
||||
passFilter && passQuery
|
||||
}
|
||||
|
||||
val secondary: Comparator<DenyAppState> = when (sort) {
|
||||
SortBy.NAME -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.info.label }
|
||||
SortBy.PACKAGE_NAME -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.info.packageName }
|
||||
SortBy.INSTALL_TIME -> compareByDescending { it.info.firstInstallTime }
|
||||
SortBy.UPDATE_TIME -> compareByDescending { it.info.lastUpdateTime }
|
||||
}
|
||||
val comparator = compareBy<DenyAppState> { it.itemsChecked == 0 }
|
||||
.then(if (reverse) secondary.reversed() else secondary)
|
||||
filtered.sortedWith(comparator)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
fun setQuery(q: String) { _query.value = q }
|
||||
fun setShowSystem(v: Boolean) {
|
||||
_showSystem.value = v
|
||||
if (!v) _showOS.value = false
|
||||
}
|
||||
fun setShowOS(v: Boolean) { _showOS.value = v }
|
||||
fun setSortBy(s: SortBy) { _sortBy.value = s }
|
||||
fun toggleSortReverse() { _sortReverse.value = !_sortReverse.value }
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override suspend fun doLoadWork() {
|
||||
_loading.value = true
|
||||
val apps = withContext(Dispatchers.Default) {
|
||||
val pm = AppContext.packageManager
|
||||
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
||||
.map { CmdlineListItem(it) }
|
||||
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES).run {
|
||||
asFlow()
|
||||
.filter { AppContext.packageName != it.packageName }
|
||||
.concurrentMap { AppProcessInfo(it, pm, denyList) }
|
||||
.filter { it.processes.isNotEmpty() }
|
||||
.concurrentMap { DenyAppState(it) }
|
||||
.toCollection(ArrayList(size))
|
||||
}
|
||||
apps.sortWith(compareBy(
|
||||
{ it.processes.count { p -> p.isEnabled } == 0 },
|
||||
{ it.info }
|
||||
))
|
||||
apps
|
||||
}
|
||||
_allApps.value = apps
|
||||
_loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
class DenyAppState(val info: AppProcessInfo) : Comparable<DenyAppState> {
|
||||
val processes = info.processes.map { DenyProcessState(it) }
|
||||
var isExpanded by mutableStateOf(false)
|
||||
|
||||
val itemsChecked: Int get() = processes.count { it.isEnabled }
|
||||
val isChecked: Boolean get() = itemsChecked > 0
|
||||
val checkedPercent: Float get() = if (processes.isEmpty()) 0f else itemsChecked.toFloat() / processes.size
|
||||
|
||||
fun toggleAll() {
|
||||
if (isChecked) {
|
||||
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
|
||||
processes.filter { it.isEnabled }.forEach { proc ->
|
||||
if (proc.process.isIsolated) {
|
||||
proc.toggle()
|
||||
} else {
|
||||
proc.isEnabled = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
processes.filterNot { it.isEnabled }.forEach { it.toggle() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: DenyAppState) = comparator.compare(this, other)
|
||||
|
||||
companion object {
|
||||
private val comparator = compareBy<DenyAppState>(
|
||||
{ it.itemsChecked == 0 },
|
||||
{ it.info }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DenyProcessState(val process: ProcessInfo) {
|
||||
var isEnabled by mutableStateOf(process.isEnabled)
|
||||
|
||||
val displayName: String =
|
||||
if (process.isIsolated) "(isolated) ${process.name}*" else process.name
|
||||
|
||||
fun toggle() {
|
||||
isEnabled = !isEnabled
|
||||
val arg = if (isEnabled) "add" else "rm"
|
||||
val (name, pkg) = process
|
||||
Shell.cmd("magisk --denylist $arg $pkg \'$name\'").submit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalScreen
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
@Composable
|
||||
fun FlashScreen(viewModel: FlashViewModel, action: String, onBack: () -> Unit) {
|
||||
val flashState by viewModel.flashState.collectAsState()
|
||||
val showReboot by viewModel.showReboot.collectAsState()
|
||||
val finished = flashState != FlashViewModel.State.FLASHING
|
||||
val useTerminal = action == Const.Value.FLASH_ZIP
|
||||
|
||||
val statusText = when (flashState) {
|
||||
FlashViewModel.State.FLASHING -> stringResource(CoreR.string.flashing)
|
||||
FlashViewModel.State.SUCCESS -> stringResource(CoreR.string.done)
|
||||
FlashViewModel.State.FAILED -> stringResource(CoreR.string.failure)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("${stringResource(CoreR.string.flash_screen_title)} - $statusText") },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (finished) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
onClick = { viewModel.saveLog() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_save_md2),
|
||||
contentDescription = stringResource(CoreR.string.menuSaveLog),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
if (flashState == FlashViewModel.State.SUCCESS && showReboot) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { viewModel.restartPressed() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_restart),
|
||||
contentDescription = stringResource(CoreR.string.reboot),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
if (useTerminal) {
|
||||
TerminalScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
onEmulatorCreated = { viewModel.onEmulatorCreated(it) },
|
||||
)
|
||||
} else {
|
||||
val items = viewModel.consoleItems
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(items.size) {
|
||||
if (items.isNotEmpty()) {
|
||||
listState.animateScrollToItem(items.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
itemsIndexed(items) { _, line ->
|
||||
Text(
|
||||
text = line,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.cmp
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
|
||||
object FlashUtils {
|
||||
|
||||
const val INTENT_FLASH = "com.topjohnwu.magisk.intent.FLASH"
|
||||
const val EXTRA_FLASH_ACTION = "flash_action"
|
||||
const val EXTRA_FLASH_URI = "flash_uri"
|
||||
|
||||
fun installIntent(context: Context, file: Uri): PendingIntent {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
component = MainActivity::class.java.cmp(context.packageName)
|
||||
action = INTENT_FLASH
|
||||
putExtra(EXTRA_FLASH_ACTION, Const.Value.FLASH_ZIP)
|
||||
putExtra(EXTRA_FLASH_URI, file.toString())
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context, file.hashCode(), intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.reboot
|
||||
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.appendLineOnMain
|
||||
import com.topjohnwu.magisk.terminal.runSuCommand
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
||||
class FlashViewModel : BaseViewModel() {
|
||||
|
||||
enum class State {
|
||||
FLASHING, SUCCESS, FAILED
|
||||
}
|
||||
|
||||
private val _flashState = MutableStateFlow(State.FLASHING)
|
||||
val flashState: StateFlow<State> = _flashState.asStateFlow()
|
||||
|
||||
private val _showReboot = MutableStateFlow(Info.isRooted)
|
||||
val showReboot: StateFlow<Boolean> = _showReboot.asStateFlow()
|
||||
|
||||
var flashAction: String = ""
|
||||
var flashUri: Uri? = null
|
||||
|
||||
// --- TerminalScreen mode (FLASH_ZIP) ---
|
||||
|
||||
private var emulator: TerminalEmulator? = null
|
||||
private val emulatorReady = CompletableDeferred<TerminalEmulator>()
|
||||
|
||||
fun onEmulatorCreated(emu: TerminalEmulator) {
|
||||
emulator = emu
|
||||
emulatorReady.complete(emu)
|
||||
}
|
||||
|
||||
// --- LazyColumn mode (MagiskInstaller) ---
|
||||
|
||||
val consoleItems = mutableStateListOf<String>()
|
||||
private val logItems = mutableListOf<String>().synchronized()
|
||||
private val outItems = object : CallbackList<String>() {
|
||||
override fun onAddElement(e: String?) {
|
||||
e ?: return
|
||||
consoleItems.add(e)
|
||||
logItems.add(e)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Shared ---
|
||||
|
||||
fun startFlashing() {
|
||||
val action = flashAction
|
||||
val uri = flashUri
|
||||
|
||||
viewModelScope.launch {
|
||||
when (action) {
|
||||
Const.Value.FLASH_ZIP -> {
|
||||
uri ?: return@launch
|
||||
flashZip(uri)
|
||||
}
|
||||
Const.Value.UNINSTALL -> {
|
||||
_showReboot.value = false
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
MagiskInstaller.Uninstall(outItems, logItems).exec()
|
||||
})
|
||||
}
|
||||
Const.Value.FLASH_MAGISK -> {
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
if (Info.isEmulator)
|
||||
MagiskInstaller.Emulator(outItems, logItems).exec()
|
||||
else
|
||||
MagiskInstaller.Direct(outItems, logItems).exec()
|
||||
})
|
||||
}
|
||||
Const.Value.FLASH_INACTIVE_SLOT -> {
|
||||
_showReboot.value = false
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
MagiskInstaller.SecondSlot(outItems, logItems).exec()
|
||||
})
|
||||
}
|
||||
Const.Value.PATCH_FILE -> {
|
||||
uri ?: return@launch
|
||||
_showReboot.value = false
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
MagiskInstaller.Patch(uri, outItems, logItems).exec()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onResult(success: Boolean) {
|
||||
_flashState.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
|
||||
private suspend fun flashZip(uri: Uri) {
|
||||
val emu = emulatorReady.await()
|
||||
|
||||
val installDir = File(AppContext.cacheDir, "flash")
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
val zipFile = if (uri.scheme == "file") {
|
||||
uri.toFile()
|
||||
} else {
|
||||
File(installDir, "install.zip").also {
|
||||
try {
|
||||
uri.inputStream().writeTo(it)
|
||||
} catch (e: IOException) {
|
||||
val msg = if (e is FileNotFoundException) "Invalid Uri" else "Cannot copy to cache"
|
||||
return@withContext msg to null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val binary = File(installDir, "update-binary")
|
||||
AppContext.assets.open("module_installer.sh").use { it.writeTo(binary) }
|
||||
|
||||
val name = uri.displayName
|
||||
null to Triple(installDir, zipFile, name)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
"Unable to extract files" to null
|
||||
}
|
||||
}
|
||||
|
||||
val (error, prepResult) = result
|
||||
if (prepResult == null) {
|
||||
emu.appendLineOnMain("! ${error ?: "Installation failed"}")
|
||||
_flashState.value = State.FAILED
|
||||
return
|
||||
}
|
||||
|
||||
val (dir, zipFile, displayName) = prepResult
|
||||
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
runSuCommand(
|
||||
emu,
|
||||
"echo '- Installing $displayName'; " +
|
||||
"sh $dir/update-binary dummy 1 '${zipFile.absolutePath}'; " +
|
||||
"EXIT=\$?; " +
|
||||
"if [ \$EXIT -ne 0 ]; then echo '! Installation failed'; fi; " +
|
||||
"exit \$EXIT"
|
||||
)
|
||||
}
|
||||
|
||||
Shell.cmd("cd /", "rm -rf $dir ${Const.TMPDIR}").submit()
|
||||
_flashState.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
|
||||
fun saveLog() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val name = "magisk_install_log_%s.log".format(
|
||||
System.currentTimeMillis().toTime(timeFormatStandard)
|
||||
)
|
||||
val file = MediaStoreUtils.getFile(name)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
val transcript = emulator?.screen?.transcriptText
|
||||
if (transcript != null) {
|
||||
writer.write(transcript)
|
||||
} else {
|
||||
synchronized(logItems) {
|
||||
logItems.forEach {
|
||||
writer.write(it)
|
||||
writer.newLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showSnackbar(file.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun restartPressed() = reboot()
|
||||
}
|
||||
1117
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt
Normal file
1117
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
package com.topjohnwu.magisk.ui.home
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.download.Subject.App
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlin.math.roundToInt
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class HomeViewModel(
|
||||
private val svc: NetworkService
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
enum class State {
|
||||
LOADING, INVALID, OUTDATED, UP_TO_DATE
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val isNoticeVisible: Boolean = Config.safetyNotice,
|
||||
val appState: State = State.LOADING,
|
||||
val managerRemoteVersion: String = "",
|
||||
val managerProgress: Int = 0,
|
||||
val showUninstall: Boolean = false,
|
||||
val showManagerInstall: Boolean = false,
|
||||
val showHideRestore: Boolean = false,
|
||||
val envFixCode: Int = 0,
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
val magiskState
|
||||
get() = when {
|
||||
Info.isRooted && Info.env.isUnsupported -> State.OUTDATED
|
||||
!Info.env.isActive -> State.INVALID
|
||||
Info.env.versionCode < BuildConfig.APP_VERSION_CODE -> State.OUTDATED
|
||||
else -> State.UP_TO_DATE
|
||||
}
|
||||
|
||||
val magiskInstalledVersion: String
|
||||
get() = Info.env.run {
|
||||
if (isActive)
|
||||
"$versionString ($versionCode)" + if (isDebug) " (D)" else ""
|
||||
else
|
||||
""
|
||||
}
|
||||
|
||||
val managerInstalledVersion: String
|
||||
get() = "${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})" +
|
||||
if (BuildConfig.DEBUG) " (D)" else ""
|
||||
|
||||
companion object {
|
||||
private var checkedEnv = false
|
||||
}
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
_uiState.update { it.copy(appState = State.LOADING) }
|
||||
Info.fetchUpdate(svc)?.apply {
|
||||
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
appState = if (BuildConfig.APP_VERSION_CODE < versionCode) State.OUTDATED else State.UP_TO_DATE,
|
||||
managerRemoteVersion = "$version ($versionCode)" + if (isDebug) " (D)" else ""
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
_uiState.update { it.copy(appState = State.INVALID, managerRemoteVersion = "") }
|
||||
}
|
||||
ensureEnv()
|
||||
}
|
||||
|
||||
private val networkObserver: (Boolean) -> Unit = { startLoading() }
|
||||
|
||||
init {
|
||||
Info.isConnected.observeForever(networkObserver)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Info.isConnected.removeObserver(networkObserver)
|
||||
}
|
||||
|
||||
fun onProgressUpdate(progress: Float, subject: Subject) {
|
||||
if (subject is App)
|
||||
_uiState.update { it.copy(managerProgress = progress.times(100f).roundToInt()) }
|
||||
}
|
||||
|
||||
fun resetProgress() {
|
||||
_uiState.update { it.copy(managerProgress = 0) }
|
||||
}
|
||||
|
||||
fun onLinkPressed(link: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
AppContext.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
AppContext.toast(CoreR.string.open_link_failed_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeletePressed() {
|
||||
_uiState.update { it.copy(showUninstall = true) }
|
||||
}
|
||||
|
||||
fun onUninstallConsumed() {
|
||||
_uiState.update { it.copy(showUninstall = false) }
|
||||
}
|
||||
|
||||
fun onManagerPressed() {
|
||||
when (_uiState.value.appState) {
|
||||
State.LOADING -> showSnackbar(CoreR.string.loading)
|
||||
State.INVALID -> showSnackbar(CoreR.string.no_connection)
|
||||
else -> _uiState.update { it.copy(showManagerInstall = true) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onManagerInstallConsumed() {
|
||||
_uiState.update { it.copy(showManagerInstall = false) }
|
||||
}
|
||||
|
||||
fun onHideRestorePressed() {
|
||||
_uiState.update { it.copy(showHideRestore = true) }
|
||||
}
|
||||
|
||||
fun onHideRestoreConsumed() {
|
||||
_uiState.update { it.copy(showHideRestore = false) }
|
||||
}
|
||||
|
||||
fun onEnvFixConsumed() {
|
||||
_uiState.update { it.copy(envFixCode = 0) }
|
||||
}
|
||||
|
||||
fun hideNotice() {
|
||||
Config.safetyNotice = false
|
||||
_uiState.update { it.copy(isNoticeVisible = false) }
|
||||
}
|
||||
|
||||
private suspend fun ensureEnv() {
|
||||
if (magiskState == State.INVALID || checkedEnv) return
|
||||
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
|
||||
val code = Shell.cmd(cmd).await().code
|
||||
if (code != 0) {
|
||||
_uiState.update { it.copy(envFixCode = code) }
|
||||
}
|
||||
checkedEnv = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.topjohnwu.magisk.ui.install
|
||||
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class InstallViewModel(svc: NetworkService) : BaseViewModel() {
|
||||
|
||||
enum class Method { NONE, PATCH, DIRECT, INACTIVE_SLOT }
|
||||
|
||||
data class UiState(
|
||||
val step: Int = 0,
|
||||
val method: Method = Method.NONE,
|
||||
val notes: String = "",
|
||||
val patchUri: Uri? = null,
|
||||
val requestFilePicker: Boolean = false,
|
||||
val showSecondSlotWarning: Boolean = false,
|
||||
)
|
||||
|
||||
val isRooted get() = Info.isRooted
|
||||
val skipOptions = Info.isEmulator || (Info.isSAR && !Info.isFDE && Info.ramdisk)
|
||||
val noSecondSlot = !isRooted || !Info.isAB || Info.isEmulator
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState(step = if (skipOptions) 1 else 0))
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val noteFile = File(AppContext.cacheDir, "${APP_VERSION_CODE}.md")
|
||||
val noteText = when {
|
||||
noteFile.exists() -> noteFile.readText()
|
||||
else -> {
|
||||
val note = svc.fetchUpdate(APP_VERSION_CODE)?.note.orEmpty()
|
||||
if (note.isEmpty()) return@launch
|
||||
noteFile.writeText(note)
|
||||
note
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_uiState.update { it.copy(notes = noteText) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun nextStep() {
|
||||
_uiState.update { it.copy(step = 1) }
|
||||
}
|
||||
|
||||
fun selectMethod(method: Method) {
|
||||
_uiState.update { it.copy(method = method) }
|
||||
when (method) {
|
||||
Method.PATCH -> {
|
||||
AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG)
|
||||
_uiState.update { it.copy(requestFilePicker = true) }
|
||||
}
|
||||
Method.INACTIVE_SLOT -> {
|
||||
_uiState.update { it.copy(showSecondSlotWarning = true) }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun onFilePickerConsumed() {
|
||||
_uiState.update { it.copy(requestFilePicker = false) }
|
||||
}
|
||||
|
||||
fun onSecondSlotWarningConsumed() {
|
||||
_uiState.update { it.copy(showSecondSlotWarning = false) }
|
||||
}
|
||||
|
||||
fun onPatchFileSelected(uri: Uri) {
|
||||
_uiState.update { it.copy(patchUri = uri) }
|
||||
if (_uiState.value.method == Method.PATCH) {
|
||||
install()
|
||||
}
|
||||
}
|
||||
|
||||
fun install() {
|
||||
when (_uiState.value.method) {
|
||||
Method.PATCH -> navigateTo(Route.Flash(
|
||||
action = Const.Value.PATCH_FILE,
|
||||
additionalData = _uiState.value.patchUri!!.toString()
|
||||
))
|
||||
Method.DIRECT -> navigateTo(Route.Flash(
|
||||
action = Const.Value.FLASH_MAGISK
|
||||
))
|
||||
Method.INACTIVE_SLOT -> navigateTo(Route.Flash(
|
||||
action = Const.Value.FLASH_INACTIVE_SLOT
|
||||
))
|
||||
else -> error("Unknown method")
|
||||
}
|
||||
}
|
||||
|
||||
val canInstall: Boolean
|
||||
get() {
|
||||
val state = _uiState.value
|
||||
return when (state.method) {
|
||||
Method.PATCH -> state.patchUri != null
|
||||
Method.DIRECT, Method.INACTIVE_SLOT -> true
|
||||
Method.NONE -> false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.core.ktx.timeDateFormat
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LogScreen(viewModel: LogViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var selectedTab by rememberSaveable { mutableIntStateOf(0) }
|
||||
val tabTitles = listOf(
|
||||
stringResource(CoreR.string.superuser),
|
||||
stringResource(CoreR.string.magisk)
|
||||
)
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(CoreR.string.logs)) },
|
||||
actions = {
|
||||
if (selectedTab == 1) {
|
||||
IconButton(onClick = { viewModel.saveMagiskLog() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = stringResource(CoreR.string.save_log),
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = {
|
||||
if (selectedTab == 0) viewModel.clearLog()
|
||||
else viewModel.clearMagiskLog()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(CoreR.string.clear_log),
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
TabRow(
|
||||
selectedTabIndex = selectedTab,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
tabTitles.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
text = { Text(title) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.loading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
when (selectedTab) {
|
||||
0 -> SuLogTab(
|
||||
logs = uiState.suLogs,
|
||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection
|
||||
)
|
||||
1 -> MagiskLogTab(
|
||||
entries = uiState.magiskLogEntries,
|
||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SuLogTab(logs: List<SuLog>, nestedScrollConnection: NestedScrollConnection) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
if (logs.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.log_data_none),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(logs, key = { it.id }) { log ->
|
||||
SuLogCard(log)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SuLogCard(log: SuLog) {
|
||||
val res = LocalContext.current.resources
|
||||
val pm = LocalContext.current.packageManager
|
||||
val icon = remember(log.packageName) {
|
||||
runCatching {
|
||||
pm.getApplicationInfo(log.packageName, 0).loadIcon(pm)
|
||||
}.getOrDefault(pm.defaultActivityIcon)
|
||||
}
|
||||
val allowed = log.action >= 2
|
||||
|
||||
val uidPidText = buildString {
|
||||
append("UID: ${log.toUid} PID: ${log.fromPid}")
|
||||
if (log.target != -1) {
|
||||
val target = if (log.target == 0) "magiskd" else log.target.toString()
|
||||
append(" → $target")
|
||||
}
|
||||
}
|
||||
|
||||
val details = buildString {
|
||||
if (log.context.isNotEmpty()) {
|
||||
append(res.getString(CoreR.string.selinux_context, log.context))
|
||||
}
|
||||
if (log.gids.isNotEmpty()) {
|
||||
if (isNotEmpty()) append("\n")
|
||||
append(res.getString(CoreR.string.supp_group, log.gids))
|
||||
}
|
||||
if (log.command.isNotEmpty()) {
|
||||
if (isNotEmpty()) append("\n")
|
||||
append(log.command)
|
||||
}
|
||||
}
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(icon),
|
||||
contentDescription = log.appName,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = log.appName,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = uidPidText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = log.time.toTime(timeDateFormat),
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
SuActionBadge(allowed)
|
||||
}
|
||||
}
|
||||
|
||||
if (details.isNotEmpty()) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
text = details,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SuActionBadge(allowed: Boolean) {
|
||||
val bg = if (allowed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||
val fg = if (allowed) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onError
|
||||
val text = if (allowed) "Approved" else "Rejected"
|
||||
Text(
|
||||
text = text,
|
||||
color = fg,
|
||||
fontSize = 10.sp,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.background(bg, RoundedCornerShape(6.dp))
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MagiskLogTab(
|
||||
entries: List<MagiskLogEntry>,
|
||||
nestedScrollConnection: NestedScrollConnection
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
if (entries.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.log_data_magisk_none),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val listState = rememberLazyListState(initialFirstVisibleItemIndex = entries.size - 1)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(entries.size, key = { it }) { index ->
|
||||
MagiskLogCard(entries[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MagiskLogCard(entry: MagiskLogEntry) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expanded = !expanded }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
if (entry.isParsed) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
LogLevelBadge(entry.level)
|
||||
Text(
|
||||
text = entry.tag,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Normal,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = entry.timestamp,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = entry.message,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = if (expanded) Int.MAX_VALUE else 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LogLevelBadge(level: Char) {
|
||||
val (bg, fg) = when (level) {
|
||||
'V' -> Color(0xFF9E9E9E) to Color.White
|
||||
'D' -> Color(0xFF2196F3) to Color.White
|
||||
'I' -> Color(0xFF4CAF50) to Color.White
|
||||
'W' -> Color(0xFFFFC107) to Color.Black
|
||||
'E' -> Color(0xFFF44336) to Color.White
|
||||
'F' -> Color(0xFF9C27B0) to Color.White
|
||||
else -> Color(0xFF757575) to Color.White
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(bg)
|
||||
.padding(horizontal = 5.dp, vertical = 1.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = level.toString(),
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = fg,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
import android.system.Os
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||
import com.topjohnwu.magisk.core.su.SuEvents
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
|
||||
class LogViewModel(
|
||||
private val repo: LogRepository
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
init {
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
viewModelScope.launch {
|
||||
SuEvents.logUpdated.debounce(500).collect { reload() }
|
||||
}
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = true,
|
||||
val magiskLog: String = "",
|
||||
val magiskLogEntries: List<MagiskLogEntry> = emptyList(),
|
||||
val suLogs: List<SuLog> = emptyList(),
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
private var magiskLogRaw = ""
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
_uiState.update { it.copy(loading = true) }
|
||||
withContext(Dispatchers.Default) {
|
||||
magiskLogRaw = repo.fetchMagiskLogs()
|
||||
val suLogs = repo.fetchSuLogs()
|
||||
val entries = MagiskLogParser.parse(magiskLogRaw)
|
||||
_uiState.update { it.copy(
|
||||
loading = false,
|
||||
magiskLog = magiskLogRaw,
|
||||
magiskLogEntries = entries,
|
||||
suLogs = suLogs,
|
||||
) }
|
||||
}
|
||||
}
|
||||
|
||||
fun saveMagiskLog() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val filename = "magisk_log_%s.log".format(
|
||||
System.currentTimeMillis().toTime(timeFormatStandard))
|
||||
val logFile = MediaStoreUtils.getFile(filename)
|
||||
logFile.uri.outputStream().bufferedWriter().use { file ->
|
||||
file.write("---Detected Device Info---\n\n")
|
||||
file.write("isAB=${Info.isAB}\n")
|
||||
file.write("isSAR=${Info.isSAR}\n")
|
||||
file.write("ramdisk=${Info.ramdisk}\n")
|
||||
val uname = Os.uname()
|
||||
file.write("kernel=${uname.sysname} ${uname.machine} ${uname.release} ${uname.version}\n")
|
||||
|
||||
file.write("\n\n---System Properties---\n\n")
|
||||
ProcessBuilder("getprop").start()
|
||||
.inputStream.reader().use { it.copyTo(file) }
|
||||
|
||||
file.write("\n\n---Environment Variables---\n\n")
|
||||
System.getenv().forEach { (key, value) -> file.write("${key}=${value}\n") }
|
||||
|
||||
file.write("\n\n---System MountInfo---\n\n")
|
||||
FileInputStream("/proc/self/mountinfo").reader().use { it.copyTo(file) }
|
||||
|
||||
file.write("\n---Magisk Logs---\n")
|
||||
file.write("${Info.env.versionString} (${Info.env.versionCode})\n\n")
|
||||
if (Info.env.isActive) file.write(magiskLogRaw)
|
||||
|
||||
file.write("\n---Manager Logs---\n")
|
||||
file.write("${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})\n\n")
|
||||
ProcessBuilder("logcat", "-d").start()
|
||||
.inputStream.reader().use { it.copyTo(file) }
|
||||
}
|
||||
showSnackbar(logFile.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMagiskLog() = repo.clearMagiskLogs {
|
||||
showSnackbar(R.string.logs_cleared)
|
||||
startLoading()
|
||||
}
|
||||
|
||||
fun clearLog() = viewModelScope.launch {
|
||||
repo.clearLogs()
|
||||
showSnackbar(R.string.logs_cleared)
|
||||
startLoading()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
data class MagiskLogEntry(
|
||||
val timestamp: String = "",
|
||||
val pid: Int = 0,
|
||||
val tid: Int = 0,
|
||||
val level: Char = 'I',
|
||||
val tag: String = "",
|
||||
val message: String = "",
|
||||
val isParsed: Boolean = false,
|
||||
)
|
||||
|
||||
object MagiskLogParser {
|
||||
|
||||
// Logcat format: "MM-DD HH:MM:SS.mmm PID TID LEVEL TAG : message"
|
||||
private val logcatRegex = Regex(
|
||||
"""(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([VDIWEF])\s+(.+?)\s*:\s+(.*)"""
|
||||
)
|
||||
|
||||
fun parse(raw: String): List<MagiskLogEntry> {
|
||||
if (raw.isBlank()) return emptyList()
|
||||
|
||||
val lines = raw.lines()
|
||||
val result = mutableListOf<MagiskLogEntry>()
|
||||
|
||||
for (line in lines) {
|
||||
if (line.isBlank()) continue
|
||||
|
||||
val match = logcatRegex.find(line)
|
||||
if (match != null) {
|
||||
result.add(
|
||||
MagiskLogEntry(
|
||||
timestamp = match.groupValues[1],
|
||||
pid = match.groupValues[2].toIntOrNull() ?: 0,
|
||||
tid = match.groupValues[3].toIntOrNull() ?: 0,
|
||||
level = match.groupValues[4].firstOrNull() ?: 'I',
|
||||
tag = match.groupValues[5].trim(),
|
||||
message = match.groupValues[6],
|
||||
isParsed = true,
|
||||
)
|
||||
)
|
||||
} else if (result.isNotEmpty() && result.last().isParsed) {
|
||||
// Continuation line — append to previous entry
|
||||
val prev = result.last()
|
||||
result[result.lastIndex] = prev.copy(
|
||||
message = prev.message + "\n" + line.trimEnd()
|
||||
)
|
||||
} else {
|
||||
result.add(
|
||||
MagiskLogEntry(message = line.trimEnd())
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalScreen
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> Unit) {
|
||||
val actionState by viewModel.actionState.collectAsState()
|
||||
val finished = actionState != ActionViewModel.State.RUNNING
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { androidx.compose.material3.Text(actionName) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (finished) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { viewModel.saveLog() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_save_md2),
|
||||
contentDescription = stringResource(CoreR.string.menuSaveLog),
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
TerminalScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
onEmulatorCreated = { viewModel.onEmulatorCreated(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.runSuCommand
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ActionViewModel : BaseViewModel() {
|
||||
|
||||
enum class State {
|
||||
RUNNING, SUCCESS, FAILED
|
||||
}
|
||||
|
||||
private val _actionState = MutableStateFlow(State.RUNNING)
|
||||
val actionState: StateFlow<State> = _actionState.asStateFlow()
|
||||
|
||||
var actionId: String = ""
|
||||
var actionName: String = ""
|
||||
|
||||
private var emulator: TerminalEmulator? = null
|
||||
private val emulatorReady = CompletableDeferred<TerminalEmulator>()
|
||||
|
||||
fun onEmulatorCreated(emu: TerminalEmulator) {
|
||||
emulator = emu
|
||||
emulatorReady.complete(emu)
|
||||
}
|
||||
|
||||
fun startRunAction() {
|
||||
viewModelScope.launch {
|
||||
val emu = emulatorReady.await()
|
||||
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
runSuCommand(
|
||||
emu,
|
||||
"cd /data/adb/modules/$actionId && sh ./action.sh"
|
||||
)
|
||||
}
|
||||
|
||||
_actionState.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLog() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val name = "%s_action_log_%s.log".format(
|
||||
actionName,
|
||||
System.currentTimeMillis().toTime(timeFormatStandard)
|
||||
)
|
||||
val file = MediaStoreUtils.getFile(name)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
val transcript = emulator?.screen?.transcriptText
|
||||
if (transcript != null) {
|
||||
writer.write(transcript)
|
||||
}
|
||||
}
|
||||
showSnackbar(file.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.component.ConfirmResult
|
||||
import com.topjohnwu.magisk.ui.component.MarkdownTextAsync
|
||||
import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.CloudUpload
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Undo
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ModuleScreen(viewModel: ModuleViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val activity = context as MainActivity
|
||||
|
||||
var pendingZipUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var pendingZipName by remember { mutableStateOf("") }
|
||||
val localInstallDialog = rememberConfirmDialog()
|
||||
val confirmInstallTitle = stringResource(CoreR.string.confirm_install_title)
|
||||
|
||||
var pendingOnlineModule by remember { mutableStateOf<OnlineModule?>(null) }
|
||||
val showOnlineDialog = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri != null) {
|
||||
val displayName = context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (cursor.moveToFirst() && idx >= 0) cursor.getString(idx) else null
|
||||
} ?: uri.lastPathSegment ?: "module.zip"
|
||||
pendingZipUri = uri
|
||||
pendingZipName = displayName
|
||||
scope.launch {
|
||||
val result = localInstallDialog.awaitConfirm(
|
||||
title = confirmInstallTitle,
|
||||
content = context.getString(CoreR.string.confirm_install, displayName),
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
viewModel.confirmLocalInstall(uri)
|
||||
}
|
||||
pendingZipUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showOnlineDialog.value && pendingOnlineModule != null) {
|
||||
OnlineModuleDialog(
|
||||
item = pendingOnlineModule!!,
|
||||
showDialog = showOnlineDialog,
|
||||
onDownload = { install ->
|
||||
showOnlineDialog.value = false
|
||||
DownloadEngine.startWithActivity(
|
||||
activity,
|
||||
OnlineModuleSubject(pendingOnlineModule!!, install)
|
||||
)
|
||||
pendingOnlineModule = null
|
||||
},
|
||||
onDismiss = {
|
||||
showOnlineDialog.value = false
|
||||
pendingOnlineModule = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(CoreR.string.modules)) },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { filePicker.launch("application/zip") },
|
||||
shape = CircleShape,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 88.dp, end = 20.dp)
|
||||
.border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape),
|
||||
content = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(CoreR.string.module_action_install_external),
|
||||
modifier = Modifier.size(28.dp),
|
||||
tint = colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
if (uiState.loading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
if (uiState.modules.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.module_empty),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 160.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
items(uiState.modules, key = { it.module.id }) { item ->
|
||||
ModuleCard(
|
||||
item = item,
|
||||
viewModel = viewModel,
|
||||
onUpdateClick = { onlineModule ->
|
||||
if (onlineModule != null && Info.isConnected.value == true) {
|
||||
pendingOnlineModule = onlineModule
|
||||
showOnlineDialog.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModuleCard(item: ModuleItem, viewModel: ModuleViewModel, onUpdateClick: (OnlineModule?) -> Unit) {
|
||||
val infoAlpha = if (!item.isRemoved && item.isEnabled && !item.showNotice) 1f else 0.5f
|
||||
val strikeThrough = if (item.isRemoved) TextDecoration.LineThrough else TextDecoration.None
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val actionIconTint = colorScheme.onSurface.copy(alpha = 0.8f)
|
||||
val actionBg = colorScheme.secondaryContainer.copy(alpha = 0.8f)
|
||||
val updateBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f)
|
||||
val updateTint = colorScheme.onTertiaryContainer.copy(alpha = 0.8f)
|
||||
val removeBg = colorScheme.errorContainer.copy(alpha = 0.6f)
|
||||
val removeTint = colorScheme.onErrorContainer.copy(alpha = 0.8f)
|
||||
var expanded by rememberSaveable(item.module.id) { mutableStateOf(false) }
|
||||
val hasDescription = item.module.description.isNotBlank()
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().clickable(enabled = hasDescription) { expanded = !expanded },
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Column(modifier = Modifier.alpha(infoAlpha)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.module.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textDecoration = strikeThrough,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
CoreR.string.module_version_author,
|
||||
item.module.version,
|
||||
item.module.author
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = colorScheme.onSurfaceVariant,
|
||||
textDecoration = strikeThrough,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = item.isEnabled,
|
||||
onCheckedChange = { viewModel.toggleEnabled(item) }
|
||||
)
|
||||
}
|
||||
|
||||
if (hasDescription) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp)
|
||||
.animateContentSize()
|
||||
) {
|
||||
Text(
|
||||
text = item.module.description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = colorScheme.onSurfaceVariant,
|
||||
textDecoration = strikeThrough,
|
||||
overflow = if (expanded) TextOverflow.Clip else TextOverflow.Ellipsis,
|
||||
maxLines = if (expanded) Int.MAX_VALUE else 4,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.showNotice) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = item.noticeText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
thickness = 0.5.dp,
|
||||
color = colorScheme.outline.copy(alpha = 0.5f)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
AnimatedVisibility(
|
||||
visible = item.isEnabled && !item.isRemoved,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (item.showAction) {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = actionBg),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp),
|
||||
onClick = { viewModel.runAction(item.module.id, item.module.name) },
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
tint = actionIconTint,
|
||||
contentDescription = stringResource(CoreR.string.module_action)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(CoreR.string.module_action),
|
||||
color = actionIconTint,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = item.showUpdate && item.updateReady,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = updateBg),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp),
|
||||
onClick = { onUpdateClick(item.module.updateInfo) },
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Default.CloudUpload,
|
||||
tint = updateTint,
|
||||
contentDescription = stringResource(CoreR.string.update),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(CoreR.string.update),
|
||||
color = updateTint,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(containerColor = if (item.isRemoved) actionBg else removeBg),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp),
|
||||
onClick = { viewModel.toggleRemove(item) },
|
||||
enabled = !item.isUpdated
|
||||
) {
|
||||
val tint = if (item.isRemoved) actionIconTint else removeTint
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = if (item.isRemoved) Icons.Default.Undo else Icons.Default.Delete,
|
||||
tint = tint,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (item.isRemoved) CoreR.string.module_state_restore
|
||||
else CoreR.string.module_state_remove
|
||||
),
|
||||
color = tint,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnlineModuleDialog(
|
||||
item: OnlineModule,
|
||||
showDialog: MutableState<Boolean>,
|
||||
onDownload: (install: Boolean) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val svc = ServiceLocator.networkService
|
||||
val title = stringResource(
|
||||
CoreR.string.repo_install_title,
|
||||
item.name, item.version, item.versionCode
|
||||
)
|
||||
|
||||
if (showDialog.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
text = {
|
||||
MarkdownTextAsync {
|
||||
val str = svc.fetchString(item.changelog)
|
||||
if (str.length > 1000) str.substring(0, 1000) else str
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { onDownload(true) }
|
||||
) {
|
||||
Text(stringResource(CoreR.string.install))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Row {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(onClick = { onDownload(false) }) {
|
||||
Text(stringResource(CoreR.string.download))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.ui.flash.FlashUtils
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class ModuleItem(val module: LocalModule) {
|
||||
val showNotice: Boolean
|
||||
val showAction: Boolean
|
||||
val noticeText: String
|
||||
|
||||
init {
|
||||
val isZygisk = module.isZygisk
|
||||
val isRiru = module.isRiru
|
||||
val zygiskUnloaded = isZygisk && module.zygiskUnloaded
|
||||
|
||||
showNotice = zygiskUnloaded ||
|
||||
(Info.isZygiskEnabled && isRiru) ||
|
||||
(!Info.isZygiskEnabled && isZygisk)
|
||||
showAction = module.hasAction && !showNotice
|
||||
noticeText = when {
|
||||
zygiskUnloaded -> "Zygisk module not loaded due to incompatibility"
|
||||
isRiru -> "Module suspended because Zygisk is enabled"
|
||||
else -> "Module suspended because Zygisk isn't enabled"
|
||||
}
|
||||
}
|
||||
|
||||
var isEnabled by mutableStateOf(module.enable)
|
||||
var isRemoved by mutableStateOf(module.remove)
|
||||
var showUpdate by mutableStateOf(module.updateInfo != null)
|
||||
val isUpdated = module.updated
|
||||
val updateReady get() = module.outdated && !isRemoved && isEnabled
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class OnlineModuleSubject(
|
||||
override val module: OnlineModule,
|
||||
override val autoLaunch: Boolean,
|
||||
override val notifyId: Int = Notifications.nextId()
|
||||
) : Subject.Module() {
|
||||
override fun pendingIntent(context: Context) = FlashUtils.installIntent(context, file)
|
||||
}
|
||||
|
||||
class ModuleViewModel : AsyncLoadViewModel() {
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = true,
|
||||
val modules: List<ModuleItem> = emptyList(),
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
_uiState.update { it.copy(loading = true) }
|
||||
val moduleLoaded = Info.env.isActive &&
|
||||
withContext(Dispatchers.IO) { LocalModule.loaded() }
|
||||
if (moduleLoaded) {
|
||||
val modules = withContext(Dispatchers.Default) {
|
||||
LocalModule.installed().map { ModuleItem(it) }
|
||||
}
|
||||
_uiState.update { it.copy(loading = false, modules = modules) }
|
||||
loadUpdateInfo()
|
||||
} else {
|
||||
_uiState.update { it.copy(loading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private val networkObserver: (Boolean) -> Unit = { startLoading() }
|
||||
|
||||
init {
|
||||
Info.isConnected.observeForever(networkObserver)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Info.isConnected.removeObserver(networkObserver)
|
||||
}
|
||||
|
||||
private suspend fun loadUpdateInfo() {
|
||||
withContext(Dispatchers.IO) {
|
||||
_uiState.value.modules.forEach { item ->
|
||||
if (item.module.fetch()) {
|
||||
item.showUpdate = item.module.updateInfo != null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmLocalInstall(uri: Uri) {
|
||||
navigateTo(Route.Flash(Const.Value.FLASH_ZIP, uri.toString()))
|
||||
}
|
||||
|
||||
fun runAction(id: String, name: String) {
|
||||
navigateTo(Route.Action(id, name))
|
||||
}
|
||||
|
||||
fun toggleEnabled(item: ModuleItem) {
|
||||
item.isEnabled = !item.isEnabled
|
||||
item.module.enable = item.isEnabled
|
||||
}
|
||||
|
||||
fun toggleRemove(item: ModuleItem) {
|
||||
item.isRemoved = !item.isRemoved
|
||||
item.module.remove = item.isRemoved
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
|
||||
@Composable
|
||||
fun CollectNavEvents(viewModel: BaseViewModel, navigator: Navigator) {
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.navEvents.collect { route ->
|
||||
navigator.push(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.topjohnwu.magisk.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
|
||||
class Navigator(initialKey: NavKey) {
|
||||
val backStack: SnapshotStateList<NavKey> = mutableStateListOf(initialKey)
|
||||
|
||||
fun push(key: NavKey) {
|
||||
backStack.add(key)
|
||||
}
|
||||
|
||||
fun replace(key: NavKey) {
|
||||
if (backStack.isNotEmpty()) {
|
||||
backStack[backStack.lastIndex] = key
|
||||
} else {
|
||||
backStack.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAll(keys: List<NavKey>) {
|
||||
if (keys.isEmpty()) return
|
||||
if (backStack.isNotEmpty()) {
|
||||
backStack.clear()
|
||||
backStack.addAll(keys)
|
||||
}
|
||||
}
|
||||
|
||||
fun pop() {
|
||||
backStack.removeLastOrNull()
|
||||
}
|
||||
|
||||
fun popUntil(predicate: (NavKey) -> Boolean) {
|
||||
while (backStack.isNotEmpty() && !predicate(backStack.last())) {
|
||||
backStack.removeAt(backStack.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun current(): NavKey? = backStack.lastOrNull()
|
||||
|
||||
fun backStackSize(): Int = backStack.size
|
||||
|
||||
companion object {
|
||||
val Saver: Saver<Navigator, Any> = listSaver(
|
||||
save = { navigator -> navigator.backStack.toList() },
|
||||
restore = { savedList ->
|
||||
val initialKey = savedList.firstOrNull() ?: Route.Main
|
||||
Navigator(initialKey).also {
|
||||
it.backStack.clear()
|
||||
it.backStack.addAll(savedList)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberNavigator(startRoute: NavKey): Navigator {
|
||||
return rememberSaveable(startRoute, saver = Navigator.Saver) {
|
||||
Navigator(startRoute)
|
||||
}
|
||||
}
|
||||
|
||||
val LocalNavigator = staticCompositionLocalOf<Navigator> {
|
||||
error("LocalNavigator not provided")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.topjohnwu.magisk.ui.navigation
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed interface Route : NavKey, Parcelable {
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data object Main : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data object DenyList : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Flash(
|
||||
val action: String,
|
||||
val additionalData: String? = null,
|
||||
) : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class SuperuserDetail(val uid: Int) : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Action(
|
||||
val id: String,
|
||||
val name: String,
|
||||
) : Route
|
||||
}
|
||||
@@ -0,0 +1,611 @@
|
||||
package com.topjohnwu.magisk.ui.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.utils.LocaleSetting
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.ui.theme.ThemeState
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import com.topjohnwu.magisk.ui.component.SettingsArrow
|
||||
import com.topjohnwu.magisk.ui.component.SettingsDropdown
|
||||
import com.topjohnwu.magisk.ui.component.SettingsSwitch
|
||||
import com.topjohnwu.magisk.ui.component.SmallTitle
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(viewModel: SettingsViewModel) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(CoreR.string.settings)) },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(bottom = 88.dp)
|
||||
) {
|
||||
CustomizationSection(viewModel)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
AppSettingsSection(viewModel)
|
||||
if (Info.env.isActive) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
MagiskSection(viewModel)
|
||||
}
|
||||
if (Info.showSuperUser) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SuperuserSection(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Customization ---
|
||||
|
||||
@Composable
|
||||
private fun CustomizationSection(viewModel: SettingsViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
SmallTitle(text = stringResource(CoreR.string.settings_customization))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
if (LocaleSetting.useLocaleManager) {
|
||||
val locale = LocaleSetting.instance.appLocale
|
||||
val summary = locale?.getDisplayName(locale) ?: stringResource(CoreR.string.system_default)
|
||||
SettingsArrow(
|
||||
title = stringResource(CoreR.string.language),
|
||||
summary = summary,
|
||||
onClick = {
|
||||
context.startActivity(LocaleSetting.localeSettingsIntent)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
val names = remember { LocaleSetting.available.names }
|
||||
val tags = remember { LocaleSetting.available.tags }
|
||||
var selectedIndex by remember {
|
||||
mutableIntStateOf(tags.indexOf(Config.locale).coerceAtLeast(0))
|
||||
}
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.language),
|
||||
items = names.toList(),
|
||||
selectedIndex = selectedIndex,
|
||||
onSelectedIndexChange = { index ->
|
||||
selectedIndex = index
|
||||
Config.locale = tags[index]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Color Mode
|
||||
val resources = context.resources
|
||||
val colorModeEntries = remember {
|
||||
resources.getStringArray(CoreR.array.color_mode).toList()
|
||||
}
|
||||
var colorMode by remember { mutableIntStateOf(Config.colorMode) }
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.settings_color_mode),
|
||||
items = colorModeEntries,
|
||||
selectedIndex = colorMode,
|
||||
onSelectedIndexChange = { index ->
|
||||
colorMode = index
|
||||
Config.colorMode = index
|
||||
ThemeState.colorMode = index
|
||||
}
|
||||
)
|
||||
|
||||
if (isRunningAsStub && ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
|
||||
SettingsArrow(
|
||||
title = stringResource(CoreR.string.add_shortcut_title),
|
||||
summary = stringResource(CoreR.string.setting_add_shortcut_summary),
|
||||
onClick = { viewModel.requestAddShortcut() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- App Settings ---
|
||||
|
||||
@Composable
|
||||
private fun AppSettingsSection(viewModel: SettingsViewModel) {
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val hidden = context.packageName != BuildConfig.APP_PACKAGE_NAME
|
||||
|
||||
SmallTitle(text = stringResource(CoreR.string.home_app_title))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
// Update Channel
|
||||
val updateChannelEntries = remember {
|
||||
resources.getStringArray(CoreR.array.update_channel).toList()
|
||||
}
|
||||
var updateChannel by remember {
|
||||
mutableIntStateOf(Config.updateChannel.coerceIn(0, updateChannelEntries.size - 1))
|
||||
}
|
||||
var showUrlDialog by remember { mutableStateOf(false) }
|
||||
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.settings_update_channel_title),
|
||||
items = updateChannelEntries,
|
||||
selectedIndex = updateChannel,
|
||||
onSelectedIndexChange = { index ->
|
||||
updateChannel = index
|
||||
Config.updateChannel = index
|
||||
Info.resetUpdate()
|
||||
if (index == Config.Value.CUSTOM_CHANNEL && Config.customChannelUrl.isBlank()) {
|
||||
showUrlDialog = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Update Channel URL (for custom channel)
|
||||
if (updateChannel == Config.Value.CUSTOM_CHANNEL) {
|
||||
UpdateChannelUrlDialog(
|
||||
show = showUrlDialog,
|
||||
onDismiss = { showUrlDialog = false }
|
||||
)
|
||||
SettingsArrow(
|
||||
title = stringResource(CoreR.string.settings_update_custom),
|
||||
summary = Config.customChannelUrl.ifBlank { null },
|
||||
onClick = { showUrlDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
// DoH Toggle
|
||||
var doh by remember { mutableStateOf(Config.doh) }
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.settings_doh_title),
|
||||
summary = stringResource(CoreR.string.settings_doh_description),
|
||||
checked = doh,
|
||||
onCheckedChange = {
|
||||
doh = it
|
||||
Config.doh = it
|
||||
}
|
||||
)
|
||||
|
||||
// Update Checker
|
||||
var checkUpdate by remember { mutableStateOf(Config.checkUpdate) }
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.settings_check_update_title),
|
||||
summary = stringResource(CoreR.string.settings_check_update_summary),
|
||||
checked = checkUpdate,
|
||||
onCheckedChange = { newValue ->
|
||||
checkUpdate = newValue
|
||||
Config.checkUpdate = newValue
|
||||
}
|
||||
)
|
||||
|
||||
// Download Path
|
||||
var showDownloadDialog by remember { mutableStateOf(false) }
|
||||
DownloadPathDialog(
|
||||
show = showDownloadDialog,
|
||||
onDismiss = { showDownloadDialog = false }
|
||||
)
|
||||
SettingsArrow(
|
||||
title = stringResource(CoreR.string.settings_download_path_title),
|
||||
summary = MediaStoreUtils.fullPath(Config.downloadDir),
|
||||
onClick = {
|
||||
showDownloadDialog = true
|
||||
}
|
||||
)
|
||||
|
||||
// Random Package Name
|
||||
var randName by remember { mutableStateOf(Config.randName) }
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.settings_random_name_title),
|
||||
summary = stringResource(CoreR.string.settings_random_name_description),
|
||||
checked = randName,
|
||||
onCheckedChange = {
|
||||
randName = it
|
||||
Config.randName = it
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// --- Magisk ---
|
||||
|
||||
@Composable
|
||||
private fun MagiskSection(viewModel: SettingsViewModel) {
|
||||
SmallTitle(text = stringResource(CoreR.string.magisk))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
// Systemless Hosts
|
||||
SettingsArrow(
|
||||
title = stringResource(CoreR.string.settings_hosts_title),
|
||||
summary = stringResource(CoreR.string.settings_hosts_summary),
|
||||
onClick = { viewModel.createHosts() }
|
||||
)
|
||||
|
||||
if (Const.Version.atLeast_24_0()) {
|
||||
// Zygisk
|
||||
var zygisk by remember { mutableStateOf(Config.zygisk) }
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.zygisk),
|
||||
summary = stringResource(
|
||||
if (zygisk != Info.isZygiskEnabled) CoreR.string.reboot_apply_change
|
||||
else CoreR.string.settings_zygisk_summary
|
||||
),
|
||||
checked = zygisk,
|
||||
onCheckedChange = {
|
||||
zygisk = it
|
||||
Config.zygisk = it
|
||||
viewModel.notifyZygiskChange()
|
||||
}
|
||||
)
|
||||
|
||||
// DenyList
|
||||
val denyListEnabled by viewModel.denyListEnabled.collectAsState()
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.settings_denylist_title),
|
||||
summary = stringResource(CoreR.string.settings_denylist_summary),
|
||||
checked = denyListEnabled,
|
||||
onCheckedChange = { viewModel.toggleDenyList(it) }
|
||||
)
|
||||
|
||||
// DenyList Config
|
||||
SettingsArrow(
|
||||
title = stringResource(CoreR.string.settings_denylist_config_title),
|
||||
summary = stringResource(CoreR.string.settings_denylist_config_summary),
|
||||
onClick = { viewModel.navigateToDenyList() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Superuser ---
|
||||
|
||||
@Composable
|
||||
private fun SuperuserSection(viewModel: SettingsViewModel) {
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
|
||||
SmallTitle(text = stringResource(CoreR.string.superuser))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
// Tapjack (SDK < S)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
var tapjack by remember { mutableStateOf(Config.suTapjack) }
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.settings_su_tapjack_title),
|
||||
summary = stringResource(CoreR.string.settings_su_tapjack_summary),
|
||||
checked = tapjack,
|
||||
onCheckedChange = {
|
||||
tapjack = it
|
||||
Config.suTapjack = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Authentication
|
||||
var suAuth by remember { mutableStateOf(Config.suAuth) }
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.settings_su_auth_title),
|
||||
summary = stringResource(
|
||||
if (Info.isDeviceSecure) CoreR.string.settings_su_auth_summary
|
||||
else CoreR.string.settings_su_auth_insecure
|
||||
),
|
||||
checked = suAuth,
|
||||
enabled = Info.isDeviceSecure,
|
||||
onCheckedChange = { newValue ->
|
||||
viewModel.withAuth {
|
||||
suAuth = newValue
|
||||
Config.suAuth = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Access Mode
|
||||
val accessEntries = remember {
|
||||
resources.getStringArray(CoreR.array.su_access).toList()
|
||||
}
|
||||
var accessMode by remember { mutableIntStateOf(Config.rootMode) }
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.superuser_access),
|
||||
items = accessEntries,
|
||||
selectedIndex = accessMode,
|
||||
onSelectedIndexChange = {
|
||||
accessMode = it
|
||||
Config.rootMode = it
|
||||
}
|
||||
)
|
||||
|
||||
// Multiuser Mode
|
||||
val multiuserEntries = remember {
|
||||
resources.getStringArray(CoreR.array.multiuser_mode).toList()
|
||||
}
|
||||
val multiuserDescriptions = remember {
|
||||
resources.getStringArray(CoreR.array.multiuser_summary).toList()
|
||||
}
|
||||
var multiuserMode by remember { mutableIntStateOf(Config.suMultiuserMode) }
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.multiuser_mode),
|
||||
summary = multiuserDescriptions.getOrElse(multiuserMode) { "" },
|
||||
items = multiuserEntries,
|
||||
selectedIndex = multiuserMode,
|
||||
enabled = Const.USER_ID == 0,
|
||||
onSelectedIndexChange = {
|
||||
multiuserMode = it
|
||||
Config.suMultiuserMode = it
|
||||
}
|
||||
)
|
||||
|
||||
// Mount Namespace Mode
|
||||
val namespaceEntries = remember {
|
||||
resources.getStringArray(CoreR.array.namespace).toList()
|
||||
}
|
||||
val namespaceDescriptions = remember {
|
||||
resources.getStringArray(CoreR.array.namespace_summary).toList()
|
||||
}
|
||||
var mntNamespaceMode by remember { mutableIntStateOf(Config.suMntNamespaceMode) }
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.mount_namespace_mode),
|
||||
summary = namespaceDescriptions.getOrElse(mntNamespaceMode) { "" },
|
||||
items = namespaceEntries,
|
||||
selectedIndex = mntNamespaceMode,
|
||||
onSelectedIndexChange = {
|
||||
mntNamespaceMode = it
|
||||
Config.suMntNamespaceMode = it
|
||||
}
|
||||
)
|
||||
|
||||
// Automatic Response
|
||||
val autoResponseEntries = remember {
|
||||
resources.getStringArray(CoreR.array.auto_response).toList()
|
||||
}
|
||||
var autoResponse by remember { mutableIntStateOf(Config.suAutoResponse) }
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.auto_response),
|
||||
items = autoResponseEntries,
|
||||
selectedIndex = autoResponse,
|
||||
onSelectedIndexChange = { newIndex ->
|
||||
val doIt = {
|
||||
autoResponse = newIndex
|
||||
Config.suAutoResponse = newIndex
|
||||
}
|
||||
if (Config.suAuth) viewModel.withAuth(doIt) else doIt()
|
||||
}
|
||||
)
|
||||
|
||||
// Request Timeout
|
||||
val timeoutEntries = remember {
|
||||
resources.getStringArray(CoreR.array.request_timeout).toList()
|
||||
}
|
||||
val timeoutValues = remember { listOf(10, 15, 20, 30, 45, 60) }
|
||||
var timeoutIndex by remember {
|
||||
mutableIntStateOf(timeoutValues.indexOf(Config.suDefaultTimeout).coerceAtLeast(0))
|
||||
}
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.request_timeout),
|
||||
items = timeoutEntries,
|
||||
selectedIndex = timeoutIndex,
|
||||
onSelectedIndexChange = {
|
||||
timeoutIndex = it
|
||||
Config.suDefaultTimeout = timeoutValues[it]
|
||||
}
|
||||
)
|
||||
|
||||
// SU Notification
|
||||
val notifEntries = remember {
|
||||
resources.getStringArray(CoreR.array.su_notification).toList()
|
||||
}
|
||||
var suNotification by remember { mutableIntStateOf(Config.suNotification) }
|
||||
SettingsDropdown(
|
||||
title = stringResource(CoreR.string.superuser_notification),
|
||||
items = notifEntries,
|
||||
selectedIndex = suNotification,
|
||||
onSelectedIndexChange = {
|
||||
suNotification = it
|
||||
Config.suNotification = it
|
||||
}
|
||||
)
|
||||
|
||||
// Reauthenticate (SDK < O)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
var reAuth by remember { mutableStateOf(Config.suReAuth) }
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.settings_su_reauth_title),
|
||||
summary = stringResource(CoreR.string.settings_su_reauth_summary),
|
||||
checked = reAuth,
|
||||
onCheckedChange = {
|
||||
reAuth = it
|
||||
Config.suReAuth = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Restrict (version >= 30.1)
|
||||
if (Const.Version.atLeast_30_1()) {
|
||||
var restrict by remember { mutableStateOf(Config.suRestrict) }
|
||||
SettingsSwitch(
|
||||
title = stringResource(CoreR.string.settings_su_restrict_title),
|
||||
summary = stringResource(CoreR.string.settings_su_restrict_summary),
|
||||
checked = restrict,
|
||||
onCheckedChange = {
|
||||
restrict = it
|
||||
Config.suRestrict = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dialogs ---
|
||||
|
||||
@Composable
|
||||
private fun UpdateChannelUrlDialog(show: Boolean, onDismiss: () -> Unit) {
|
||||
val showState = rememberSaveable { mutableStateOf(show) }
|
||||
showState.value = show
|
||||
var url by rememberSaveable { mutableStateOf(Config.customChannelUrl) }
|
||||
|
||||
if (showState.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(CoreR.string.settings_update_custom_msg)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
Config.customChannelUrl = url
|
||||
Info.resetUpdate()
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadPathDialog(show: Boolean, onDismiss: () -> Unit) {
|
||||
val showState = rememberSaveable { mutableStateOf(show) }
|
||||
showState.value = show
|
||||
var path by rememberSaveable { mutableStateOf(Config.downloadDir) }
|
||||
|
||||
if (showState.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(CoreR.string.settings_download_path_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.settings_download_path_message, MediaStoreUtils.fullPath(path)),
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = path,
|
||||
onValueChange = { path = it },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
Config.downloadDir = path
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HideAppDialog(show: Boolean, onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
|
||||
val showState = rememberSaveable { mutableStateOf(show) }
|
||||
showState.value = show
|
||||
var appName by rememberSaveable { mutableStateOf("Settings") }
|
||||
val isError = appName.length > AppMigration.MAX_LABEL_LENGTH || appName.isBlank()
|
||||
|
||||
if (showState.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(CoreR.string.settings_hide_app_title)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = appName,
|
||||
onValueChange = { appName = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text(stringResource(CoreR.string.settings_app_name_hint)) }
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { if (!isError) onConfirm(appName) }
|
||||
) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RestoreDialog(show: Boolean, onDismiss: () -> Unit, onConfirm: () -> Unit) {
|
||||
val showState = rememberSaveable { mutableStateOf(show) }
|
||||
showState.value = show
|
||||
|
||||
if (showState.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(CoreR.string.settings_restore_app_title)) },
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.restore_app_confirmation),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.topjohnwu.magisk.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SettingsViewModel : BaseViewModel() {
|
||||
|
||||
private val _denyListEnabled = MutableStateFlow(Config.denyList)
|
||||
val denyListEnabled: StateFlow<Boolean> = _denyListEnabled.asStateFlow()
|
||||
|
||||
val zygiskMismatch get() = Config.zygisk != Info.isZygiskEnabled
|
||||
|
||||
var authenticate: (onSuccess: () -> Unit) -> Unit = { it() }
|
||||
|
||||
fun navigateToDenyList() {
|
||||
navigateTo(Route.DenyList)
|
||||
}
|
||||
|
||||
fun requestAddShortcut() {
|
||||
Shortcuts.addHomeIcon(AppContext)
|
||||
}
|
||||
|
||||
suspend fun hideApp(context: Context, name: String): Boolean {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
AppMigration.patchAndHide(context, name)
|
||||
}
|
||||
if (!success) {
|
||||
context.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun restoreApp(context: Context): Boolean {
|
||||
val success = AppMigration.restoreApp(context)
|
||||
if (!success) {
|
||||
context.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun createHosts() {
|
||||
viewModelScope.launch {
|
||||
RootUtils.addSystemlessHosts()
|
||||
AppContext.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleDenyList(enabled: Boolean) {
|
||||
_denyListEnabled.value = enabled
|
||||
val cmd = if (enabled) "enable" else "disable"
|
||||
Shell.cmd("magisk --denylist $cmd").submit { result ->
|
||||
if (result.isSuccess) {
|
||||
Config.denyList = enabled
|
||||
} else {
|
||||
_denyListEnabled.value = !enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun withAuth(action: () -> Unit) = authenticate(action)
|
||||
|
||||
fun notifyZygiskChange() {
|
||||
if (zygiskMismatch) showSnackbar(R.string.reboot_apply_change)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.topjohnwu.magisk.ui.superuser
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.ui.component.ConfirmResult
|
||||
import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SuperuserDetailScreen(
|
||||
uid: Int,
|
||||
viewModel: SuperuserViewModel,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val items = uiState.policies.filter { it.policy.uid == uid }
|
||||
val item = items.firstOrNull()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val scope = rememberCoroutineScope()
|
||||
val revokeDialog = rememberConfirmDialog()
|
||||
val revokeTitle = stringResource(CoreR.string.su_revoke_title)
|
||||
val revokeMsg = item?.let { stringResource(CoreR.string.su_revoke_msg, it.appName) } ?: ""
|
||||
|
||||
LaunchedEffect(Unit) { viewModel.refreshSuRestrict() }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(CoreR.string.superuser_setting)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(CoreR.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
if (item == null) return@Scaffold
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(item.icon),
|
||||
contentDescription = item.appName,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (item.isSharedUid) {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
SharedUidBadge()
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.packageName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "UID: ${item.policy.uid}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
if (uiState.suRestrict || item.isRestricted) {
|
||||
SwitchRow(
|
||||
title = stringResource(CoreR.string.settings_su_restrict_title),
|
||||
checked = item.isRestricted,
|
||||
onCheckedChange = { viewModel.toggleRestrict(item) }
|
||||
)
|
||||
}
|
||||
SwitchRow(
|
||||
title = stringResource(CoreR.string.superuser_toggle_notification),
|
||||
checked = item.notification,
|
||||
onCheckedChange = { viewModel.updateNotify(item) }
|
||||
)
|
||||
SwitchRow(
|
||||
title = stringResource(CoreR.string.logs),
|
||||
checked = item.logging,
|
||||
onCheckedChange = { viewModel.updateLogging(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
if (viewModel.requiresAuth) {
|
||||
viewModel.authenticate { viewModel.performDelete(item, onBack) }
|
||||
} else {
|
||||
scope.launch {
|
||||
val result = revokeDialog.awaitConfirm(
|
||||
title = revokeTitle,
|
||||
content = revokeMsg,
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
viewModel.performDelete(item, onBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
RevokeRow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwitchRow(
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = { onCheckedChange() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RevokeRow() {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.superuser_toggle_revoke),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.topjohnwu.magisk.ui.superuser
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SuperuserScreen(viewModel: SuperuserViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val navigator = LocalNavigator.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(CoreR.string.superuser)) },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
if (uiState.loading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
if (uiState.policies.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.superuser_policy_none),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
items(uiState.policies, key = { "${it.policy.uid}_${it.packageName}" }) { item ->
|
||||
PolicyCard(
|
||||
item = item,
|
||||
onToggle = { viewModel.togglePolicy(item) },
|
||||
onDetail = { navigator.push(Route.SuperuserDetail(item.policy.uid)) },
|
||||
)
|
||||
}
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PolicyCard(
|
||||
item: PolicyItem,
|
||||
onToggle: () -> Unit,
|
||||
onDetail: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (item.isEnabled) 1f else 0.5f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable(onClick = onDetail)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(item.icon),
|
||||
contentDescription = item.appName,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (item.isSharedUid) {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
SharedUidBadge()
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.packageName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 12.dp)
|
||||
.width(0.5.dp)
|
||||
.background(MaterialTheme.colorScheme.outlineVariant)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Switch(
|
||||
checked = item.isEnabled,
|
||||
onCheckedChange = { onToggle() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SharedUidBadge(modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "SharedUID",
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
fontSize = 10.sp,
|
||||
maxLines = 1,
|
||||
modifier = modifier
|
||||
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(6.dp))
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.topjohnwu.magisk.ui.superuser
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Process
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.su.SuEvents
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
class PolicyItem(
|
||||
val policy: SuPolicy,
|
||||
val packageName: String,
|
||||
val isSharedUid: Boolean,
|
||||
val icon: Drawable,
|
||||
val appName: String,
|
||||
) {
|
||||
val title get() = appName
|
||||
val showSlider = Config.suRestrict || policy.policy == SuPolicy.RESTRICT
|
||||
|
||||
var isExpanded by mutableStateOf(false)
|
||||
var policyValue by mutableIntStateOf(policy.policy)
|
||||
var notification by mutableStateOf(policy.notification)
|
||||
var logging by mutableStateOf(policy.logging)
|
||||
|
||||
val isEnabled get() = policyValue >= SuPolicy.ALLOW
|
||||
val isRestricted get() = policyValue == SuPolicy.RESTRICT
|
||||
}
|
||||
|
||||
class SuperuserViewModel(
|
||||
private val db: PolicyDao
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
var authenticate: (onSuccess: () -> Unit) -> Unit = { it() }
|
||||
|
||||
init {
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
viewModelScope.launch {
|
||||
SuEvents.policyChanged.debounce(500).collect { reload() }
|
||||
}
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = true,
|
||||
val policies: List<PolicyItem> = emptyList(),
|
||||
val suRestrict: Boolean = Config.suRestrict,
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override suspend fun doLoadWork() {
|
||||
if (!Info.showSuperUser) {
|
||||
_uiState.update { it.copy(loading = false) }
|
||||
return
|
||||
}
|
||||
_uiState.update { it.copy(loading = true) }
|
||||
withContext(Dispatchers.IO) {
|
||||
db.deleteOutdated()
|
||||
db.delete(AppContext.applicationInfo.uid)
|
||||
val policies = ArrayList<PolicyItem>()
|
||||
val pm = AppContext.packageManager
|
||||
for (policy in db.fetchAll()) {
|
||||
val pkgs =
|
||||
if (policy.uid == Process.SYSTEM_UID) arrayOf("android")
|
||||
else pm.getPackagesForUid(policy.uid)
|
||||
if (pkgs == null) {
|
||||
db.delete(policy.uid)
|
||||
continue
|
||||
}
|
||||
val map = pkgs.mapNotNull { pkg ->
|
||||
try {
|
||||
val info = pm.getPackageInfo(pkg, MATCH_UNINSTALLED_PACKAGES)
|
||||
PolicyItem(
|
||||
policy = policy,
|
||||
packageName = info.packageName,
|
||||
isSharedUid = info.sharedUserId != null,
|
||||
icon = info.applicationInfo?.loadIcon(pm) ?: pm.defaultActivityIcon,
|
||||
appName = info.applicationInfo?.getLabel(pm) ?: info.packageName
|
||||
)
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (map.isEmpty()) {
|
||||
db.delete(policy.uid)
|
||||
continue
|
||||
}
|
||||
policies.addAll(map)
|
||||
}
|
||||
policies.sortWith(compareBy(
|
||||
{ it.appName.lowercase(Locale.ROOT) },
|
||||
{ it.packageName }
|
||||
))
|
||||
_uiState.update { it.copy(loading = false, policies = policies, suRestrict = Config.suRestrict) }
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSuRestrict() {
|
||||
_uiState.update { it.copy(suRestrict = Config.suRestrict) }
|
||||
}
|
||||
|
||||
val requiresAuth get() = Config.suAuth
|
||||
|
||||
fun performDelete(item: PolicyItem, onDeleted: () -> Unit = {}) {
|
||||
viewModelScope.launch {
|
||||
db.delete(item.policy.uid)
|
||||
_uiState.update { state ->
|
||||
state.copy(policies = state.policies.filter { it.policy.uid != item.policy.uid })
|
||||
}
|
||||
onDeleted()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotify(item: PolicyItem) {
|
||||
item.notification = !item.notification
|
||||
item.policy.notification = item.notification
|
||||
viewModelScope.launch {
|
||||
db.update(item.policy)
|
||||
_uiState.value.policies
|
||||
.filter { it.policy.uid == item.policy.uid }
|
||||
.forEach { it.notification = item.notification }
|
||||
val res = if (item.notification) R.string.su_snack_notif_on else R.string.su_snack_notif_off
|
||||
showSnackbar(AppContext.getString(res, item.appName))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLogging(item: PolicyItem) {
|
||||
item.logging = !item.logging
|
||||
item.policy.logging = item.logging
|
||||
viewModelScope.launch {
|
||||
db.update(item.policy)
|
||||
_uiState.value.policies
|
||||
.filter { it.policy.uid == item.policy.uid }
|
||||
.forEach { it.logging = item.logging }
|
||||
val res = if (item.logging) R.string.su_snack_log_on else R.string.su_snack_log_off
|
||||
showSnackbar(AppContext.getString(res, item.appName))
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePolicy(item: PolicyItem, newPolicy: Int) {
|
||||
fun updateState() {
|
||||
viewModelScope.launch {
|
||||
item.policy.policy = newPolicy
|
||||
item.policyValue = newPolicy
|
||||
db.update(item.policy)
|
||||
_uiState.value.policies
|
||||
.filter { it.policy.uid == item.policy.uid }
|
||||
.forEach { it.policyValue = newPolicy }
|
||||
val res = if (newPolicy >= SuPolicy.ALLOW) R.string.su_snack_grant else R.string.su_snack_deny
|
||||
showSnackbar(AppContext.getString(res, item.appName))
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.suAuth) {
|
||||
authenticate { updateState() }
|
||||
} else {
|
||||
updateState()
|
||||
}
|
||||
}
|
||||
|
||||
fun togglePolicy(item: PolicyItem) {
|
||||
val newPolicy = if (item.isEnabled) SuPolicy.DENY else SuPolicy.ALLOW
|
||||
updatePolicy(item, newPolicy)
|
||||
}
|
||||
|
||||
fun toggleRestrict(item: PolicyItem) {
|
||||
val newPolicy = if (item.isRestricted) SuPolicy.ALLOW else SuPolicy.RESTRICT
|
||||
updatePolicy(item, newPolicy)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.view.accessibility.AccessibilityNodeProvider
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.VMFactory
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.ActivityExtension
|
||||
import com.topjohnwu.magisk.core.base.UntrackedActivity
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ui.theme.MagiskTheme
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
open class SuRequestActivity : AppCompatActivity(), UntrackedActivity {
|
||||
|
||||
private val extension = ActivityExtension(this)
|
||||
private val viewModel: SuRequestViewModel by lazy {
|
||||
ViewModelProvider(this, VMFactory)[SuRequestViewModel::class.java]
|
||||
}
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
extension.onCreate(savedInstanceState)
|
||||
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
window.addFlags(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
window.setHideOverlayWindows(true)
|
||||
}
|
||||
setTheme(Theme.selected.themeRes)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.finishActivity = { finish() }
|
||||
viewModel.authenticate = { onSuccess ->
|
||||
extension.withAuthentication { if (it) onSuccess() }
|
||||
}
|
||||
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val action = intent.getStringExtra("action")
|
||||
if (action == REQUEST) {
|
||||
viewModel.handleRequest(intent)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
SuCallbackHandler.run(this@SuRequestActivity, action, intent.extras)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
|
||||
if (viewModel.useTapjackProtection) {
|
||||
window.decorView.rootView.accessibilityDelegate = EmptyAccessibilityDelegate
|
||||
}
|
||||
|
||||
setContent {
|
||||
MagiskTheme {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
SuRequestScreen(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
extension.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
val theme = super.getTheme()
|
||||
theme.applyStyle(R.style.Foundation_Floating, true)
|
||||
return theme
|
||||
}
|
||||
|
||||
@Deprecated("Use OnBackPressedDispatcher")
|
||||
override fun onBackPressed() {
|
||||
viewModel.denyPressed()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finishAndRemoveTask()
|
||||
}
|
||||
|
||||
private object EmptyAccessibilityDelegate : View.AccessibilityDelegate() {
|
||||
override fun sendAccessibilityEvent(host: View, eventType: Int) {}
|
||||
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?) = true
|
||||
override fun sendAccessibilityEventUnchecked(host: View, event: AccessibilityEvent) {}
|
||||
override fun dispatchPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) = true
|
||||
override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {}
|
||||
override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {}
|
||||
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {}
|
||||
override fun addExtraDataToAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo, extraDataKey: String, arguments: Bundle?) {}
|
||||
override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean = false
|
||||
override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProvider? = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.view.MotionEvent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.ui.superuser.SharedUidBadge
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SuRequestScreen(viewModel: SuRequestViewModel) {
|
||||
if (!viewModel.showUi) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val icon = viewModel.icon
|
||||
val title = viewModel.title
|
||||
val packageName = viewModel.packageName
|
||||
val grantEnabled = viewModel.grantEnabled
|
||||
val denyCountdown = viewModel.denyCountdown
|
||||
val selectedPosition = viewModel.selectedItemPosition
|
||||
val timeoutEntries = stringArrayResource(CoreR.array.allow_timeout).toList()
|
||||
// Slider order: Once(1), 10min(2), 20min(3), 30min(4), 60min(5), Forever(0)
|
||||
val sliderToIndex = intArrayOf(1, 2, 3, 4, 5, 0)
|
||||
val indexToSlider = remember {
|
||||
IntArray(sliderToIndex.size).also { arr ->
|
||||
sliderToIndex.forEachIndexed { slider, orig -> arr[orig] = slider }
|
||||
}
|
||||
}
|
||||
val sliderValue = indexToSlider[selectedPosition].toFloat()
|
||||
val sliderLabel by remember(sliderValue) {
|
||||
derivedStateOf { timeoutEntries[sliderToIndex[sliderValue.toInt()]] }
|
||||
}
|
||||
|
||||
val denyText = if (denyCountdown > 0) {
|
||||
"${stringResource(CoreR.string.deny)} ($denyCountdown)"
|
||||
} else {
|
||||
stringResource(CoreR.string.deny)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.widthIn(min = 320.dp, max = 420.dp)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
if (icon != null) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
}
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (viewModel.isSharedUid) {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
SharedUidBadge()
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = packageName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(CoreR.string.su_request_title),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Permission timeout: $sliderLabel",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 8.dp),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Slider(
|
||||
value = sliderValue,
|
||||
onValueChange = { value ->
|
||||
viewModel.spinnerTouched()
|
||||
val pos = value.toInt().coerceIn(0, sliderToIndex.lastIndex)
|
||||
viewModel.selectedItemPosition = sliderToIndex[pos]
|
||||
},
|
||||
valueRange = 0f..5f,
|
||||
steps = 4,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.denyPressed() },
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(denyText)
|
||||
}
|
||||
Button(
|
||||
enabled = grantEnabled,
|
||||
onClick = { viewModel.grantPressed() },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.then(
|
||||
if (viewModel.useTapjackProtection) {
|
||||
Modifier.pointerInteropFilter { event ->
|
||||
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0 ||
|
||||
event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
|
||||
) {
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
context.toast(
|
||||
CoreR.string.touch_filtered_warning,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
Text(stringResource(CoreR.string.grant))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.CountDownTimer
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY
|
||||
import com.topjohnwu.magisk.core.su.SuRequestHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
|
||||
class SuRequestViewModel(
|
||||
policyDB: PolicyDao,
|
||||
private val timeoutPrefs: SharedPreferences
|
||||
) : BaseViewModel() {
|
||||
|
||||
var authenticate: (onSuccess: () -> Unit) -> Unit = { it() }
|
||||
var finishActivity: () -> Unit = {}
|
||||
|
||||
var icon by mutableStateOf<Drawable?>(null)
|
||||
var title by mutableStateOf("")
|
||||
var packageName by mutableStateOf("")
|
||||
var isSharedUid by mutableStateOf(false)
|
||||
|
||||
var selectedItemPosition by mutableIntStateOf(0)
|
||||
var grantEnabled by mutableStateOf(false)
|
||||
var denyCountdown by mutableIntStateOf(0)
|
||||
|
||||
var showUi by mutableStateOf(false)
|
||||
var useTapjackProtection by mutableStateOf(false)
|
||||
|
||||
private val handler = SuRequestHandler(AppContext.packageManager, policyDB)
|
||||
private val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
|
||||
private var timer = SuTimer(millis, 1000)
|
||||
private var initialized = false
|
||||
|
||||
fun grantPressed() {
|
||||
cancelTimer()
|
||||
if (Config.suAuth) {
|
||||
authenticate { respond(ALLOW) }
|
||||
} else {
|
||||
respond(ALLOW)
|
||||
}
|
||||
}
|
||||
|
||||
fun denyPressed() {
|
||||
respond(DENY)
|
||||
}
|
||||
|
||||
fun spinnerTouched() {
|
||||
cancelTimer()
|
||||
}
|
||||
|
||||
fun handleRequest(intent: Intent) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
if (handler.start(intent))
|
||||
showDialog()
|
||||
else
|
||||
finishActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
val pm = handler.pm
|
||||
val info = handler.pkgInfo
|
||||
val app = info.applicationInfo
|
||||
|
||||
isSharedUid = info.sharedUserId != null
|
||||
if (app == null) {
|
||||
icon = pm.defaultActivityIcon
|
||||
title = info.sharedUserId.toString()
|
||||
packageName = info.sharedUserId.toString()
|
||||
} else {
|
||||
icon = app.loadIcon(pm)
|
||||
title = app.getLabel(pm)
|
||||
packageName = info.packageName
|
||||
}
|
||||
|
||||
selectedItemPosition = timeoutPrefs.getInt(packageName, 0)
|
||||
timer.start()
|
||||
useTapjackProtection = Config.suTapjack
|
||||
showUi = true
|
||||
initialized = true
|
||||
}
|
||||
|
||||
private fun respond(action: Int) {
|
||||
if (!initialized) return
|
||||
timer.cancel()
|
||||
|
||||
val pos = selectedItemPosition
|
||||
timeoutPrefs.edit().putInt(packageName, pos).apply()
|
||||
|
||||
viewModelScope.launch {
|
||||
handler.respond(action, Config.Value.TIMEOUT_LIST[pos])
|
||||
finishActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelTimer() {
|
||||
timer.cancel()
|
||||
denyCountdown = 0
|
||||
}
|
||||
|
||||
private inner class SuTimer(
|
||||
private val millis: Long,
|
||||
interval: Long
|
||||
) : CountDownTimer(millis, interval) {
|
||||
|
||||
override fun onTick(remains: Long) {
|
||||
if (!grantEnabled && remains <= millis - 1000) {
|
||||
grantEnabled = true
|
||||
}
|
||||
denyCountdown = (remains / 1000).toInt() + 1
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
denyCountdown = 0
|
||||
respond(DENY)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package com.topjohnwu.magisk.ui.terminal
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Typeface
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.TextStyle
|
||||
import com.topjohnwu.magisk.terminal.WcWidth
|
||||
|
||||
/**
|
||||
* Renderer of a [TerminalEmulator] into a [Canvas].
|
||||
*
|
||||
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
|
||||
*/
|
||||
class TerminalRenderer(
|
||||
textSize: Int,
|
||||
typeface: Typeface,
|
||||
) {
|
||||
val textSize: Int = textSize
|
||||
val typeface: Typeface = typeface
|
||||
private val textPaint = Paint()
|
||||
|
||||
/** The width of a single mono spaced character obtained by [Paint.measureText] on a single 'X'. */
|
||||
val fontWidth: Float
|
||||
|
||||
/** The [Paint.getFontSpacing]. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
val fontLineSpacing: Int
|
||||
|
||||
/** The [Paint.ascent]. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
private val fontAscent: Int
|
||||
|
||||
/** The [fontLineSpacing] + [fontAscent]. */
|
||||
val fontLineSpacingAndAscent: Int
|
||||
|
||||
private val asciiMeasures = FloatArray(127)
|
||||
|
||||
init {
|
||||
textPaint.typeface = typeface
|
||||
textPaint.isAntiAlias = true
|
||||
textPaint.textSize = textSize.toFloat()
|
||||
|
||||
fontLineSpacing = kotlin.math.ceil(textPaint.fontSpacing).toInt()
|
||||
fontAscent = kotlin.math.ceil(textPaint.ascent()).toInt()
|
||||
fontLineSpacingAndAscent = fontLineSpacing + fontAscent
|
||||
fontWidth = textPaint.measureText("X")
|
||||
|
||||
val sb = StringBuilder(" ")
|
||||
for (i in asciiMeasures.indices) {
|
||||
sb[0] = i.toChar()
|
||||
asciiMeasures[i] = textPaint.measureText(sb, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection.
|
||||
*/
|
||||
fun render(
|
||||
mEmulator: TerminalEmulator,
|
||||
canvas: Canvas,
|
||||
topRow: Int,
|
||||
selectionY1: Int,
|
||||
selectionY2: Int,
|
||||
selectionX1: Int,
|
||||
selectionX2: Int,
|
||||
) {
|
||||
val reverseVideo = mEmulator.isReverseVideo
|
||||
val endRow = topRow + mEmulator.mRows
|
||||
val columns = mEmulator.mColumns
|
||||
val cursorCol = mEmulator.cursorCol
|
||||
val cursorRow = mEmulator.cursorRow
|
||||
val cursorVisible = mEmulator.shouldCursorBeVisible()
|
||||
val screen = mEmulator.screen
|
||||
val palette = mEmulator.mColors.currentColors
|
||||
val cursorShape = mEmulator.cursorStyle
|
||||
|
||||
if (reverseVideo) {
|
||||
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC)
|
||||
}
|
||||
|
||||
var heightOffset = fontLineSpacingAndAscent.toFloat()
|
||||
for (row in topRow until endRow) {
|
||||
heightOffset += fontLineSpacing
|
||||
|
||||
val cursorX = if (row == cursorRow && cursorVisible) cursorCol else -1
|
||||
var selx1 = -1
|
||||
var selx2 = -1
|
||||
if (row in selectionY1..selectionY2) {
|
||||
if (row == selectionY1) selx1 = selectionX1
|
||||
selx2 = if (row == selectionY2) selectionX2 else mEmulator.mColumns
|
||||
}
|
||||
|
||||
val lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row))
|
||||
val line = lineObject.text
|
||||
val charsUsedInLine = lineObject.spaceUsed
|
||||
|
||||
var lastRunStyle = 0L
|
||||
var lastRunInsideCursor = false
|
||||
var lastRunInsideSelection = false
|
||||
var lastRunStartColumn = -1
|
||||
var lastRunStartIndex = 0
|
||||
var lastRunFontWidthMismatch = false
|
||||
var currentCharIndex = 0
|
||||
var measuredWidthForRun = 0f
|
||||
|
||||
var column = 0
|
||||
while (column < columns) {
|
||||
val charAtIndex = line[currentCharIndex]
|
||||
val charIsHighsurrogate = Character.isHighSurrogate(charAtIndex)
|
||||
val charsForCodePoint = if (charIsHighsurrogate) 2 else 1
|
||||
val codePoint = if (charIsHighsurrogate) {
|
||||
Character.toCodePoint(charAtIndex, line[currentCharIndex + 1])
|
||||
} else {
|
||||
charAtIndex.code
|
||||
}
|
||||
val codePointWcWidth = WcWidth.width(codePoint)
|
||||
val insideCursor = cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)
|
||||
val insideSelection = column >= selx1 && column <= selx2
|
||||
val style = lineObject.getStyle(column)
|
||||
|
||||
// Check if the measured text width for this code point is not the same as that expected by wcwidth().
|
||||
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as
|
||||
// smileys which android font renders as wide.
|
||||
// If this is detected, we draw this code point scaled to match what wcwidth() expects.
|
||||
val measuredCodePointWidth = if (codePoint < asciiMeasures.size) {
|
||||
asciiMeasures[codePoint]
|
||||
} else {
|
||||
textPaint.measureText(line, currentCharIndex, charsForCodePoint)
|
||||
}
|
||||
val fontWidthMismatch = kotlin.math.abs(measuredCodePointWidth / fontWidth - codePointWcWidth.toFloat()) > 0.01f
|
||||
|
||||
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection ||
|
||||
fontWidthMismatch || lastRunFontWidthMismatch
|
||||
) {
|
||||
if (column != 0) {
|
||||
val columnWidthSinceLastRun = column - lastRunStartColumn
|
||||
val charsSinceLastRun = currentCharIndex - lastRunStartIndex
|
||||
val cursorColor = if (lastRunInsideCursor) mEmulator.mColors.currentColors[TextStyle.COLOR_INDEX_CURSOR] else 0
|
||||
var invertCursorTextColor = false
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true
|
||||
}
|
||||
drawTextRun(
|
||||
canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle,
|
||||
reverseVideo || invertCursorTextColor || lastRunInsideSelection,
|
||||
)
|
||||
}
|
||||
measuredWidthForRun = 0f
|
||||
lastRunStyle = style
|
||||
lastRunInsideCursor = insideCursor
|
||||
lastRunInsideSelection = insideSelection
|
||||
lastRunStartColumn = column
|
||||
lastRunStartIndex = currentCharIndex
|
||||
lastRunFontWidthMismatch = fontWidthMismatch
|
||||
}
|
||||
measuredWidthForRun += measuredCodePointWidth
|
||||
column += codePointWcWidth
|
||||
currentCharIndex += charsForCodePoint
|
||||
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
|
||||
// Eat combining chars so that they are treated as part of the last non-combining code point,
|
||||
// instead of e.g. being considered inside the cursor in the next run.
|
||||
currentCharIndex += if (Character.isHighSurrogate(line[currentCharIndex])) 2 else 1
|
||||
}
|
||||
}
|
||||
|
||||
val columnWidthSinceLastRun = columns - lastRunStartColumn
|
||||
val charsSinceLastRun = currentCharIndex - lastRunStartIndex
|
||||
val cursorColor = if (lastRunInsideCursor) mEmulator.mColors.currentColors[TextStyle.COLOR_INDEX_CURSOR] else 0
|
||||
var invertCursorTextColor = false
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true
|
||||
}
|
||||
drawTextRun(
|
||||
canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle,
|
||||
reverseVideo || invertCursorTextColor || lastRunInsideSelection,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawTextRun(
|
||||
canvas: Canvas,
|
||||
text: CharArray,
|
||||
palette: IntArray,
|
||||
y: Float,
|
||||
startColumn: Int,
|
||||
runWidthColumns: Int,
|
||||
startCharIndex: Int,
|
||||
runWidthChars: Int,
|
||||
mes: Float,
|
||||
cursor: Int,
|
||||
cursorStyle: Int,
|
||||
textStyle: Long,
|
||||
reverseVideo: Boolean,
|
||||
) {
|
||||
var foreColor = TextStyle.decodeForeColor(textStyle)
|
||||
val effect = TextStyle.decodeEffect(textStyle)
|
||||
var backColor = TextStyle.decodeBackColor(textStyle)
|
||||
val bold = (effect and (TextStyle.CHARACTER_ATTRIBUTE_BOLD or TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0
|
||||
val underline = (effect and TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0
|
||||
val italic = (effect and TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0
|
||||
val strikeThrough = (effect and TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0
|
||||
val dim = (effect and TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0
|
||||
|
||||
if ((foreColor and 0xff000000.toInt()) != 0xff000000.toInt()) {
|
||||
// Let bold have bright colors if applicable (one of the first 8):
|
||||
if (bold && foreColor in 0..7) foreColor += 8
|
||||
foreColor = palette[foreColor]
|
||||
}
|
||||
|
||||
if ((backColor and 0xff000000.toInt()) != 0xff000000.toInt()) {
|
||||
backColor = palette[backColor]
|
||||
}
|
||||
|
||||
// Reverse video here if _one and only one_ of the reverse flags are set:
|
||||
val reverseVideoHere = reverseVideo xor ((effect and TextStyle.CHARACTER_ATTRIBUTE_INVERSE) != 0)
|
||||
if (reverseVideoHere) {
|
||||
val tmp = foreColor
|
||||
foreColor = backColor
|
||||
backColor = tmp
|
||||
}
|
||||
|
||||
var left = startColumn * fontWidth
|
||||
var right = left + runWidthColumns * fontWidth
|
||||
|
||||
var adjustedMes = mes / fontWidth
|
||||
var savedMatrix = false
|
||||
if (kotlin.math.abs(adjustedMes - runWidthColumns) > 0.01) {
|
||||
canvas.save()
|
||||
canvas.scale(runWidthColumns / adjustedMes, 1f)
|
||||
left *= adjustedMes / runWidthColumns
|
||||
right *= adjustedMes / runWidthColumns
|
||||
savedMatrix = true
|
||||
}
|
||||
|
||||
if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) {
|
||||
// Only draw non-default background.
|
||||
textPaint.color = backColor
|
||||
canvas.drawRect(left, y - fontLineSpacingAndAscent + fontAscent, right, y, textPaint)
|
||||
}
|
||||
|
||||
if (cursor != 0) {
|
||||
textPaint.color = cursor
|
||||
var cursorHeight = (fontLineSpacingAndAscent - fontAscent).toFloat()
|
||||
if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE) cursorHeight /= 4f
|
||||
else if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4f
|
||||
canvas.drawRect(left, y - cursorHeight, right, y, textPaint)
|
||||
}
|
||||
|
||||
if ((effect and TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
|
||||
if (dim) {
|
||||
var red = 0xFF and (foreColor shr 16)
|
||||
var green = 0xFF and (foreColor shr 8)
|
||||
var blue = 0xFF and foreColor
|
||||
// Dim color handling used by libvte which in turn took it from xterm
|
||||
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
|
||||
red = red * 2 / 3
|
||||
green = green * 2 / 3
|
||||
blue = blue * 2 / 3
|
||||
foreColor = -0x1000000 or (red shl 16) or (green shl 8) or blue
|
||||
}
|
||||
|
||||
textPaint.isFakeBoldText = bold
|
||||
textPaint.isUnderlineText = underline
|
||||
textPaint.textSkewX = if (italic) -0.35f else 0f
|
||||
textPaint.isStrikeThruText = strikeThrough
|
||||
textPaint.color = foreColor
|
||||
|
||||
// The text alignment is the default Paint.Align.LEFT.
|
||||
canvas.drawTextRun(
|
||||
text, startCharIndex, runWidthChars, startCharIndex, runWidthChars,
|
||||
left, y - fontLineSpacingAndAscent, false, textPaint,
|
||||
)
|
||||
}
|
||||
|
||||
if (savedMatrix) canvas.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.topjohnwu.magisk.ui.terminal
|
||||
|
||||
import android.graphics.Typeface
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun TerminalScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onEmulatorCreated: (TerminalEmulator) -> Unit = {},
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val renderer = remember {
|
||||
val textSizePx = with(density) { 12.sp.toPx().toInt() }
|
||||
TerminalRenderer(textSizePx, Typeface.MONOSPACE)
|
||||
}
|
||||
|
||||
var emulator by remember { mutableStateOf<TerminalEmulator?>(null) }
|
||||
var updateTick by remember { mutableIntStateOf(0) }
|
||||
var topRow by remember { mutableIntStateOf(0) }
|
||||
var scrolledToBottom by remember { mutableStateOf(true) }
|
||||
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val widthPx = constraints.maxWidth
|
||||
val heightPx = constraints.maxHeight
|
||||
val cols = max(4, (widthPx / renderer.fontWidth).toInt())
|
||||
val rows = max(4, (heightPx - renderer.fontLineSpacingAndAscent) / renderer.fontLineSpacing)
|
||||
val lineHeight = renderer.fontLineSpacing.toFloat()
|
||||
|
||||
LaunchedEffect(cols, rows) {
|
||||
val emu = emulator
|
||||
if (emu == null) {
|
||||
val newEmu = TerminalEmulator(cols, rows, renderer.fontWidth.toInt(), renderer.fontLineSpacing, null)
|
||||
newEmu.onScreenUpdate = {
|
||||
if (scrolledToBottom) topRow = 0
|
||||
updateTick++
|
||||
}
|
||||
emulator = newEmu
|
||||
onEmulatorCreated(newEmu)
|
||||
} else {
|
||||
emu.resize(cols, rows, renderer.fontWidth.toInt(), renderer.fontLineSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.scrollable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberScrollableState { delta ->
|
||||
val emu = emulator ?: return@rememberScrollableState 0f
|
||||
val minTop = -emu.screen.activeTranscriptRows
|
||||
val rowDelta = -(delta / lineHeight).toInt()
|
||||
if (rowDelta != 0) {
|
||||
val newTopRow = (topRow + rowDelta).coerceIn(minTop, 0)
|
||||
topRow = newTopRow
|
||||
scrolledToBottom = newTopRow >= 0
|
||||
}
|
||||
delta
|
||||
}
|
||||
)
|
||||
.drawBehind {
|
||||
@Suppress("UNUSED_EXPRESSION")
|
||||
updateTick
|
||||
val emu = emulator ?: return@drawBehind
|
||||
drawIntoCanvas { canvas ->
|
||||
renderer.render(emu, canvas.nativeCanvas, topRow, -1, -1, -1, -1)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.topjohnwu.magisk.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
|
||||
object ThemeState {
|
||||
var colorMode by mutableIntStateOf(Config.colorMode)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MagiskTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val mode = ThemeState.colorMode
|
||||
val context = LocalContext.current
|
||||
|
||||
val isDarkTheme = when (mode) {
|
||||
1 -> false
|
||||
2 -> true
|
||||
3 -> isDark
|
||||
4 -> false
|
||||
5 -> true
|
||||
else -> isDark
|
||||
}
|
||||
|
||||
val useDynamicColor = mode in listOf(3, 4, 5) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
val colorScheme = when {
|
||||
useDynamicColor && isDarkTheme -> dynamicDarkColorScheme(context)
|
||||
useDynamicColor && !isDarkTheme -> dynamicLightColorScheme(context)
|
||||
isDarkTheme -> darkColorScheme()
|
||||
else -> lightColorScheme()
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -43,10 +43,6 @@ enum class Theme(
|
||||
|
||||
val isSelected get() = Config.themeOrdinal == ordinal
|
||||
|
||||
fun select() {
|
||||
Config.themeOrdinal = ordinal
|
||||
}
|
||||
|
||||
companion object {
|
||||
val selected get() = values().getOrNull(Config.themeOrdinal) ?: Piplup
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.ui.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
|
||||
@Composable
|
||||
fun rememberDrawablePainter(drawable: Drawable): Painter {
|
||||
return remember(drawable) {
|
||||
val w = drawable.intrinsicWidth.coerceAtLeast(1)
|
||||
val h = drawable.intrinsicHeight.coerceAtLeast(1)
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
val canvas = android.graphics.Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, w, h)
|
||||
drawable.draw(canvas)
|
||||
BitmapPainter(bitmap.asImageBitmap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.provider.Settings
|
||||
|
||||
class AccessibilityUtils {
|
||||
companion object {
|
||||
fun isAnimationEnabled(cr: ContentResolver): Boolean {
|
||||
return !(Settings.Global.getFloat(cr, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f
|
||||
&& Settings.Global.getFloat(cr, Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f) == 0.0f
|
||||
&& Settings.Global.getFloat(cr, Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f) == 0.0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 20 8 L 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 M 14 16 C 14 15.43 14 14.859 14 14.289 L 14 14 C 13.869 14 13.739 14 13.608 14 C 12.405 14 11.203 14 10 14 C 10 14.509 10 15.017 10 15.526 C 10 15.684 10 15.842 10 16 L 10.33 16 C 10.392 16 10.454 16 10.515 16 C 11.677 16 12.838 16 14 16 C 14 16 14 16 14 16 M 14 10 L 14 12 L 14 12 L 10 12 L 10 10 L 14 10 M 12 15 L 12 15 L 12 15 L 12 15 L 12 15 L 12 15"
|
||||
android:valueTo="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14"
|
||||
@@ -18,7 +18,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14"
|
||||
android:valueTo="M 20 8 L 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 M 14 16 C 14 15.43 14 14.859 14 14.289 L 14 14 C 13.869 14 13.739 14 13.608 14 C 12.405 14 11.203 14 10 14 C 10 14.509 10 15.017 10 15.526 C 10 15.684 10 15.842 10 16 L 10.33 16 C 10.392 16 10.454 16 10.515 16 C 11.677 16 12.838 16 14 16 C 14 16 14 16 14 16 M 14 10 L 14 12 L 14 12 L 10 12 L 10 10 L 14 10 M 12 15 L 12 15 L 12 15 L 12 15 L 12 15 L 12 15"
|
||||
@@ -17,7 +17,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 12 2 C 9.217 2 6.689 3.152 4.872 5.004 C 3.098 6.811 2 9.283 2 12 C 2 14.744 3.12 17.24 4.927 19.052 C 6.74 20.87 9.244 22 12 22 C 13.911 22 15.701 21.457 17.224 20.517 C 18.628 19.651 19.804 18.448 20.638 17.024 C 21.503 15.545 22 13.828 22 12 C 22 10.2 21.518 8.507 20.677 7.044 C 19.755 5.441 18.402 4.114 16.779 3.224 C 15.357 2.444 13.728 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 6 13 L 10 17 L 18 9 L 16.59 7.58 L 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13"
|
||||
android:valueTo="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 7 13 L 7 13 L 17 13 L 17 11 L 17 11 L 7 11 L 7 11 L 7 11"
|
||||
@@ -17,7 +17,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 7 13 L 7 13 L 17 13 L 17 11 L 17 11 L 7 11 L 7 11 L 7 11"
|
||||
android:valueTo="M 12 2 C 9.217 2 6.689 3.152 4.872 5.004 C 3.098 6.811 2 9.283 2 12 C 2 14.856 3.213 17.442 5.149 19.268 C 6.942 20.96 9.356 22 12 22 C 14.061 22 15.982 21.368 17.578 20.288 C 19.114 19.249 20.349 17.796 21.119 16.092 C 21.685 14.841 22 13.456 22 12 C 22 10.122 21.475 8.361 20.566 6.856 C 19.691 5.408 18.46 4.197 16.997 3.347 C 15.524 2.491 13.817 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 6 13 L 10 17 L 18 9 L 16.59 7.58 L 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13"
|
||||
@@ -19,7 +19,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4"
|
||||
android:valueTo="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
|
||||
@@ -19,7 +19,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
|
||||
android:valueTo="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4"
|
||||
@@ -18,7 +18,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12"
|
||||
android:valueTo="M 22 13.5 C 22 14.087 21.856 14.64 21.6 15.126 C 21.344 15.612 20.978 16.03 20.533 16.347 C 20.089 16.664 19.567 16.88 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.12 4.433 7.336 3.911 7.653 3.467 C 7.97 3.022 8.388 2.656 8.874 2.4 C 9.36 2.144 9.913 2 10.5 2 C 11.087 2 11.64 2.144 12.126 2.4 C 12.612 2.656 13.03 3.022 13.347 3.467 C 13.664 3.911 13.88 4.433 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 19.425 10.1 19.825 10.236 20.186 10.434 C 20.547 10.633 20.869 10.893 21.137 11.2 C 21.406 11.508 21.622 11.863 21.77 12.251 C 21.919 12.639 22 13.06 22 13.5 M 17 12 L 18.5 12 C 18.898 12 19.279 12.158 19.561 12.439 C 19.842 12.721 20 13.102 20 13.5 C 20 13.898 19.842 14.279 19.561 14.561 C 19.279 14.842 18.898 15 18.5 15 L 17 15 L 17 15 L 17 20 L 14.88 20 C 14.2 18.25 12.5 17 10.5 17 C 8.5 17 6.8 18.25 6.12 20 L 4 20 L 4 17.88 C 5.75 17.2 7 15.5 7 13.5 C 7 11.5 5.76 9.8 4 9.12 L 4 7 L 9 7 L 9 5.5 C 9 5.102 9.158 4.721 9.439 4.439 C 9.721 4.158 10.102 4 10.5 4 C 10.898 4 11.279 4.158 11.561 4.439 C 11.842 4.721 12 5.102 12 5.5 L 12 7 L 17 7 L 17 12"
|
||||
@@ -18,7 +18,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 22 13.5 C 22 14.087 21.856 14.64 21.6 15.126 C 21.344 15.612 20.978 16.03 20.533 16.347 C 20.089 16.664 19.567 16.88 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.12 4.433 7.336 3.911 7.653 3.467 C 7.97 3.022 8.388 2.656 8.874 2.4 C 9.36 2.144 9.913 2 10.5 2 C 11.087 2 11.64 2.144 12.126 2.4 C 12.612 2.656 13.03 3.022 13.347 3.467 C 13.664 3.911 13.88 4.433 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 19.425 10.1 19.825 10.236 20.186 10.434 C 20.547 10.633 20.869 10.893 21.137 11.2 C 21.406 11.508 21.622 11.863 21.77 12.251 C 21.919 12.639 22 13.06 22 13.5 M 17 12 L 18.5 12 C 18.898 12 19.279 12.158 19.561 12.439 C 19.842 12.721 20 13.102 20 13.5 C 20 13.898 19.842 14.279 19.561 14.561 C 19.279 14.842 18.898 15 18.5 15 L 17 15 L 17 15 L 17 20 L 14.88 20 C 14.2 18.25 12.5 17 10.5 17 C 8.5 17 6.8 18.25 6.12 20 L 4 20 L 4 17.88 C 5.75 17.2 7 15.5 7 13.5 C 7 11.5 5.76 9.8 4 9.12 L 4 7 L 9 7 L 9 5.5 C 9 5.102 9.158 4.721 9.439 4.439 C 9.721 4.158 10.102 4 10.5 4 C 10.898 4 11.279 4.158 11.561 4.439 C 11.842 4.721 12 5.102 12 5.5 L 12 7 L 17 7 L 17 12"
|
||||
android:valueTo="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12"
|
||||
@@ -18,7 +18,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 L 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 M 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 M 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12"
|
||||
android:valueTo="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4"
|
||||
@@ -18,7 +18,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4"
|
||||
android:valueTo="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 L 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 M 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 M 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12"
|
||||
@@ -18,7 +18,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 12 1 L 12 1 L 21 5 L 21 11 M 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18"
|
||||
android:valueTo="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21"
|
||||
@@ -18,7 +18,7 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@interpolator/fast_out_slow_in"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21"
|
||||
android:valueTo="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 12 1 L 12 1 L 21 5 L 21 11 M 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user