Compare commits
561 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c1cf5b9eb | ||
|
|
ac6f7fe19a | ||
|
|
d1a2a1b745 | ||
|
|
6ce881126a | ||
|
|
71854a9132 | ||
|
|
7c52b36aaa | ||
|
|
c8fbb363ea | ||
|
|
acdcd603c5 | ||
|
|
d8e336edf2 | ||
|
|
22f12b7266 | ||
|
|
cc2330c924 | ||
|
|
98d8b056df | ||
|
|
83261bb3f9 | ||
|
|
406e4004f2 | ||
|
|
5cd01b178a | ||
|
|
fb9ffa0b82 | ||
|
|
5e7a01fb60 | ||
|
|
10d14f8ee0 | ||
|
|
232f318bde | ||
|
|
bd042125c5 | ||
|
|
f16f13b5f2 | ||
|
|
cd744af161 | ||
|
|
6969e1b657 | ||
|
|
2c106d5a7e | ||
|
|
fd7449e939 | ||
|
|
f99c28876b | ||
|
|
8975459dad | ||
|
|
9ec91cacf5 | ||
|
|
4496e61cbe | ||
|
|
b93319ba39 | ||
|
|
6f78f4aa18 | ||
|
|
15de77afe6 | ||
|
|
ed2f1d70f9 | ||
|
|
bb3404a0b0 | ||
|
|
e4042d5d9b | ||
|
|
06677c446e | ||
|
|
6cf12d4c24 | ||
|
|
cd9e586023 | ||
|
|
f846763814 | ||
|
|
b6393f21d4 | ||
|
|
f2df41f0a6 | ||
|
|
bfff0fe997 | ||
|
|
4f8f6b2a8c | ||
|
|
b0a9076fcb | ||
|
|
f6f410fda8 | ||
|
|
fda769cc69 | ||
|
|
7a04dd0e27 | ||
|
|
f6a4e8987d | ||
|
|
734d0331bc | ||
|
|
7d9cededeb | ||
|
|
3749da0fbf | ||
|
|
61093eb8d6 | ||
|
|
b2ba3cd20b | ||
|
|
d56d56e2df | ||
|
|
99cbc449c2 | ||
|
|
4ece5c4315 | ||
|
|
a597a4611d | ||
|
|
253ae118df | ||
|
|
11fe93b8c6 | ||
|
|
38d0d4397e | ||
|
|
131097a236 | ||
|
|
88c33b1763 | ||
|
|
d4ba9fd808 | ||
|
|
290d63943f | ||
|
|
a59e87143c | ||
|
|
7178d0f3ec | ||
|
|
618533887d | ||
|
|
adbc68aa62 | ||
|
|
4bf5016921 | ||
|
|
6a9a469073 | ||
|
|
ac6cfa02c8 | ||
|
|
2e852e54f8 | ||
|
|
309cc6af12 | ||
|
|
39987022c6 | ||
|
|
6f521d5ac1 | ||
|
|
efb2df507d | ||
|
|
54077cb29f | ||
|
|
19cc82e0db | ||
|
|
4e45661e26 | ||
|
|
5816076bf6 | ||
|
|
59f2a2019e | ||
|
|
effbf8f244 | ||
|
|
b240eb7e25 | ||
|
|
f444075342 | ||
|
|
a6ceb88956 | ||
|
|
9db73ea07b | ||
|
|
356a845b69 | ||
|
|
d344cc8e77 | ||
|
|
d8e46ae27d | ||
|
|
4f22a46f93 | ||
|
|
a012d81cd1 | ||
|
|
57f043408f | ||
|
|
e5d8c9f355 | ||
|
|
2760de9d40 | ||
|
|
64a69e6627 | ||
|
|
9fd394d7c5 | ||
|
|
e8e00108e6 | ||
|
|
3100e1700c | ||
|
|
7598481c01 | ||
|
|
565d1d7a81 | ||
|
|
e15477cc04 | ||
|
|
94c46a04c4 | ||
|
|
c513a198f4 | ||
|
|
754f2801ee | ||
|
|
4b96ce2556 | ||
|
|
15f283c1c7 | ||
|
|
6c39a894ba | ||
|
|
af2a309c7a | ||
|
|
94489d1dca | ||
|
|
88f92dd98f | ||
|
|
deeebdb271 | ||
|
|
f75ee05b52 | ||
|
|
c39838e631 | ||
|
|
b7e4ff551a | ||
|
|
7b828ee0d9 | ||
|
|
266c9da66d | ||
|
|
f0f82fb9f9 | ||
|
|
54cfb54b8b | ||
|
|
1430b29340 | ||
|
|
d79fbf2519 | ||
|
|
aea7edd2f8 | ||
|
|
8f8c7724ba | ||
|
|
2ac1ade188 | ||
|
|
6122aeffb5 | ||
|
|
97f52bbcb6 | ||
|
|
e39bfe8832 | ||
|
|
36bc78070a | ||
|
|
e164e219bd | ||
|
|
309726dddd | ||
|
|
786ac02a1d | ||
|
|
e966e52b77 | ||
|
|
0e1f3f5823 | ||
|
|
649201f52d | ||
|
|
bbe9dc42d2 | ||
|
|
3ce66040b3 | ||
|
|
43337eacd2 | ||
|
|
a197f8fce1 | ||
|
|
8f1c235af5 | ||
|
|
c6810bc441 | ||
|
|
fb1c6a2cc3 | ||
|
|
3cc77c7c95 | ||
|
|
bb2c5283f8 | ||
|
|
a04c1883fa | ||
|
|
e834d67922 | ||
|
|
cfe71bbe60 | ||
|
|
262a6b563c | ||
|
|
a1d838a98a | ||
|
|
b1459134e8 | ||
|
|
9edf35c4bc | ||
|
|
b661d94278 | ||
|
|
c82acb38d5 | ||
|
|
7e24d7fea3 | ||
|
|
158e47b372 | ||
|
|
4df39342a2 | ||
|
|
0dc219193b | ||
|
|
8c587fd22a | ||
|
|
c9fa690601 | ||
|
|
e548b933e2 | ||
|
|
90f3a09805 | ||
|
|
d62430af1b | ||
|
|
bacc75393b | ||
|
|
fec7df02e4 | ||
|
|
0a1406eb86 | ||
|
|
26c2ecef6d | ||
|
|
15ef59a856 | ||
|
|
a9b10acac2 | ||
|
|
08f87e27ed | ||
|
|
2e0d02ac1d | ||
|
|
904353a1f3 | ||
|
|
143c84d47b | ||
|
|
65bfd8a132 | ||
|
|
e7e870ac0f | ||
|
|
f64c4b5bc9 | ||
|
|
8d88011c08 | ||
|
|
bc8c24af3b | ||
|
|
fa627f0da8 | ||
|
|
e701551655 | ||
|
|
7242f2719c | ||
|
|
0126d3b09d | ||
|
|
e5bbbe28c5 | ||
|
|
5e397c2aed | ||
|
|
5e8d6df9f0 | ||
|
|
ef88732a9a | ||
|
|
2419d14505 | ||
|
|
937d9c5d3b | ||
|
|
1f6dbe4278 | ||
|
|
e2b1b10d33 | ||
|
|
f0d388d571 | ||
|
|
667d6b6109 | ||
|
|
58d16cb3de | ||
|
|
78ffee360c | ||
|
|
53bc1da503 | ||
|
|
436f8600d2 | ||
|
|
669e644d81 | ||
|
|
346096aab7 | ||
|
|
9506f50850 | ||
|
|
d7328953ba | ||
|
|
2a2686fa11 | ||
|
|
1377d42dc2 | ||
|
|
c7c13b3a89 | ||
|
|
cd3aad232b | ||
|
|
c18fe9ab77 | ||
|
|
2832c9ee4c | ||
|
|
fa360d83f8 | ||
|
|
916d6a7777 | ||
|
|
dccab90843 | ||
|
|
c64e1a005e | ||
|
|
4dd7aec7ec | ||
|
|
8fa4e33bda | ||
|
|
1b5ce606e4 | ||
|
|
60974e8672 | ||
|
|
2205a90de9 | ||
|
|
c7666a3e01 | ||
|
|
a08a0c1dd1 | ||
|
|
dc80d751cb | ||
|
|
80037c2d23 | ||
|
|
cbbcc2a6fe | ||
|
|
f8a6a6afe2 | ||
|
|
a58af39fae | ||
|
|
e47dca02cf | ||
|
|
7d9886773a | ||
|
|
6caa238298 | ||
|
|
546378b9c4 | ||
|
|
ad471f3837 | ||
|
|
00b2c816a4 | ||
|
|
e1930528b9 | ||
|
|
27c98e31f4 | ||
|
|
021939264f | ||
|
|
ccfcb71565 | ||
|
|
74c36ac735 | ||
|
|
b6f4f7ac62 | ||
|
|
bd82ba7773 | ||
|
|
acc573f1a8 | ||
|
|
9773ea73d4 | ||
|
|
b091a87a37 | ||
|
|
ecf03507e6 | ||
|
|
4051033d7d | ||
|
|
64fcf59a5f | ||
|
|
2aae2257b3 | ||
|
|
e0559f8f70 | ||
|
|
a931a882b1 | ||
|
|
d0e0a04981 | ||
|
|
ec78180820 | ||
|
|
c78945aa3a | ||
|
|
a7f3663d01 | ||
|
|
f89fad5410 | ||
|
|
1b8799b1a3 | ||
|
|
a097278076 | ||
|
|
1b44bf26b9 | ||
|
|
fbf1aee8a7 | ||
|
|
e04d532c2a | ||
|
|
15e0d87c6d | ||
|
|
191c8bc2dc | ||
|
|
79366ba9c1 | ||
|
|
9a895feb6c | ||
|
|
115abf8ea7 | ||
|
|
184a19dbb4 | ||
|
|
f2de7bb124 | ||
|
|
2d1011694c | ||
|
|
c3da619dcf | ||
|
|
14f7fefd8a | ||
|
|
927727dee1 | ||
|
|
d4c078bab0 | ||
|
|
e5563b392e | ||
|
|
2131a85699 | ||
|
|
d6c3ca83e6 | ||
|
|
7811edb88c | ||
|
|
e30a3b415e | ||
|
|
81e6e2578f | ||
|
|
57504a7e8e | ||
|
|
14a961f521 | ||
|
|
a21dc96f01 | ||
|
|
2250714569 | ||
|
|
cab2840b66 | ||
|
|
bb14957982 | ||
|
|
fa530a47d6 | ||
|
|
9dc7e5f229 | ||
|
|
fd433244c7 | ||
|
|
6d5dd87551 | ||
|
|
d6a33bc81b | ||
|
|
a4d7747078 | ||
|
|
008e99417e | ||
|
|
26a47020fe | ||
|
|
b328766b80 | ||
|
|
30ce16d086 | ||
|
|
73ebba3329 | ||
|
|
1462d70ed2 | ||
|
|
38ccdfe869 | ||
|
|
07e7f0c139 | ||
|
|
ec489ab491 | ||
|
|
ed02d0d13d | ||
|
|
31999bf09c | ||
|
|
e93267cf62 | ||
|
|
da805707e4 | ||
|
|
9038de1e62 | ||
|
|
2229b04f27 | ||
|
|
68a575367a | ||
|
|
4a71c6ad11 | ||
|
|
7b144a84e5 | ||
|
|
86e94da6a6 | ||
|
|
1d86190f8f | ||
|
|
5fd4713a1e | ||
|
|
b173a0809c | ||
|
|
ac95f41c81 | ||
|
|
b83ba170f4 | ||
|
|
c9731ae37a | ||
|
|
f112fa1b74 | ||
|
|
9aa1e56d89 | ||
|
|
06dfa140d8 | ||
|
|
01474ca37e | ||
|
|
79f335cc5b | ||
|
|
723bfb5653 | ||
|
|
ee5d50dfe0 | ||
|
|
38d7ed94a6 | ||
|
|
0083fe0db8 | ||
|
|
9587632c49 | ||
|
|
4467ef3351 | ||
|
|
95f2bd3002 | ||
|
|
a0021a0f73 | ||
|
|
2fd3bb4a36 | ||
|
|
de6311c698 | ||
|
|
f675392143 | ||
|
|
8794a53fc8 | ||
|
|
51e5142866 | ||
|
|
b93d95cb1a | ||
|
|
1eb561c784 | ||
|
|
e8b9603bdd | ||
|
|
2ab1571e3a | ||
|
|
c6084c67a5 | ||
|
|
b6ec92e642 | ||
|
|
05e4237e71 | ||
|
|
e06c901bf5 | ||
|
|
d2f5503330 | ||
|
|
38d79e57ca | ||
|
|
f6735f5863 | ||
|
|
cf80984e52 | ||
|
|
3d69aeaca1 | ||
|
|
ee0b2f9361 | ||
|
|
fff0cc277d | ||
|
|
d9c8a39948 | ||
|
|
2803a540e3 | ||
|
|
9d98b90cae | ||
|
|
e607a75882 | ||
|
|
ae9ef717a3 | ||
|
|
e40ca8f007 | ||
|
|
353a551870 | ||
|
|
d083aea38e | ||
|
|
fd0faa3a97 | ||
|
|
f442dd7082 | ||
|
|
244d788146 | ||
|
|
306a3abd6c | ||
|
|
e1933944a3 | ||
|
|
921e420add | ||
|
|
09e55cf2d2 | ||
|
|
6f3d2a213c | ||
|
|
5bc2162df0 | ||
|
|
8d69bad65b | ||
|
|
cfc35e1028 | ||
|
|
6d30c58054 | ||
|
|
5255a0de81 | ||
|
|
6626e1649b | ||
|
|
af37c0625d | ||
|
|
44ab789450 | ||
|
|
f5a93057ad | ||
|
|
97d8907a76 | ||
|
|
fd410ab38e | ||
|
|
44199eb5ba | ||
|
|
415fcadb8e | ||
|
|
f856c3f85f | ||
|
|
03bc3c0876 | ||
|
|
d753ca7225 | ||
|
|
a0cdb8acbe | ||
|
|
8695ef75de | ||
|
|
53f3fad499 | ||
|
|
fcbeec95e4 | ||
|
|
7048c10f1d | ||
|
|
a4091cc776 | ||
|
|
6fa31f96b9 | ||
|
|
bef2d5ae77 | ||
|
|
d10b80f39d | ||
|
|
11c9d7fd2d | ||
|
|
968e25bc6b | ||
|
|
8b2ef8aaac | ||
|
|
46519a6cf8 | ||
|
|
db9406655f | ||
|
|
814097ab19 | ||
|
|
34f1e6f984 | ||
|
|
41127e9312 | ||
|
|
1de0c80e40 | ||
|
|
e84d3ce7cc | ||
|
|
a5942862ff | ||
|
|
a405372f6a | ||
|
|
b472580ca6 | ||
|
|
5a587167ae | ||
|
|
5149caf2b6 | ||
|
|
ffa559a571 | ||
|
|
5c3ab5470a | ||
|
|
e6859ff56d | ||
|
|
f16b0048f3 | ||
|
|
17b3a0d0ed | ||
|
|
1e9f38f2bb | ||
|
|
ef8f9e5738 | ||
|
|
bf59ead3b2 | ||
|
|
5dbd94e42c | ||
|
|
b92fc860eb | ||
|
|
20bdf171ee | ||
|
|
5a6ca2ce12 | ||
|
|
636941da70 | ||
|
|
5bc3975591 | ||
|
|
5e54e4561c | ||
|
|
a7b560b576 | ||
|
|
5f9d4c1572 | ||
|
|
a83ecb8b22 | ||
|
|
1cc88cc04f | ||
|
|
92bce6c2c6 | ||
|
|
ab96790c73 | ||
|
|
d5618321e9 | ||
|
|
4b8fc6ae9a | ||
|
|
152e24748b | ||
|
|
2efdf21130 | ||
|
|
e99c2b64b0 | ||
|
|
3915d9549e | ||
|
|
23077537f9 | ||
|
|
de0cbd6b29 | ||
|
|
d0efa0690a | ||
|
|
5b6d04d558 | ||
|
|
791794f59d | ||
|
|
6a6465ae5b | ||
|
|
056a78c7c4 | ||
|
|
11b0025faf | ||
|
|
3009b29e0c | ||
|
|
5d52d970a3 | ||
|
|
aef0b84275 | ||
|
|
12c704d89c | ||
|
|
38e070a565 | ||
|
|
c7d33c684b | ||
|
|
b2661d31fd | ||
|
|
53aa718674 | ||
|
|
c173114368 | ||
|
|
7006cb516c | ||
|
|
7809c30052 | ||
|
|
7e46d75159 | ||
|
|
87f96b9ec7 | ||
|
|
3920f338f6 | ||
|
|
2565231867 | ||
|
|
b7af3f7341 | ||
|
|
d93a7af534 | ||
|
|
1dcc8307ee | ||
|
|
9936a9469d | ||
|
|
0e245b8936 | ||
|
|
226c11dda3 | ||
|
|
8330054a81 | ||
|
|
c7cb1dab38 | ||
|
|
a3686c16d4 | ||
|
|
b2a6a6a1ac | ||
|
|
e3e9a8ccc3 | ||
|
|
7058c7a4cd | ||
|
|
2591de1c1d | ||
|
|
d20f19c08f | ||
|
|
a5ba4b4d23 | ||
|
|
094815a098 | ||
|
|
5224fa656c | ||
|
|
73550164a8 | ||
|
|
48a9ab313e | ||
|
|
25936c8361 | ||
|
|
3397cdd95f | ||
|
|
69f8ff2688 | ||
|
|
41ab062103 | ||
|
|
c78431a0fb | ||
|
|
3b33216961 | ||
|
|
6373636d57 | ||
|
|
04a1f9987e | ||
|
|
90d286cd5a | ||
|
|
1826d108a5 | ||
|
|
a5619bb0ee | ||
|
|
5c1fadaba2 | ||
|
|
7928f4321a | ||
|
|
3b62367003 | ||
|
|
c8994d8488 | ||
|
|
4033994111 | ||
|
|
2050fb56d0 | ||
|
|
eeea9d0fce | ||
|
|
4b1f7a76c7 | ||
|
|
db2a7feb34 | ||
|
|
8b91b2eeb1 | ||
|
|
899803739b | ||
|
|
1ea3a5b682 | ||
|
|
f21cf86cc4 | ||
|
|
bb1052bbc7 | ||
|
|
7dad08346c | ||
|
|
b7c56e58ae | ||
|
|
a6f6da5bc5 | ||
|
|
f271ba3d97 | ||
|
|
66d02c7e2d | ||
|
|
0406573317 | ||
|
|
4a8b6b2704 | ||
|
|
603cec113c | ||
|
|
a579c7bb86 | ||
|
|
654c488ed8 | ||
|
|
ecfb339680 | ||
|
|
ecbe222a50 | ||
|
|
aacf62c259 | ||
|
|
3973fb2706 | ||
|
|
07ff81f4db | ||
|
|
c0e1e45b72 | ||
|
|
b07f70ab0b | ||
|
|
e1a35f8b2d | ||
|
|
e364af393a | ||
|
|
11c4e19e0c | ||
|
|
fa23be76ed | ||
|
|
8e50d35eba | ||
|
|
1112534be6 | ||
|
|
784f249d54 | ||
|
|
d6ad21b402 | ||
|
|
bd03cac80d | ||
|
|
da3ee71dfd | ||
|
|
62fb089e27 | ||
|
|
51dab21f6f | ||
|
|
9e89b3a201 | ||
|
|
1c1e2b9f3d | ||
|
|
e2e0238dcd | ||
|
|
9745e11da4 | ||
|
|
8ae7ccbfc9 | ||
|
|
5a2229596a | ||
|
|
a463d94480 | ||
|
|
750abc8c71 | ||
|
|
8375fbd8b3 | ||
|
|
394e406a36 | ||
|
|
2645af0d4c | ||
|
|
48222c22c8 | ||
|
|
d6a71267c6 | ||
|
|
0f4a46188c | ||
|
|
5917b90837 | ||
|
|
a222e90d1f | ||
|
|
3f60d713f8 | ||
|
|
9cb8433f3b | ||
|
|
428ca53532 | ||
|
|
6c4d7e537b | ||
|
|
58ab89736a | ||
|
|
5e34221a09 | ||
|
|
ac35f2a5f4 | ||
|
|
73992dca54 | ||
|
|
53dc20109d | ||
|
|
e7fef0a767 | ||
|
|
c73beabf7e | ||
|
|
c7409d7ac6 | ||
|
|
5e238ab5d3 | ||
|
|
a8f502ef4f | ||
|
|
c48ce82640 | ||
|
|
5594485bec | ||
|
|
f967bd299a | ||
|
|
a75c8f5dfc | ||
|
|
271d56c01c | ||
|
|
12220789a3 | ||
|
|
ca85a688cc | ||
|
|
0e3ed548c9 | ||
|
|
27dc5395e0 | ||
|
|
4f05ba1ac6 | ||
|
|
1a36218c80 | ||
|
|
3125b90efe | ||
|
|
2a7425700a |
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.bat text eol=crlf
|
||||
*.jar binary
|
||||
57
.github/ISSUE_TEMPLATE/01-bug-report-en.md
vendored
@@ -1,57 +0,0 @@
|
||||
---
|
||||
name: "[English] Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Be sure to put a clear title after [BUG] in the text box above -->
|
||||
<!-- Be sure to put a clear title after [BUG] in the text box above -->
|
||||
<!-- Be sure to put a clear title after [BUG] in the text box above -->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Device Info (please complete the following information):**
|
||||
|
||||
- Device: [e.g. Pixel 4]
|
||||
- ROM: [e.g: AOSP]
|
||||
- ROM Version:
|
||||
- Android Version [e.g. 10]
|
||||
|
||||
**Application Info (please complete the following information):**
|
||||
|
||||
- Version: [e.g. 1.1.10]
|
||||
- Apk File Name: [e.g. app-release-arm64-v8a.apk]
|
||||
- Distribution Channel: [e.g. Google Play]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Configure**
|
||||
Paste configure file which **removed server info**
|
||||
```yaml
|
||||
# paste here
|
||||
```
|
||||
|
||||
**Logs**
|
||||
Paste logs to help detect problem
|
||||
|
||||
```
|
||||
<paste here>
|
||||
```
|
||||
107
.github/ISSUE_TEMPLATE/01-bug-report-en.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: "[English] Bug Report"
|
||||
description: "Create a report to help us debug bugs"
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
NOTE: Be sure to put a clear and concise title **AFTER** `[BUG]` in the text box above.
|
||||
|
||||
NOTE: We do not provide any services such as proxies, DO NOT feedback any problems not caused by this application here.
|
||||
|
||||
<!-- template -->
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: "Describe the bug"
|
||||
description: "A clear and concise description of what the bug is."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: "To Reproduce"
|
||||
description: "Steps to reproduce the behavior:"
|
||||
value: |
|
||||
Step 1: ...
|
||||
Step 2: ...
|
||||
Step 3: ...
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: device-info
|
||||
attributes:
|
||||
label: "Device Info"
|
||||
description: |
|
||||
Input your device information.
|
||||
|
||||
Example:
|
||||
- Device: Pixel 4
|
||||
- ROM: AOSP
|
||||
- Android Version: 10
|
||||
value: |
|
||||
- Device:
|
||||
- ROM:
|
||||
- Android Version:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: app-info
|
||||
attributes:
|
||||
label: "Application Info"
|
||||
description: |
|
||||
Input application you are using information.
|
||||
|
||||
Example:
|
||||
```
|
||||
- Version: 2.5.4-premium
|
||||
- APK filename: cfa-2.5.4-premium-arm64-v8a-release.apk
|
||||
- Distribution Channel: Google Play
|
||||
```
|
||||
value: |
|
||||
- Version:
|
||||
- APK filename:
|
||||
- Distribution Channel:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: configure
|
||||
attributes:
|
||||
render: yml
|
||||
label: "Configure File"
|
||||
description: |
|
||||
Please paste or upload the configuration file here.
|
||||
|
||||
TIPS: If you only have a subscription link, please use your browser to download it.
|
||||
|
||||
**NOTE: Please remove proxies from the configuration file before uploading it.**
|
||||
**NOTE: Please remove proxies from the configuration file before uploading it.**
|
||||
**NOTE: Please remove proxies from the configuration file before uploading it.**
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
render: raw
|
||||
label: "Logs"
|
||||
description: |
|
||||
Please paste or upload the log file here.
|
||||
|
||||
TIPS: Please use the `Logcat` in application or `adb logcat`. `adb logcat` would be better.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshot
|
||||
attributes:
|
||||
label: "Screenshot"
|
||||
description: "If applicable, add screenshots to help explain your problem."
|
||||
placeholder: "Optional"
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: "Additional"
|
||||
description: "Add any other context about the problem here."
|
||||
24
.github/ISSUE_TEMPLATE/02-feature-request-en.md
vendored
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: "[English] Feature request"
|
||||
about: Suggest an idea for this app
|
||||
title: "[Feature Request] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
|
||||
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
|
||||
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
27
.github/ISSUE_TEMPLATE/02-feature-request-en.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "[English] Feature Request"
|
||||
description: "Create a report to help us improve"
|
||||
title: "[Feature Request] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request!
|
||||
|
||||
NOTE: Be sure to put a clear and concise title **AFTER** `[Feature Request]` in the text box above.
|
||||
|
||||
<!-- template -->
|
||||
- type: textarea
|
||||
id: "description"
|
||||
attributes:
|
||||
label: "Feature Description"
|
||||
description: |
|
||||
A clear and concise description of the feature.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: "additional"
|
||||
attributes:
|
||||
label: "Additional"
|
||||
description: |
|
||||
Add any other context or screenshots about the feature request here.
|
||||
55
.github/ISSUE_TEMPLATE/03-bug-report-zh-cn.md
vendored
@@ -1,55 +0,0 @@
|
||||
---
|
||||
name: "[简体中文] 创建错误报告"
|
||||
about: 创建错误报告以帮助我们改进应用
|
||||
title: "[BUG] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
|
||||
|
||||
**描述出现的错误**
|
||||
请简洁的描述你遇到的错误
|
||||
|
||||
**如何复现该错误**
|
||||
复现步骤:
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
4. ...
|
||||
|
||||
**预期行为**
|
||||
清晰简单的描述你预期的应用应该表现的行为
|
||||
|
||||
**屏幕截图**
|
||||
如果适用, 上传屏幕截图以帮助描述错误
|
||||
|
||||
**设备信息 (请完成以下信息):**
|
||||
- 机型: [例如: Pixel 4]
|
||||
- 系统/ROM: [例如: MIUI 11]
|
||||
- Android 版本 [例如: 10]
|
||||
- ROM版本 [例如: 20.3.19]
|
||||
|
||||
**应用信息**
|
||||
- 版本: [例如: 1.1.10]
|
||||
- 安装包文件名: [例如: app-release-arm64-v8a.apk]
|
||||
- 应用来源: [例如: Google Play]
|
||||
|
||||
**附加信息**
|
||||
其他的可能与改错误相关的信息
|
||||
|
||||
**配置文件**
|
||||
在此粘贴 **去除服务器信息的** 的 **配置文件**
|
||||
```yaml
|
||||
# 在此粘贴
|
||||
```
|
||||
|
||||
**日志**
|
||||
粘贴日志以帮助侦测错误
|
||||
```
|
||||
<在此粘贴>
|
||||
```
|
||||
|
||||
108
.github/ISSUE_TEMPLATE/03-bug-report-zh-cn.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: "[简体中文] 错误报告"
|
||||
description: "创建错误报告以帮助我们修正应用"
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您在百忙之中填写此错误报告。
|
||||
|
||||
注意: 请务必在上方文本框的 `[BUG]` **之后**填写清晰明了的标题。
|
||||
|
||||
注意:这里不提供像是代理服务器之类的服务,请不要反馈非应用自身引起的问题。
|
||||
|
||||
<!-- template -->
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: "描述此错误"
|
||||
description: "请清晰简洁的描述你遇到的错误。"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: "如何复现该错误"
|
||||
description: "复现步骤:"
|
||||
value: |
|
||||
步骤 1: ...
|
||||
步骤 2: ...
|
||||
步骤 3: ...
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: device-info
|
||||
attributes:
|
||||
label: "设备信息"
|
||||
description: |
|
||||
输入您正在使用的设备信息。
|
||||
|
||||
例子:
|
||||
- 机型: Pixel 4
|
||||
- 系统类型: MIUI/AOSP
|
||||
- Android 版本: 10
|
||||
value: |
|
||||
- 机型:
|
||||
- 系统类型:
|
||||
- Android 版本:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: app-info
|
||||
attributes:
|
||||
label: "应用信息"
|
||||
description: |
|
||||
输入您正在使用的应用信息。
|
||||
|
||||
例子:
|
||||
```
|
||||
- 版本: 2.5.4-premium
|
||||
- 安装包文件名: cfa-2.5.4-premium-arm64-v8a-release.apk
|
||||
- 应用来源: Google Play
|
||||
```
|
||||
value: |
|
||||
- 版本:
|
||||
- 安装包文件名:
|
||||
- 应用来源:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: configure
|
||||
attributes:
|
||||
render: yml
|
||||
label: "配置文件"
|
||||
description: |
|
||||
请在此粘贴和上传配置文件。
|
||||
|
||||
提示:如果您仅有一个订阅链接,请使用浏览器打开此链接以下载配置文件。
|
||||
|
||||
**注意: 请在上传配置文件前,移除其中的代理服务器信息。**
|
||||
**注意: 请在上传配置文件前,移除其中的代理服务器信息。**
|
||||
**注意: 请在上传配置文件前,移除其中的代理服务器信息。**
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
render: raw
|
||||
label: "日志"
|
||||
description: |
|
||||
请在此粘贴或上传日志。
|
||||
|
||||
提示: 请使用应用内的 `Logcat` 或 `adb logcat` 捕获日志. `adb logcat` 能更好地帮助侦测问题.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshot
|
||||
attributes:
|
||||
label: "屏幕截图"
|
||||
description: "如果适用,请在此粘贴或上传屏幕截图。"
|
||||
placeholder: "可选"
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: "附加信息"
|
||||
description: "其他的可能与改错误相关的信息。"
|
||||
placeholder: "可选"
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: "[简体中文] 功能请求"
|
||||
about: 你希望的能够在应用中增加的功能
|
||||
title: "[Feature Request] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
|
||||
**功能描述**
|
||||
请清晰的描述你想要的功能
|
||||
|
||||
**描述你希望的实现方式**
|
||||
清晰的描述应用应该如何实现该功能
|
||||
|
||||
**附加信息**
|
||||
其他的与改功能相关的附加信息
|
||||
27
.github/ISSUE_TEMPLATE/04-feature-request-zh-cn.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "[简体中文] 功能请求"
|
||||
description: "您希望的能够在应用中增加功能"
|
||||
title: "[Feature Request] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您在百忙之中填写此功能请求报告。
|
||||
|
||||
注意: 请务必在上方文本框的 `[Feature Request]` **之后**填写清晰明了的标题。
|
||||
|
||||
<!-- template -->
|
||||
- type: textarea
|
||||
id: "description"
|
||||
attributes:
|
||||
label: "功能描述"
|
||||
description: |
|
||||
简介明了的描述此功能。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: "additional"
|
||||
attributes:
|
||||
label: "附加信息"
|
||||
description: |
|
||||
与此功能相关的其他附加信息。
|
||||
33
.github/patch/disable_pidfd_on_android.patch
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
From 7115c480196f4bdcbdae5e14ebaa4510540680e9 Mon Sep 17 00:00:00 2001
|
||||
From: Brad Fitzpatrick <bradfitz@tailscale.com>
|
||||
Date: Tue, 27 Jan 2026 09:52:22 -0800
|
||||
Subject: [PATCH] [tailscale] os: disable pidfd on Android
|
||||
|
||||
Updates tailscale/tailscale#13452
|
||||
Updates golang/go#70508
|
||||
Updates tailscale/go#99
|
||||
---
|
||||
src/os/pidfd_linux.go | 10 ++++++++++
|
||||
1 file changed, 10 insertions(+)
|
||||
|
||||
diff --git a/src/os/pidfd_linux.go b/src/os/pidfd_linux.go
|
||||
index 796d8c018c7f2a..5cdbf1175e0db5 100644
|
||||
--- a/src/os/pidfd_linux.go
|
||||
+++ b/src/os/pidfd_linux.go
|
||||
@@ -138,6 +138,16 @@ func (p *Process) pidfdSendSignal(s syscall.Signal) error {
|
||||
|
||||
// pidfdWorks returns whether we can use pidfd on this system.
|
||||
func pidfdWorks() bool {
|
||||
+ if runtime.GOOS == "android" {
|
||||
+ // Tailscale-specific workaround since https://github.com/golang/go/pull/69543/commits/aad6b3b32c81795f86bc4a9e81aad94899daf520
|
||||
+ // does not solve https://github.com/golang/go/issues/69065 for Android apps using Go libraries.
|
||||
+ //
|
||||
+ // See: https://github.com/tailscale/tailscale/issues/13452
|
||||
+ //
|
||||
+ // For now (2025-04-09), we'll just disable pidfd
|
||||
+ // on all Android releases.
|
||||
+ return false
|
||||
+ }
|
||||
return checkPidfdOnce() == nil
|
||||
}
|
||||
|
||||
56
.github/patch/remove_64bits_syscall_on_32bit_linux.patch
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
Subject: [PATCH] remove 64bits syscall on 32bit linux
|
||||
---
|
||||
Index: src/runtime/os_linux32.go
|
||||
IDEA additional info:
|
||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||
<+>UTF-8
|
||||
===================================================================
|
||||
diff --git a/src/runtime/os_linux32.go b/src/runtime/os_linux32.go
|
||||
--- a/src/runtime/os_linux32.go (revision 030384681641464bf71ed16500075c458363510f)
|
||||
+++ b/src/runtime/os_linux32.go (date 1771666707318)
|
||||
@@ -21,14 +21,14 @@
|
||||
|
||||
//go:nosplit
|
||||
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32 {
|
||||
- if !isFutexTime32bitOnly.Load() {
|
||||
- ret := futex_time64(addr, op, val, ts, addr2, val3)
|
||||
- // futex_time64 is only supported on Linux 5.0+
|
||||
- if ret != -_ENOSYS {
|
||||
- return ret
|
||||
- }
|
||||
- isFutexTime32bitOnly.Store(true)
|
||||
- }
|
||||
+ //if !isFutexTime32bitOnly.Load() {
|
||||
+ // ret := futex_time64(addr, op, val, ts, addr2, val3)
|
||||
+ // // futex_time64 is only supported on Linux 5.0+
|
||||
+ // if ret != -_ENOSYS {
|
||||
+ // return ret
|
||||
+ // }
|
||||
+ // isFutexTime32bitOnly.Store(true)
|
||||
+ //}
|
||||
// Downgrade ts.
|
||||
var ts32 timespec32
|
||||
var pts32 *timespec32
|
||||
@@ -49,14 +49,14 @@
|
||||
|
||||
//go:nosplit
|
||||
func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
|
||||
- if !isSetTime32bitOnly.Load() {
|
||||
- ret := timer_settime64(timerid, flags, new, old)
|
||||
- // timer_settime64 is only supported on Linux 5.0+
|
||||
- if ret != -_ENOSYS {
|
||||
- return ret
|
||||
- }
|
||||
- isSetTime32bitOnly.Store(true)
|
||||
- }
|
||||
+ //if !isSetTime32bitOnly.Load() {
|
||||
+ // ret := timer_settime64(timerid, flags, new, old)
|
||||
+ // // timer_settime64 is only supported on Linux 5.0+
|
||||
+ // if ret != -_ENOSYS {
|
||||
+ // return ret
|
||||
+ // }
|
||||
+ // isSetTime32bitOnly.Store(true)
|
||||
+ //}
|
||||
|
||||
var newts, oldts itimerspec32
|
||||
var new32, old32 *itimerspec32
|
||||
111
.github/workflows/build-debug.yaml
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
name: Build Debug
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
BuildDebug:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive --force
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: 21
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-download-base-url: 'https://github.com/MetaCubeX/go/releases/download/build'
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Apply Patches
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
for p in $GITHUB_WORKSPACE/.github/patch/*.patch; do patch --verbose -p 1 < "$p"; done
|
||||
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Update CA
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install ca-certificates
|
||||
sudo update-ca-certificates
|
||||
cp -f /etc/ssl/certs/ca-certificates.crt core/src/foss/golang/clash/component/ca/ca-certificates.crt
|
||||
|
||||
# - name: Signing properties
|
||||
# env:
|
||||
# SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
# SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
# SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
# run: |
|
||||
# touch signing.properties
|
||||
# echo keystore.password="$SIGNING_STORE_PASSWORD" > signing.properties
|
||||
# echo key.alias="$SIGNING_KEY_ALIAS" >> signing.properties
|
||||
# echo key.password="$SIGNING_KEY_PASSWORD" >> signing.properties
|
||||
|
||||
# echo "cat signing.properties"
|
||||
# cat signing.properties
|
||||
|
||||
- name: Build
|
||||
if: success()
|
||||
run: ./gradlew --no-daemon app:assembleAlphaRelease
|
||||
|
||||
- name: Upload Aritfact (universal)
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
name: CMFA Debug Unsigned APK (universal)
|
||||
path: |
|
||||
app/build/outputs/apk/alpha/release/*-universal-*.apk
|
||||
|
||||
- name: Upload Aritfact (arm64-v8a)
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
name: CMFA Debug Unsigned APK (arm64-v8a)
|
||||
path: |
|
||||
app/build/outputs/apk/alpha/release/*-arm64-v8a-*.apk
|
||||
|
||||
- name: Upload Aritfact (armeabi-v7a)
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
name: CMFA Debug Unsigned APK (armeabi-v7a)
|
||||
path: |
|
||||
app/build/outputs/apk/alpha/release/*-armeabi-v7a-*.apk
|
||||
|
||||
- name: Upload Aritfact (x86_64)
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
name: CMFA Debug Unsigned APK (x86_64)
|
||||
path: |
|
||||
app/build/outputs/apk/alpha/release/*-x86_64-*.apk
|
||||
|
||||
- name: Upload Aritfact (x86)
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
name: CMFA Debug Unsigned APK (x86)
|
||||
path: |
|
||||
app/build/outputs/apk/alpha/release/*-x86-*.apk
|
||||
95
.github/workflows/build-pre-release.yaml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
name: Build Pre-Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
BuildPreRelease:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive --force
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: 21
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-download-base-url: 'https://github.com/MetaCubeX/go/releases/download/build'
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Apply Patches
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
for p in $GITHUB_WORKSPACE/.github/patch/*.patch; do patch --verbose -p 1 < "$p"; done
|
||||
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Update CA
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install ca-certificates
|
||||
sudo update-ca-certificates
|
||||
cp -f /etc/ssl/certs/ca-certificates.crt core/src/foss/golang/clash/component/ca/ca-certificates.crt
|
||||
|
||||
- name: Signing properties
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
run: |
|
||||
touch signing.properties
|
||||
echo keystore.password="$SIGNING_STORE_PASSWORD" > signing.properties
|
||||
echo key.alias="$SIGNING_KEY_ALIAS" >> signing.properties
|
||||
echo key.password="$SIGNING_KEY_PASSWORD" >> signing.properties
|
||||
|
||||
echo "cat signing.properties"
|
||||
cat signing.properties
|
||||
|
||||
- name: Pre-release Build
|
||||
if: success()
|
||||
run: ./gradlew --no-daemon app:assembleAlphaRelease
|
||||
|
||||
# Delete old Prerelease-alpha
|
||||
- uses: dev-drprasad/delete-tag-and-release@v1.1
|
||||
with:
|
||||
tag_name: Prerelease-alpha
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
delete_release: true
|
||||
|
||||
- name: Tag Repo
|
||||
uses: richardsimko/update-tag@v1
|
||||
with:
|
||||
tag_name: Prerelease-alpha
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Alpha
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
tag_name: Prerelease-alpha
|
||||
files: app/build/outputs/apk/alpha/release/*
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Release Changelog Builder
|
||||
uses: mikepenz/release-changelog-builder-action@v4
|
||||
133
.github/workflows/build-release.yaml
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
name: Build Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-tag:
|
||||
description: 'Release Tag (v2.x.x)'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
BuildRelease:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive --force
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: 21
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-download-base-url: 'https://github.com/MetaCubeX/go/releases/download/build'
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Apply Patches
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
for p in $GITHUB_WORKSPACE/.github/patch/*.patch; do patch --verbose -p 1 < "$p"; done
|
||||
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Convert and set version env
|
||||
id: process-version
|
||||
run: |
|
||||
VERSION_TAG=${{ inputs.release-tag }}
|
||||
VERSION_TAG=${VERSION_TAG#v} # remove the 'v' prefix
|
||||
IFS='.' read -ra VERSION_PARTS <<< "$VERSION_TAG" # split into array
|
||||
VERSION_CODE=$(printf "%1d%02d%03d" "${VERSION_PARTS[0]}" "${VERSION_PARTS[1]}" "${VERSION_PARTS[2]}")
|
||||
|
||||
echo "versonName=$VERSION_TAG" >> $GITHUB_OUTPUT # "1.2.3"
|
||||
echo "versonCode=$VERSION_CODE" >> $GITHUB_OUTPUT # "102003"
|
||||
|
||||
# Re-write version in build.gradle.kts
|
||||
- name: Re-write version
|
||||
uses: Devofure/advance-android-version-actions@v1.5
|
||||
with:
|
||||
gradlePath: build.gradle.kts
|
||||
versionCode: ${{ steps.process-version.outputs.versonCode }}
|
||||
versionName: ${{ steps.process-version.outputs.versonName }}
|
||||
|
||||
# If any change found, commit it and push
|
||||
- name: Commit and push if changes
|
||||
run: |
|
||||
changes=$(git diff --name-only origin/main | wc -l)
|
||||
if [ $changes -gt 0 ]
|
||||
then
|
||||
newVersionName=${{ steps.process-version.outputs.versonName }}
|
||||
newVersionCode=${{ steps.process-version.outputs.versonCode }}
|
||||
git config --global user.name 'GitHub Action'
|
||||
git config --global user.email 'action@github.com'
|
||||
git add build.gradle.kts
|
||||
git commit -am "Bump version to $newVersionName ($newVersionCode)"
|
||||
git tag "v$newVersionName"
|
||||
git push --follow-tags
|
||||
fi
|
||||
|
||||
- name: Update CA
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install ca-certificates
|
||||
sudo update-ca-certificates
|
||||
cp -f /etc/ssl/certs/ca-certificates.crt core/src/foss/golang/clash/component/ca/ca-certificates.crt
|
||||
|
||||
- name: Signing properties
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
run: |
|
||||
touch signing.properties
|
||||
echo keystore.password="$SIGNING_STORE_PASSWORD" > signing.properties
|
||||
echo key.alias="$SIGNING_KEY_ALIAS" >> signing.properties
|
||||
echo key.password="$SIGNING_KEY_PASSWORD" >> signing.properties
|
||||
|
||||
echo "cat signing.properties"
|
||||
cat signing.properties
|
||||
|
||||
- name: Release Build
|
||||
if: success()
|
||||
run: ./gradlew --no-daemon app:assembleMetaRelease
|
||||
|
||||
- name: Tag Repo
|
||||
uses: richardsimko/update-tag@v1
|
||||
with:
|
||||
tag_name: ${{ inputs.release-tag }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
tag_name: ${{ inputs.release-tag }}
|
||||
files: app/build/outputs/apk/meta/release/*
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Release Changelog Builder
|
||||
uses: mikepenz/release-changelog-builder-action@v4.1.1
|
||||
with:
|
||||
configurationJson: |
|
||||
{
|
||||
"ignore_labels": [
|
||||
"Update"
|
||||
],
|
||||
}
|
||||
87
.github/workflows/update-dependencies.yaml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Update Clash-Core and Go Modules
|
||||
on:
|
||||
repository_dispatch:
|
||||
types:
|
||||
- core-updated
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-dependencies:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout and Update submodules
|
||||
run: git submodule update --init --recursive --remote --force
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: 21
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-download-base-url: 'https://github.com/MetaCubeX/go/releases/download/build'
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Apply Patches
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
for p in $GITHUB_WORKSPACE/.github/patch/*.patch; do patch --verbose -p 1 < "$p"; done
|
||||
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Install update-go-mod-replace
|
||||
run: |
|
||||
go install github.com/metacubex/update-go-mod-replace@latest
|
||||
|
||||
- name: Update Foss Gomod
|
||||
run: |
|
||||
cd ${{ github.workspace }}/core/src/foss/golang/
|
||||
update-go-mod-replace ${{ github.workspace }}/core/src/foss/golang/clash/go.mod $(pwd)/go.mod
|
||||
go mod tidy
|
||||
|
||||
- name: Update Main Gomod
|
||||
run: |
|
||||
cd ${{ github.workspace }}/core/src/main/golang/
|
||||
update-go-mod-replace ${{ github.workspace }}/core/src/foss/golang/clash/go.mod $(pwd)/go.mod
|
||||
go mod tidy
|
||||
|
||||
- uses: tibdex/github-app-token@v2
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.MAINTAINER_APPID }}
|
||||
private_key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
commit-message: Update Dependencies
|
||||
branch: update-dependencies
|
||||
delete-branch: true
|
||||
title: 'Update Dependencies'
|
||||
draft: false
|
||||
body: |
|
||||
- Update Clash-Meta Core
|
||||
- Update Go Module Dependecies
|
||||
labels: |
|
||||
Update
|
||||
|
||||
- name: PR result
|
||||
if: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
run: |
|
||||
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
|
||||
19
.gitignore
vendored
@@ -19,11 +19,19 @@ gradle-app.setting
|
||||
# Ignore IDEA config
|
||||
*.iml
|
||||
/.idea/*
|
||||
/core/src/main/golang/.idea/*
|
||||
!/.idea/codeStyles
|
||||
/core/src/main/golang/.idea/*
|
||||
!/core/src/main/golang/.idea/codeStyles
|
||||
/core/src/foss/golang/.idea/*
|
||||
!/core/src/foss/golang/.idea/codeStyles
|
||||
/core/src/premium/golang/.idea/*
|
||||
!/core/src/premium/golang/.idea/codeStyles
|
||||
|
||||
# Ignore builtin geofiles
|
||||
app/src/main/assets
|
||||
|
||||
# KeyStore
|
||||
signing.properties
|
||||
*.keystore
|
||||
*.jks
|
||||
|
||||
@@ -33,8 +41,9 @@ cmake-build-*
|
||||
# local.properties
|
||||
local.properties
|
||||
|
||||
# keystore
|
||||
keystore.properties
|
||||
|
||||
# tracker
|
||||
tracker.properties
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
@@ -53,5 +62,5 @@ google-services.json
|
||||
# logs
|
||||
*.log
|
||||
|
||||
# Ignore sum
|
||||
/core/src/main/golang/go.sum
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
13
.gitmodules
vendored
@@ -1,9 +1,4 @@
|
||||
[submodule "core/src/main/golang/clash"]
|
||||
path = core/src/main/golang/clash
|
||||
url = https://github.com/Kr328/clash.git
|
||||
[submodule "kaidl"]
|
||||
path = kaidl
|
||||
url = https://github.com/Kr328/kaidl.git
|
||||
[submodule "core/src/main/golang/tun2socket"]
|
||||
path = core/src/main/golang/tun2socket
|
||||
url = https://github.com/Kr328/tun2socket-lwip.git
|
||||
[submodule "clash-foss"]
|
||||
path = core/src/foss/golang/clash
|
||||
url = https://github.com/MetaCubeX/mihomo
|
||||
branch = Alpha
|
||||
|
||||
9
.idea/codeStyles/Project.xml
generated
@@ -20,15 +20,6 @@
|
||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
||||
89
README.md
@@ -1,32 +1,20 @@
|
||||
## Clash for Android
|
||||
## Clash Meta for Android
|
||||
|
||||
A Graphical user interface of [clash](https://github.com/Dreamacro/clash) for Android
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.github.kr328.clash"><img width="200px" alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or [Releases](https://github.com/Kr328/ClashForAndroid/releases)
|
||||
A Graphical user interface of [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) for Android
|
||||
|
||||
### Feature
|
||||
|
||||
Fully feature of [clash](https://github.com/Dreamacro/clash) ~~(Exclude `external-controller`~~
|
||||
|
||||
Feature of [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta)
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/com.github.metacubex.clash.meta/)
|
||||
|
||||
### Requirement
|
||||
|
||||
* Android 5.0+ (minimum)
|
||||
* Android 7.0+ (recommend)
|
||||
* `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
|
||||
|
||||
### License
|
||||
|
||||
See also [LICENSE](./LICENSE) and [NOTICE](./NOTICE)
|
||||
|
||||
|
||||
|
||||
### Privacy Policy
|
||||
|
||||
See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
|
||||
|
||||
|
||||
- Android 5.0+ (minimum)
|
||||
- Android 7.0+ (recommend)
|
||||
- `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
|
||||
|
||||
### Build
|
||||
|
||||
@@ -38,25 +26,62 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
|
||||
|
||||
2. Install **OpenJDK 11**, **Android SDK**, **CMake** and **Golang**
|
||||
|
||||
3. Create `local.properties` in project root with
|
||||
3. Create `local.properties` in project root with
|
||||
|
||||
```properties
|
||||
sdk.dir=/path/to/android-sdk
|
||||
```
|
||||
|
||||
4. Create `keystore.properties` in project root with
|
||||
4. (Optional) Custom app package name. Add the following configuration to `local.properties`.
|
||||
|
||||
```properties
|
||||
storeFile=/path/to/keystore/file
|
||||
storePassword=<key store password>
|
||||
keyAlias=<key alias>
|
||||
keyPassword=<key password>
|
||||
```
|
||||
# config your ownn applicationId, or it will be 'com.github.metacubex.clash'
|
||||
custom.application.id=com.my.compile.clash
|
||||
# remove application id suffix, or the applicaion id will be 'com.github.metacubex.clash.alpha'
|
||||
remove.suffix=true
|
||||
|
||||
5. Build
|
||||
5. Create `signing.properties` in project root with
|
||||
|
||||
```bash
|
||||
./gradlew app:assembleFossRelease
|
||||
```properties
|
||||
keystore.path=/path/to/keystore/file
|
||||
keystore.password=<key store password>
|
||||
key.alias=<key alias>
|
||||
key.password=<key password>
|
||||
```
|
||||
|
||||
6. Pick `app-release-<arch>.apk` in `app/build/outputs/apks`
|
||||
6. Build
|
||||
|
||||
```bash
|
||||
./gradlew app:assembleAlphaRelease
|
||||
```
|
||||
|
||||
### Automation
|
||||
|
||||
APP package name is `com.github.metacubex.clash.meta`
|
||||
|
||||
- Toggle Clash.Meta service status
|
||||
- Send intent to activity `com.github.kr328.clash.ExternalControlActivity` with action `com.github.metacubex.clash.meta.action.TOGGLE_CLASH`
|
||||
- Start Clash.Meta service
|
||||
- Send intent to activity `com.github.kr328.clash.ExternalControlActivity` with action `com.github.metacubex.clash.meta.action.START_CLASH`
|
||||
- Stop Clash.Meta service
|
||||
- Send intent to activity `com.github.kr328.clash.ExternalControlActivity` with action `com.github.metacubex.clash.meta.action.STOP_CLASH`
|
||||
- Import a profile
|
||||
- URL Scheme `clash://install-config?url=<encoded URI>` or `clashmeta://install-config?url=<encoded URI>`
|
||||
|
||||
### Contribution and Project Maintenance
|
||||
|
||||
#### Meta Kernel
|
||||
|
||||
- CMFA uses the kernel from `android-real` branch under `MetaCubeX/Clash.Meta`, which is a merge of the main `Alpha` branch and `android-open`.
|
||||
- If you want to contribute to the kernel, make PRs to `Alpha` branch of the Meta kernel repository.
|
||||
- If you want to contribute Android-specific patches to the kernel, make PRs to `android-open` branch of the Meta kernel repository.
|
||||
|
||||
#### Maintenance
|
||||
|
||||
- When `MetaCubeX/Clash.Meta` kernel is updated to a new version, the `Update Dependencies` actions in this repo will be triggered automatically.
|
||||
- It will pull the new version of the meta kernel, update all the golang dependencies, and create a PR without manual intervention.
|
||||
- If there is any compile error in PR, you need to fix it before merging. Alternatively, you may merge the PR directly.
|
||||
- Manually triggering `Build Pre-Release` actions will compile and publish a `PreRelease` version.
|
||||
- Manually triggering `Build Release` actions will compile, tag and publish a `Release` version.
|
||||
- You must fill the blank `Release Tag` with the tag you want to release in the format of `v1.2.3`.
|
||||
- `versionName` and `versionCode` in `build.gradle.kts` will be automatically bumped to the tag you filled above.
|
||||
|
||||
@@ -1,133 +1,71 @@
|
||||
import java.util.*
|
||||
import java.net.URL
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = buildTargetSdkVersion
|
||||
|
||||
flavorDimensions(buildFlavor)
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.github.kr328.clash"
|
||||
|
||||
minSdk = buildMinSdkVersion
|
||||
targetSdk = buildTargetSdkVersion
|
||||
|
||||
versionCode = buildVersionCode
|
||||
versionName = buildVersionName
|
||||
|
||||
resConfigs("zh-rCN", "zh-rHK", "zh-rTW")
|
||||
|
||||
resValue("string", "release_name", "v$buildVersionName")
|
||||
resValue("integer", "release_code", "$buildVersionCode")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
create("foss") {
|
||||
dimension = "foss"
|
||||
versionNameSuffix = ".foss"
|
||||
applicationIdSuffix = ".foss"
|
||||
}
|
||||
create("premium") {
|
||||
dimension = "premium"
|
||||
versionNameSuffix = ".premium"
|
||||
|
||||
if (buildFlavor == "premium") {
|
||||
val appCenterKey = rootProject.file("local.properties").inputStream()
|
||||
.use { Properties().apply { load(it) } }
|
||||
.getProperty("appcenter.key", null)
|
||||
|
||||
Objects.requireNonNull(appCenterKey)
|
||||
|
||||
buildConfigField("String", "APP_CENTER_KEY", "\"$appCenterKey\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val signingFile = rootProject.file("keystore.properties")
|
||||
if (signingFile.exists()) {
|
||||
val properties = Properties().apply {
|
||||
signingFile.inputStream().use {
|
||||
load(it)
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = rootProject.file(properties.getProperty("storeFile")!!)
|
||||
storePassword = properties.getProperty("storePassword")!!
|
||||
keyAlias = properties.getProperty("keyAlias")!!
|
||||
keyPassword = properties.getProperty("keyPassword")!!
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
named("release") {
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
id("com.android.application")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val premiumImplementation by configurations
|
||||
compileOnly(project(":hideapi"))
|
||||
|
||||
api(project(":core"))
|
||||
api(project(":service"))
|
||||
api(project(":design"))
|
||||
api(project(":common"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":service"))
|
||||
implementation(project(":design"))
|
||||
implementation(project(":common"))
|
||||
|
||||
premiumImplementation("com.microsoft.appcenter:appcenter-analytics:$appcenterVersion")
|
||||
premiumImplementation("com.microsoft.appcenter:appcenter-crashes:$appcenterVersion")
|
||||
|
||||
implementation(kotlin("stdlib-jdk7"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
|
||||
implementation("androidx.core:core-ktx:$ktxVersion")
|
||||
implementation("androidx.activity:activity:$activityVersion")
|
||||
implementation("androidx.appcompat:appcompat:$appcompatVersion")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayoutVersion")
|
||||
implementation("androidx.recyclerview:recyclerview:$recyclerviewVersion")
|
||||
implementation("androidx.fragment:fragment:$fragmentVersion")
|
||||
implementation("com.google.android.material:material:$materialVersion")
|
||||
implementation(libs.kotlin.coroutine)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.fragment)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.coordinator)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.google.material)
|
||||
implementation(libs.quickie.bundled)
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
}
|
||||
|
||||
task("cleanRelease", type = Delete::class) {
|
||||
tasks.getByName("clean", type = Delete::class) {
|
||||
delete(file("release"))
|
||||
}
|
||||
|
||||
val geoFilesDownloadDir = "src/main/assets"
|
||||
|
||||
task("downloadGeoFiles") {
|
||||
|
||||
val geoFilesUrls = mapOf(
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb" to "geoip.metadb",
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat" to "geosite.dat",
|
||||
// "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb" to "country.mmdb",
|
||||
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb" to "ASN.mmdb",
|
||||
)
|
||||
|
||||
doLast {
|
||||
geoFilesUrls.forEach { (downloadUrl, outputFileName) ->
|
||||
val url = URL(downloadUrl)
|
||||
val outputPath = file("$geoFilesDownloadDir/$outputFileName")
|
||||
outputPath.parentFile.mkdirs()
|
||||
url.openStream().use { input ->
|
||||
Files.copy(input, outputPath.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
println("$outputFileName downloaded to $outputPath")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks["clean"].dependsOn(tasks["cleanRelease"])
|
||||
val downloadGeoFilesTask = tasks["downloadGeoFiles"]
|
||||
|
||||
tasks.forEach {
|
||||
if (it.name.startsWith("assemble")) {
|
||||
it.dependsOn(downloadGeoFilesTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.getByName("clean", type = Delete::class) {
|
||||
delete(file(geoFilesDownloadDir))
|
||||
}
|
||||
26
app/proguard-rules.pro
vendored
@@ -31,3 +31,29 @@
|
||||
public static void checkParameterIsNotNull(...);
|
||||
public static void checkNotNullParameter(...);
|
||||
}
|
||||
|
||||
# Kotlin Coroutine
|
||||
# Allow R8 to optimize away the FastServiceLoader.
|
||||
# Together with ServiceLoader optimization in R8
|
||||
# this results in direct instantiation when loading Dispatchers.Main
|
||||
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {
|
||||
boolean FAST_SERVICE_LOADER_ENABLED return false;
|
||||
}
|
||||
|
||||
-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoaderKt {
|
||||
boolean ANDROID_DETECTED return true;
|
||||
}
|
||||
|
||||
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
|
||||
|
||||
# Disable support for "Missing Main Dispatcher", since we always have Android main dispatcher
|
||||
-assumenosideeffects class kotlinx.coroutines.internal.MainDispatchersKt {
|
||||
boolean SUPPORT_MISSING return false;
|
||||
}
|
||||
|
||||
# Statically turn off all debugging facilities and assertions
|
||||
-assumenosideeffects class kotlinx.coroutines.DebugKt {
|
||||
boolean getASSERTIONS_ENABLED() return false;
|
||||
boolean getDEBUG() return false;
|
||||
boolean getRECOVER_STACK_TRACES() return false;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.app.Application
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
object Tracker {
|
||||
fun initialize(application: Application) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
fun uploadLogcat(logcat: String) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.github.kr328.clash">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
@@ -12,10 +11,11 @@
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@@ -41,22 +41,34 @@
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="uiMode"
|
||||
android:exported="true"
|
||||
android:label="@string/launch_name">
|
||||
android:label="@string/launch_name"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ExternalImportActivity"
|
||||
<activity-alias
|
||||
android:name=".MainActivityAlias"
|
||||
android:exported="true"
|
||||
android:label="@string/import_from_file"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name=".ExternalControlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="@string/external_control_activity"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -64,9 +76,21 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="install-config"
|
||||
android:scheme="clash" />
|
||||
<data android:scheme="clash"/>
|
||||
<data android:scheme="clashmeta"/>
|
||||
<data android:host="install-config"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.github.metacubex.clash.meta.action.START_CLASH" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.github.metacubex.clash.meta.action.STOP_CLASH" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.github.metacubex.clash.meta.action.TOGGLE_CLASH" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
@@ -135,6 +159,11 @@
|
||||
android:configChanges="uiMode"
|
||||
android:exported="false"
|
||||
android:label="@string/override" />
|
||||
<activity
|
||||
android:name=".MetaFeatureSettingsActivity"
|
||||
android:configChanges="uiMode"
|
||||
android:exported="false"
|
||||
android:label="@string/meta_features" />
|
||||
<activity
|
||||
android:name=".AccessControlActivity"
|
||||
android:configChanges="uiMode"
|
||||
@@ -154,7 +183,11 @@
|
||||
<service
|
||||
android:name=".LogcatService"
|
||||
android:exported="false"
|
||||
android:label="@string/clash_logcat" />
|
||||
android:label="@string/clash_logcat"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="explanation_for_special_use"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".TileService"
|
||||
android:exported="true"
|
||||
@@ -175,5 +208,14 @@
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".DialerReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.SECRET_CODE" />
|
||||
<!-- 252746382 is the name of Clash Meta in T9 -->
|
||||
<data android:scheme="android_secret_code" android:host="252746382" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -11,7 +11,10 @@ import com.github.kr328.clash.design.AccessControlDesign
|
||||
import com.github.kr328.clash.design.model.AppInfo
|
||||
import com.github.kr328.clash.design.util.toAppInfo
|
||||
import com.github.kr328.clash.service.store.ServiceStore
|
||||
import com.github.kr328.clash.util.startClashService
|
||||
import com.github.kr328.clash.util.stopClashService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -26,7 +29,15 @@ class AccessControlActivity : BaseActivity<AccessControlDesign>() {
|
||||
|
||||
defer {
|
||||
withContext(Dispatchers.IO) {
|
||||
val changed = selected != service.accessControlPackages
|
||||
service.accessControlPackages = selected
|
||||
if (clashRunning && changed) {
|
||||
stopClashService()
|
||||
while (clashRunning) {
|
||||
delay(200)
|
||||
}
|
||||
startClashService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +57,7 @@ class AccessControlActivity : BaseActivity<AccessControlDesign>() {
|
||||
AccessControlDesign.Request.ReloadApps -> {
|
||||
design.patchApps(loadApps(selected))
|
||||
}
|
||||
|
||||
AccessControlDesign.Request.SelectAll -> {
|
||||
val all = withContext(Dispatchers.Default) {
|
||||
design.apps.map(AppInfo::packageName)
|
||||
@@ -56,11 +68,13 @@ class AccessControlActivity : BaseActivity<AccessControlDesign>() {
|
||||
|
||||
design.rebindAll()
|
||||
}
|
||||
|
||||
AccessControlDesign.Request.SelectNone -> {
|
||||
selected.clear()
|
||||
|
||||
design.rebindAll()
|
||||
}
|
||||
|
||||
AccessControlDesign.Request.SelectInvert -> {
|
||||
val all = withContext(Dispatchers.Default) {
|
||||
design.apps.map(AppInfo::packageName).toSet() - selected
|
||||
@@ -71,16 +85,14 @@ class AccessControlActivity : BaseActivity<AccessControlDesign>() {
|
||||
|
||||
design.rebindAll()
|
||||
}
|
||||
|
||||
AccessControlDesign.Request.Import -> {
|
||||
val clipboard = getSystemService<ClipboardManager>()
|
||||
val data = clipboard?.primaryClip
|
||||
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val all = withContext(Dispatchers.IO) {
|
||||
val packages = data.getItemAt(0).text.split("\n").toSet()
|
||||
|
||||
design.apps.map(AppInfo::packageName).intersect(packages)
|
||||
}
|
||||
val packages = data.getItemAt(0).text.split("\n").toSet()
|
||||
val all = design.apps.map(AppInfo::packageName).intersect(packages)
|
||||
|
||||
selected.clear()
|
||||
selected.addAll(all)
|
||||
@@ -88,17 +100,16 @@ class AccessControlActivity : BaseActivity<AccessControlDesign>() {
|
||||
|
||||
design.rebindAll()
|
||||
}
|
||||
|
||||
AccessControlDesign.Request.Export -> {
|
||||
val clipboard = getSystemService<ClipboardManager>()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val data = ClipData.newPlainText(
|
||||
"packages",
|
||||
selected.joinToString("\n")
|
||||
)
|
||||
val data = ClipData.newPlainText(
|
||||
"packages",
|
||||
selected.joinToString("\n")
|
||||
)
|
||||
|
||||
clipboard?.setPrimaryClip(data)
|
||||
}
|
||||
clipboard?.setPrimaryClip(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +134,10 @@ class AccessControlActivity : BaseActivity<AccessControlDesign>() {
|
||||
it.packageName != packageName
|
||||
}
|
||||
.filter {
|
||||
it.packageName == "android" || it.requestedPermissions?.contains(INTERNET) == true
|
||||
it.applicationInfo != null
|
||||
}
|
||||
.filter {
|
||||
it.requestedPermissions?.contains(INTERNET) == true || it.applicationInfo!!.uid < android.os.Process.FIRST_APPLICATION_UID
|
||||
}
|
||||
.filter {
|
||||
systemApp || !it.isSystemApp
|
||||
@@ -137,6 +151,6 @@ class AccessControlActivity : BaseActivity<AccessControlDesign>() {
|
||||
|
||||
private val PackageInfo.isSystemApp: Boolean
|
||||
get() {
|
||||
return applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0
|
||||
return applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,6 @@ class AppCrashedActivity : BaseActivity<AppCrashedDesign>() {
|
||||
SystemLogcat.dumpCrash()
|
||||
}
|
||||
|
||||
Tracker.uploadLogcat(logs)
|
||||
|
||||
design.setAppLogs(logs)
|
||||
|
||||
while (isActive) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
import com.github.kr328.clash.common.util.componentName
|
||||
import com.github.kr328.clash.design.AppSettingsDesign
|
||||
@@ -17,6 +18,7 @@ class AppSettingsActivity : BaseActivity<AppSettingsDesign>(), Behavior {
|
||||
ServiceStore(this),
|
||||
this,
|
||||
clashRunning,
|
||||
::onHideIconChange,
|
||||
)
|
||||
|
||||
setContentDesign(design)
|
||||
@@ -59,4 +61,17 @@ class AppSettingsActivity : BaseActivity<AppSettingsDesign>(), Behavior {
|
||||
PackageManager.DONT_KILL_APP,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onHideIconChange(hide: Boolean) {
|
||||
val newState = if (hide) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
} else {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
}
|
||||
packageManager.setComponentEnabledSetting(
|
||||
ComponentName(this, mainActivityAlias),
|
||||
newState,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
|
||||
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
|
||||
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
|
||||
@@ -24,34 +26,24 @@ import com.github.kr328.clash.util.ActivityResultLifecycle
|
||||
import com.github.kr328.clash.util.ApplicationObserver
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
abstract class BaseActivity<D : Design<*>> :
|
||||
AppCompatActivity(),
|
||||
abstract class BaseActivity<D : Design<*>> : AppCompatActivity(),
|
||||
CoroutineScope by MainScope(),
|
||||
Broadcasts.Observer {
|
||||
enum class Event {
|
||||
ServiceRecreated,
|
||||
ActivityStart,
|
||||
ActivityStop,
|
||||
ClashStop,
|
||||
ClashStart,
|
||||
ProfileLoaded,
|
||||
ProfileChanged
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected val uiStore by lazy { UiStore(this) }
|
||||
protected val events = Channel<Event>(Channel.UNLIMITED)
|
||||
protected var activityStarted: Boolean = false
|
||||
protected val clashRunning: Boolean
|
||||
get() = Remote.broadcasts.clashRunning
|
||||
protected var design: D? = null
|
||||
private set(value) {
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
if (value != null) {
|
||||
setContentView(value.root)
|
||||
} else {
|
||||
@@ -72,14 +64,14 @@ abstract class BaseActivity<D : Design<*>> :
|
||||
|
||||
suspend fun <I, O> startActivityForResult(
|
||||
contracts: ActivityResultContract<I, O>,
|
||||
input: I
|
||||
input: I,
|
||||
): O = withContext(Dispatchers.Main) {
|
||||
val requestKey = nextRequestKey.getAndIncrement().toString()
|
||||
|
||||
ActivityResultLifecycle().use { lifecycle, start ->
|
||||
suspendCoroutine { c ->
|
||||
activityResultRegistry.register(requestKey, lifecycle, contracts) {
|
||||
c.resumeWith(Result.success(it))
|
||||
c.resume(it)
|
||||
}.apply { start() }.launch(input)
|
||||
}
|
||||
}
|
||||
@@ -89,7 +81,6 @@ abstract class BaseActivity<D : Design<*>> :
|
||||
suspendCoroutine<Unit> {
|
||||
window.decorView.post {
|
||||
this.design = design
|
||||
|
||||
it.resume(Unit)
|
||||
}
|
||||
}
|
||||
@@ -97,49 +88,40 @@ abstract class BaseActivity<D : Design<*>> :
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applyDayNight()
|
||||
|
||||
// Apply excludeFromRecents setting to all app tasks.
|
||||
checkNotNull(getSystemService<ActivityManager>()).appTasks.forEach { task ->
|
||||
task.setExcludeFromRecents(uiStore.hideFromRecents)
|
||||
}
|
||||
|
||||
launch {
|
||||
main()
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
activityStarted = true
|
||||
|
||||
Remote.broadcasts.addObserver(this)
|
||||
|
||||
events.offer(Event.ActivityStart)
|
||||
events.trySend(Event.ActivityStart)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
activityStarted = false
|
||||
|
||||
Remote.broadcasts.removeObserver(this)
|
||||
|
||||
events.offer(Event.ActivityStop)
|
||||
events.trySend(Event.ActivityStop)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
design?.cancel()
|
||||
|
||||
cancel()
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
if (deferRunning) {
|
||||
return
|
||||
}
|
||||
|
||||
if (deferRunning) return
|
||||
deferRunning = true
|
||||
|
||||
launch {
|
||||
@@ -169,28 +151,35 @@ abstract class BaseActivity<D : Design<*>> :
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
this.onBackPressed()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onProfileChanged() {
|
||||
events.offer(Event.ProfileChanged)
|
||||
events.trySend(Event.ProfileChanged)
|
||||
}
|
||||
|
||||
override fun onProfileUpdateCompleted(uuid: UUID?) {
|
||||
events.trySend(Event.ProfileUpdateCompleted)
|
||||
}
|
||||
|
||||
override fun onProfileUpdateFailed(uuid: UUID?, reason: String?) {
|
||||
events.trySend(Event.ProfileUpdateFailed)
|
||||
}
|
||||
|
||||
override fun onProfileLoaded() {
|
||||
events.offer(Event.ProfileLoaded)
|
||||
events.trySend(Event.ProfileLoaded)
|
||||
}
|
||||
|
||||
override fun onServiceRecreated() {
|
||||
events.offer(Event.ServiceRecreated)
|
||||
events.trySend(Event.ServiceRecreated)
|
||||
}
|
||||
|
||||
override fun onStarted() {
|
||||
events.offer(Event.ClashStart)
|
||||
events.trySend(Event.ClashStart)
|
||||
}
|
||||
|
||||
override fun onStopped(cause: String?) {
|
||||
events.offer(Event.ClashStop)
|
||||
events.trySend(Event.ClashStop)
|
||||
|
||||
if (cause != null && activityStarted) {
|
||||
launch {
|
||||
@@ -201,49 +190,45 @@ abstract class BaseActivity<D : Design<*>> :
|
||||
|
||||
private fun queryDayNight(config: Configuration = resources.configuration): DayNight {
|
||||
return when (uiStore.darkMode) {
|
||||
DarkMode.Auto -> {
|
||||
if (config.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
|
||||
DayNight.Night
|
||||
else
|
||||
DayNight.Day
|
||||
}
|
||||
DarkMode.ForceLight -> {
|
||||
DayNight.Day
|
||||
}
|
||||
DarkMode.ForceDark -> {
|
||||
DayNight.Night
|
||||
}
|
||||
DarkMode.Auto -> if (config.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) DayNight.Night else DayNight.Day
|
||||
DarkMode.ForceLight -> DayNight.Day
|
||||
DarkMode.ForceDark -> DayNight.Night
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyDayNight(config: Configuration = resources.configuration) {
|
||||
val dayNight = queryDayNight(config)
|
||||
|
||||
when (dayNight) {
|
||||
DayNight.Night -> {
|
||||
theme.applyStyle(R.style.AppThemeDark, true)
|
||||
}
|
||||
DayNight.Day -> {
|
||||
theme.applyStyle(R.style.AppThemeLight, true)
|
||||
}
|
||||
DayNight.Night -> theme.applyStyle(R.style.AppThemeDark, true)
|
||||
DayNight.Day -> theme.applyStyle(R.style.AppThemeLight, true)
|
||||
}
|
||||
|
||||
window.isAllowForceDarkCompat = false
|
||||
window.isSystemBarsTranslucentCompat = true
|
||||
|
||||
|
||||
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
|
||||
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
window.isLightStatusBarsCompat =
|
||||
resolveThemedBoolean(android.R.attr.windowLightStatusBar)
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
window.isLightStatusBarsCompat = resolveThemedBoolean(android.R.attr.windowLightStatusBar)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
window.isLightNavigationBarCompat =
|
||||
resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
window.isLightNavigationBarCompat = resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
|
||||
}
|
||||
|
||||
this.dayNight = dayNight
|
||||
}
|
||||
|
||||
enum class Event {
|
||||
ServiceRecreated,
|
||||
ActivityStart,
|
||||
ActivityStop,
|
||||
ClashStop,
|
||||
ClashStart,
|
||||
ProfileLoaded,
|
||||
ProfileChanged,
|
||||
ProfileUpdateCompleted,
|
||||
ProfileUpdateFailed,
|
||||
}
|
||||
}
|
||||
|
||||
13
app/src/main/java/com/github/kr328/clash/DialerReceiver.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
class DialerReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.github.kr328.clash.common.constants.Intents
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.common.util.setUUID
|
||||
import com.github.kr328.clash.design.MainDesign
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.remote.Remote
|
||||
import com.github.kr328.clash.remote.StatusClient
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
import com.github.kr328.clash.util.startClashService
|
||||
import com.github.kr328.clash.util.stopClashService
|
||||
import com.github.kr328.clash.util.withProfile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
class ExternalControlActivity : Activity(), CoroutineScope by MainScope() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(0, 0)
|
||||
|
||||
when(intent.action) {
|
||||
Intent.ACTION_VIEW -> {
|
||||
val uri = intent.data ?: return finish()
|
||||
val url = uri.getQueryParameter("url") ?: return finish()
|
||||
|
||||
launch {
|
||||
val uuid = withProfile {
|
||||
val type = when (uri.getQueryParameter("type")?.lowercase(Locale.getDefault())) {
|
||||
"url" -> Profile.Type.Url
|
||||
"file" -> Profile.Type.File
|
||||
else -> Profile.Type.Url
|
||||
}
|
||||
val name = uri.getQueryParameter("name") ?: getString(R.string.new_profile)
|
||||
|
||||
create(type, name).also {
|
||||
patch(it, name, url, 0)
|
||||
}
|
||||
}
|
||||
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
Intents.ACTION_TOGGLE_CLASH -> if(Remote.broadcasts.clashRunning) {
|
||||
stopClash()
|
||||
}
|
||||
else {
|
||||
startClash()
|
||||
}
|
||||
|
||||
Intents.ACTION_START_CLASH -> if(!Remote.broadcasts.clashRunning) {
|
||||
startClash()
|
||||
}
|
||||
else {
|
||||
Toast.makeText(this, R.string.external_control_started, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
Intents.ACTION_STOP_CLASH -> if(Remote.broadcasts.clashRunning) {
|
||||
stopClash()
|
||||
}
|
||||
else {
|
||||
Toast.makeText(this, R.string.external_control_stopped, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
return finish()
|
||||
}
|
||||
|
||||
private fun startClash() {
|
||||
// if (currentProfile == null) {
|
||||
// Toast.makeText(this, R.string.no_profile_selected, Toast.LENGTH_LONG).show()
|
||||
// return
|
||||
// }
|
||||
val vpnRequest = startClashService()
|
||||
if (vpnRequest != null) {
|
||||
Toast.makeText(this, R.string.unable_to_start_vpn, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
Toast.makeText(this, R.string.external_control_started, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun stopClash() {
|
||||
stopClashService()
|
||||
Toast.makeText(this, R.string.external_control_stopped, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.common.util.setUUID
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
import com.github.kr328.clash.util.withProfile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class ExternalImportActivity : Activity(), CoroutineScope by MainScope() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (intent.action != Intent.ACTION_VIEW)
|
||||
return finish()
|
||||
|
||||
val uri = intent.data ?: return finish()
|
||||
val url = uri.getQueryParameter("url") ?: return finish()
|
||||
|
||||
launch {
|
||||
val uuid = withProfile {
|
||||
val type = when (uri.getQueryParameter("type")?.lowercase(Locale.getDefault())) {
|
||||
"url" -> Profile.Type.Url
|
||||
"file" -> Profile.Type.File
|
||||
else -> Profile.Type.Url
|
||||
}
|
||||
val name = uri.getQueryParameter("name") ?: getString(R.string.new_profile)
|
||||
|
||||
create(type, name).also {
|
||||
patch(it, name, url, 0)
|
||||
}
|
||||
}
|
||||
|
||||
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,24 +81,6 @@ class FilesActivity : BaseActivity<FilesDesign>() {
|
||||
client.renameDocument(it.file.id, newName)
|
||||
}
|
||||
is FilesDesign.Request.ImportFile -> {
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
val hasPermission = ContextCompat.checkSelfPermission(
|
||||
this@FilesActivity,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
if (!hasPermission) {
|
||||
val granted = startActivityForResult(
|
||||
ActivityResultContracts.RequestPermission(),
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
)
|
||||
|
||||
if (!granted) {
|
||||
return@onReceive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri? = startActivityForResult(
|
||||
ActivityResultContracts.GetContent(),
|
||||
"*/*"
|
||||
@@ -116,7 +98,7 @@ class FilesActivity : BaseActivity<FilesDesign>() {
|
||||
}
|
||||
is FilesDesign.Request.ExportFile -> {
|
||||
val uri: Uri? = startActivityForResult(
|
||||
ActivityResultContracts.CreateDocument(),
|
||||
ActivityResultContracts.CreateDocument("text/plain"),
|
||||
it.file.name
|
||||
)
|
||||
|
||||
@@ -141,7 +123,7 @@ class FilesActivity : BaseActivity<FilesDesign>() {
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
design?.requests?.offer(FilesDesign.Request.PopStack)
|
||||
design?.requests?.trySend(FilesDesign.Request.PopStack)
|
||||
}
|
||||
|
||||
private suspend fun FilesDesign.fetch(client: FilesClient, stack: Stack<String>, root: String) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.github.kr328.clash.common.compat.startForegroundServiceCompat
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import com.github.kr328.clash.common.util.fileName
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.common.util.ticker
|
||||
@@ -27,6 +28,7 @@ import kotlinx.coroutines.withContext
|
||||
import java.io.OutputStreamWriter
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
class LogcatActivity : BaseActivity<LogcatDesign>() {
|
||||
private var conn: ServiceConnection? = null
|
||||
@@ -47,6 +49,7 @@ class LogcatActivity : BaseActivity<LogcatDesign>() {
|
||||
val messages = try {
|
||||
LogcatReader(this, file).readAll()
|
||||
} catch (e: Exception) {
|
||||
Log.e("Fail to read log file ${file.fileName}: ${e.message}")
|
||||
return showInvalid()
|
||||
}
|
||||
|
||||
@@ -67,7 +70,7 @@ class LogcatActivity : BaseActivity<LogcatDesign>() {
|
||||
}
|
||||
LogcatDesign.Request.Export -> {
|
||||
val output = startActivityForResult(
|
||||
ActivityResultContracts.CreateDocument(),
|
||||
ActivityResultContracts.CreateDocument("text/plain"),
|
||||
file.fileName
|
||||
)
|
||||
|
||||
@@ -109,7 +112,7 @@ class LogcatActivity : BaseActivity<LogcatDesign>() {
|
||||
when (it) {
|
||||
LogcatDesign.Request.Close -> {
|
||||
stopService(LogcatService::class.intent)
|
||||
|
||||
startActivity(LogsActivity::class.intent)
|
||||
finish()
|
||||
}
|
||||
else -> Unit
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ComponentName
|
||||
@@ -9,20 +7,22 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.IInterface
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.github.kr328.clash.common.compat.getColorCompat
|
||||
import com.github.kr328.clash.common.compat.pendingIntentFlags
|
||||
import com.github.kr328.clash.common.compat.startForegroundCompat
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.core.model.LogMessage
|
||||
import com.github.kr328.clash.log.LogcatCache
|
||||
import com.github.kr328.clash.log.LogcatWriter
|
||||
import com.github.kr328.clash.service.ClashManager
|
||||
import com.github.kr328.clash.service.remote.IClashManager
|
||||
import com.github.kr328.clash.service.RemoteService
|
||||
import com.github.kr328.clash.service.remote.ILogObserver
|
||||
import com.github.kr328.clash.service.remote.IRemoteService
|
||||
import com.github.kr328.clash.service.remote.unwrap
|
||||
import com.github.kr328.clash.util.logsDir
|
||||
import kotlinx.coroutines.*
|
||||
@@ -52,7 +52,7 @@ class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.De
|
||||
|
||||
showNotification()
|
||||
|
||||
bindService(ClashManager::class.intent, connection, Context.BIND_AUTO_CREATE)
|
||||
bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -88,7 +88,7 @@ class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.De
|
||||
return stopSelf()
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val service = binder.unwrap(IClashManager::class)
|
||||
val service = binder.unwrap(IRemoteService::class).clash()
|
||||
val channel = Channel<LogMessage>(CACHE_CAPACITY)
|
||||
|
||||
try {
|
||||
@@ -97,7 +97,7 @@ class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.De
|
||||
LogcatWriter(this@LogcatService).use {
|
||||
val observer = object : ILogObserver {
|
||||
override fun newItem(log: LogMessage) {
|
||||
channel.offer(log)
|
||||
channel.trySend(log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,38 +126,34 @@ class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.De
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return
|
||||
|
||||
NotificationManagerCompat.from(this)
|
||||
.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NotificationChannelCompat.Builder(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.clash_logcat),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||
).setName(getString(com.github.kr328.clash.design.R.string.clash_logcat)).build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNotification() {
|
||||
val notification = NotificationCompat
|
||||
.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_logo_service)
|
||||
.setColor(getColorCompat(R.color.color_clash_light))
|
||||
.setContentTitle(getString(R.string.clash_logcat))
|
||||
.setContentText(getString(R.string.running))
|
||||
.setSmallIcon(com.github.kr328.clash.service.R.drawable.ic_logo_service)
|
||||
.setColor(getColorCompat(com.github.kr328.clash.design.R.color.color_clash_light))
|
||||
.setContentTitle(getString(com.github.kr328.clash.design.R.string.clash_logcat))
|
||||
.setContentText(getString(com.github.kr328.clash.design.R.string.running))
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
R.id.nf_logcat_status,
|
||||
LogcatActivity::class.intent
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
)
|
||||
.build()
|
||||
|
||||
startForeground(R.id.nf_logcat_status, notification)
|
||||
startForegroundCompat(R.id.nf_logcat_status, notification)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -11,11 +11,8 @@ import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LogsActivity : BaseActivity<LogsDesign>() {
|
||||
override suspend fun main() {
|
||||
if (LogcatService.running) {
|
||||
return startActivity(LogcatActivity::class.intent)
|
||||
}
|
||||
|
||||
override suspend fun main() {
|
||||
val design = LogsDesign(this)
|
||||
|
||||
setContentDesign(design)
|
||||
@@ -38,7 +35,6 @@ class LogsActivity : BaseActivity<LogsDesign>() {
|
||||
when (it) {
|
||||
LogsDesign.Request.StartLogcat -> {
|
||||
startActivity(LogcatActivity::class.intent)
|
||||
|
||||
finish()
|
||||
}
|
||||
LogsDesign.Request.DeleteAll -> {
|
||||
@@ -47,7 +43,7 @@ class LogsActivity : BaseActivity<LogsDesign>() {
|
||||
deleteAllLogs()
|
||||
}
|
||||
|
||||
events.offer(Event.ActivityStart)
|
||||
events.trySend(Event.ActivityStart)
|
||||
}
|
||||
}
|
||||
is LogsDesign.Request.OpenFile -> {
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.common.util.ticker
|
||||
import com.github.kr328.clash.design.MainDesign
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.store.TipsStore
|
||||
import com.github.kr328.clash.util.startClashService
|
||||
import com.github.kr328.clash.util.stopClashService
|
||||
import com.github.kr328.clash.util.withClash
|
||||
import com.github.kr328.clash.util.withProfile
|
||||
import com.github.kr328.clash.core.bridge.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
class MainActivity : BaseActivity<MainDesign>() {
|
||||
override suspend fun main() {
|
||||
@@ -23,10 +30,6 @@ class MainActivity : BaseActivity<MainDesign>() {
|
||||
|
||||
setContentDesign(design)
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
showUpdatedTips(design)
|
||||
}
|
||||
|
||||
design.fetch()
|
||||
|
||||
val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
|
||||
@@ -56,8 +59,13 @@ class MainActivity : BaseActivity<MainDesign>() {
|
||||
startActivity(ProfilesActivity::class.intent)
|
||||
MainDesign.Request.OpenProviders ->
|
||||
startActivity(ProvidersActivity::class.intent)
|
||||
MainDesign.Request.OpenLogs ->
|
||||
startActivity(LogsActivity::class.intent)
|
||||
MainDesign.Request.OpenLogs -> {
|
||||
if (LogcatService.running) {
|
||||
startActivity(LogcatActivity::class.intent)
|
||||
} else {
|
||||
startActivity(LogsActivity::class.intent)
|
||||
}
|
||||
}
|
||||
MainDesign.Request.OpenSettings ->
|
||||
startActivity(SettingsActivity::class.intent)
|
||||
MainDesign.Request.OpenHelp ->
|
||||
@@ -75,20 +83,6 @@ class MainActivity : BaseActivity<MainDesign>() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showUpdatedTips(design: MainDesign) {
|
||||
val tips = TipsStore(this)
|
||||
|
||||
if (tips.primaryVersion != TipsStore.CURRENT_PRIMARY_VERSION) {
|
||||
tips.primaryVersion = TipsStore.CURRENT_PRIMARY_VERSION
|
||||
|
||||
val pkg = packageManager.getPackageInfo(packageName, 0)
|
||||
|
||||
if (pkg.firstInstallTime != pkg.lastUpdateTime) {
|
||||
design.showUpdatedTips()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MainDesign.fetch() {
|
||||
setClashRunning(clashRunning)
|
||||
|
||||
@@ -145,7 +139,25 @@ class MainActivity : BaseActivity<MainDesign>() {
|
||||
|
||||
private suspend fun queryAppVersionName(): String {
|
||||
return withContext(Dispatchers.IO) {
|
||||
packageManager.getPackageInfo(packageName, 0).versionName
|
||||
packageManager.getPackageInfo(packageName, 0).versionName + "\n" + Bridge.nativeCoreVersion().replace("_", "-")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val requestPermissionLauncher =
|
||||
registerForActivityResult(RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
android.Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mainActivityAlias = "${MainActivity::class.java.name}Alias"
|
||||
@@ -2,11 +2,21 @@ package com.github.kr328.clash
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.github.kr328.clash.common.Global
|
||||
import com.github.kr328.clash.common.compat.currentProcessName
|
||||
import com.github.kr328.clash.common.constants.Intents
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import com.github.kr328.clash.remote.Remote
|
||||
import com.github.kr328.clash.service.util.sendServiceRecreated
|
||||
import com.github.kr328.clash.util.clashDir
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import com.github.kr328.clash.design.R as DesignR
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
class MainApplication : Application() {
|
||||
@@ -19,17 +29,100 @@ class MainApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Initialize AppCenter
|
||||
Tracker.initialize(this)
|
||||
|
||||
val processName = currentProcessName
|
||||
extractGeoFiles()
|
||||
|
||||
Log.d("Process $processName started")
|
||||
|
||||
if (processName == packageName) {
|
||||
Remote.launch()
|
||||
setupShortcuts()
|
||||
} else {
|
||||
sendServiceRecreated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupShortcuts() {
|
||||
val icon = IconCompat.createWithResource(this, R.mipmap.ic_launcher)
|
||||
val flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or
|
||||
Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||
|
||||
val toggle = ShortcutInfoCompat.Builder(this, "toggle_clash")
|
||||
.setShortLabel(getString(DesignR.string.shortcut_toggle_short))
|
||||
.setLongLabel(getString(DesignR.string.shortcut_toggle_long))
|
||||
.setIcon(icon)
|
||||
.setIntent(
|
||||
Intent(Intents.ACTION_TOGGLE_CLASH)
|
||||
.setClassName(this, ExternalControlActivity::class.java.name)
|
||||
.addFlags(flags)
|
||||
)
|
||||
.setRank(0)
|
||||
.build()
|
||||
|
||||
val start = ShortcutInfoCompat.Builder(this, "start_clash")
|
||||
.setShortLabel(getString(DesignR.string.shortcut_start_short))
|
||||
.setLongLabel(getString(DesignR.string.shortcut_start_long))
|
||||
.setIcon(icon)
|
||||
.setIntent(
|
||||
Intent(Intents.ACTION_START_CLASH)
|
||||
.setClassName(this, ExternalControlActivity::class.java.name)
|
||||
.addFlags(flags)
|
||||
)
|
||||
.setRank(1)
|
||||
.build()
|
||||
|
||||
val stop = ShortcutInfoCompat.Builder(this, "stop_clash")
|
||||
.setShortLabel(getString(DesignR.string.shortcut_stop_short))
|
||||
.setLongLabel(getString(DesignR.string.shortcut_stop_long))
|
||||
.setIcon(icon)
|
||||
.setIntent(
|
||||
Intent(Intents.ACTION_STOP_CLASH)
|
||||
.setClassName(this, ExternalControlActivity::class.java.name)
|
||||
.addFlags(flags)
|
||||
)
|
||||
.setRank(2)
|
||||
.build()
|
||||
|
||||
ShortcutManagerCompat.setDynamicShortcuts(this, listOf(toggle, start, stop))
|
||||
}
|
||||
|
||||
private fun extractGeoFiles() {
|
||||
clashDir.mkdirs()
|
||||
|
||||
val updateDate = packageManager.getPackageInfo(packageName, 0).lastUpdateTime
|
||||
val geoipFile = File(clashDir, "geoip.metadb")
|
||||
if (geoipFile.exists() && geoipFile.lastModified() < updateDate) {
|
||||
geoipFile.delete()
|
||||
}
|
||||
if (!geoipFile.exists()) {
|
||||
FileOutputStream(geoipFile).use {
|
||||
assets.open("geoip.metadb").copyTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
val geositeFile = File(clashDir, "geosite.dat")
|
||||
if (geositeFile.exists() && geositeFile.lastModified() < updateDate) {
|
||||
geositeFile.delete()
|
||||
}
|
||||
if (!geositeFile.exists()) {
|
||||
FileOutputStream(geositeFile).use {
|
||||
assets.open("geosite.dat").copyTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
val asnFile = File(clashDir, "ASN.mmdb")
|
||||
if (asnFile.exists() && asnFile.lastModified() < updateDate) {
|
||||
asnFile.delete()
|
||||
}
|
||||
if (!asnFile.exists()) {
|
||||
FileOutputStream(asnFile).use {
|
||||
assets.open("ASN.mmdb").copyTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun finalize() {
|
||||
Global.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.github.kr328.clash.core.Clash
|
||||
import com.github.kr328.clash.design.MetaFeatureSettingsDesign
|
||||
import com.github.kr328.clash.util.clashDir
|
||||
import com.github.kr328.clash.util.withClash
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
|
||||
class MetaFeatureSettingsActivity : BaseActivity<MetaFeatureSettingsDesign>() {
|
||||
override suspend fun main() {
|
||||
val configuration = withClash { queryOverride(Clash.OverrideSlot.Persist) }
|
||||
|
||||
defer {
|
||||
withClash {
|
||||
patchOverride(Clash.OverrideSlot.Persist, configuration)
|
||||
}
|
||||
}
|
||||
|
||||
val design = MetaFeatureSettingsDesign(
|
||||
this,
|
||||
configuration
|
||||
)
|
||||
|
||||
setContentDesign(design)
|
||||
|
||||
while (isActive) {
|
||||
select<Unit> {
|
||||
events.onReceive {
|
||||
|
||||
}
|
||||
design.requests.onReceive {
|
||||
when (it) {
|
||||
MetaFeatureSettingsDesign.Request.ResetOverride -> {
|
||||
if (design.requestResetConfirm()) {
|
||||
defer {
|
||||
withClash {
|
||||
clearOverride(Clash.OverrideSlot.Persist)
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
MetaFeatureSettingsDesign.Request.ImportGeoIp -> {
|
||||
val uri = startActivityForResult(
|
||||
ActivityResultContracts.GetContent(),
|
||||
"*/*")
|
||||
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportGeoIp)
|
||||
}
|
||||
MetaFeatureSettingsDesign.Request.ImportGeoSite -> {
|
||||
val uri = startActivityForResult(
|
||||
ActivityResultContracts.GetContent(),
|
||||
"*/*")
|
||||
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportGeoSite)
|
||||
}
|
||||
MetaFeatureSettingsDesign.Request.ImportCountry -> {
|
||||
val uri = startActivityForResult(
|
||||
ActivityResultContracts.GetContent(),
|
||||
"*/*")
|
||||
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportCountry)
|
||||
}
|
||||
MetaFeatureSettingsDesign.Request.ImportASN -> {
|
||||
val uri = startActivityForResult(
|
||||
ActivityResultContracts.GetContent(),
|
||||
"*/*")
|
||||
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportASN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val validDatabaseExtensions = listOf(
|
||||
".metadb", ".db", ".dat", ".mmdb"
|
||||
)
|
||||
|
||||
private suspend fun importGeoFile(uri: Uri?, importType: MetaFeatureSettingsDesign.Request) {
|
||||
val cursor: Cursor? = uri?.let {
|
||||
contentResolver.query(it, null, null, null, null, null)
|
||||
}
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val columnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
val displayName: String =
|
||||
if (columnIndex != -1) it.getString(columnIndex) else "";
|
||||
val ext = "." + displayName.substringAfterLast(".")
|
||||
|
||||
if (!validDatabaseExtensions.contains(ext)) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.geofile_unknown_db_format)
|
||||
.setMessage(getString(R.string.geofile_unknown_db_format_message,
|
||||
validDatabaseExtensions.joinToString("/")))
|
||||
.setPositiveButton("OK") { _, _ -> }
|
||||
.show()
|
||||
return
|
||||
}
|
||||
val outputFileName = when (importType) {
|
||||
MetaFeatureSettingsDesign.Request.ImportGeoIp ->
|
||||
"geoip$ext"
|
||||
MetaFeatureSettingsDesign.Request.ImportGeoSite ->
|
||||
"geosite$ext"
|
||||
MetaFeatureSettingsDesign.Request.ImportCountry ->
|
||||
"country$ext"
|
||||
MetaFeatureSettingsDesign.Request.ImportASN ->
|
||||
"ASN$ext"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val outputFile = File(clashDir, outputFileName);
|
||||
contentResolver.openInputStream(uri).use { ins ->
|
||||
FileOutputStream(outputFile).use { outs ->
|
||||
ins?.copyTo(outs)
|
||||
}
|
||||
}
|
||||
}
|
||||
Toast.makeText(this, getString(R.string.geofile_imported, displayName),
|
||||
Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
}
|
||||
Toast.makeText(this, R.string.geofile_import_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,25 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.kr328.clash.common.constants.Intents
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.common.util.setUUID
|
||||
import com.github.kr328.clash.design.NewProfileDesign
|
||||
import com.github.kr328.clash.design.R
|
||||
import com.github.kr328.clash.design.model.ProfileProvider
|
||||
import com.github.kr328.clash.design.util.showExceptionToast
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
import com.github.kr328.clash.util.withProfile
|
||||
import io.github.g00fy2.quickie.QRResult
|
||||
import io.github.g00fy2.quickie.QRResult.QRError
|
||||
import io.github.g00fy2.quickie.QRResult.QRMissingPermission
|
||||
import io.github.g00fy2.quickie.QRResult.QRSuccess
|
||||
import io.github.g00fy2.quickie.QRResult.QRUserCanceled
|
||||
import io.github.g00fy2.quickie.ScanQRCode
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
@@ -23,6 +33,8 @@ class NewProfileActivity : BaseActivity<NewProfileDesign>() {
|
||||
private val self: NewProfileActivity
|
||||
get() = this
|
||||
|
||||
private val scanLauncher = registerForActivityResult(ScanQRCode(), ::scanResultHandler)
|
||||
|
||||
override suspend fun main() {
|
||||
val design = NewProfileDesign(this)
|
||||
|
||||
@@ -44,8 +56,14 @@ class NewProfileActivity : BaseActivity<NewProfileDesign>() {
|
||||
val uuid: UUID? = when (val p = it.provider) {
|
||||
is ProfileProvider.File ->
|
||||
create(Profile.Type.File, name)
|
||||
|
||||
is ProfileProvider.Url ->
|
||||
create(Profile.Type.Url, name)
|
||||
|
||||
is ProfileProvider.QR -> {
|
||||
null
|
||||
}
|
||||
|
||||
is ProfileProvider.External -> {
|
||||
val data = p.get()
|
||||
|
||||
@@ -67,9 +85,14 @@ class NewProfileActivity : BaseActivity<NewProfileDesign>() {
|
||||
launchProperties(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
is NewProfileDesign.Request.OpenDetail -> {
|
||||
launchAppDetailed(it.provider)
|
||||
}
|
||||
|
||||
is NewProfileDesign.Request.LaunchScanner -> {
|
||||
scanLauncher.launch(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,7 +160,41 @@ class NewProfileActivity : BaseActivity<NewProfileDesign>() {
|
||||
ProfileProvider.External(name.toString(), summary.toString(), icon, intent)
|
||||
}
|
||||
|
||||
listOf(ProfileProvider.File(self), ProfileProvider.Url(self)) + providers
|
||||
listOf(
|
||||
ProfileProvider.File(self),
|
||||
ProfileProvider.Url(self),
|
||||
ProfileProvider.QR(self)
|
||||
) + providers
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanResultHandler(result: QRResult) {
|
||||
lifecycleScope.launch {
|
||||
when (result) {
|
||||
is QRSuccess -> {
|
||||
val url = result.content.rawValue
|
||||
?: result.content.rawBytes?.let { String(it) }.orEmpty()
|
||||
|
||||
createProfileByQrCode(url)
|
||||
}
|
||||
|
||||
QRUserCanceled -> {}
|
||||
QRMissingPermission -> design?.showExceptionToast(getString(R.string.import_from_qr_no_permission))
|
||||
is QRError -> design?.showExceptionToast(getString(R.string.import_from_qr_exception))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createProfileByQrCode(url: String) {
|
||||
withProfile {
|
||||
launchProperties(
|
||||
create(
|
||||
type = Profile.Type.Url,
|
||||
name = getString(R.string.new_profile),
|
||||
url,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,45 +45,14 @@ class OverrideSettingsActivity : BaseActivity<OverrideSettingsDesign>() {
|
||||
withClash {
|
||||
clearOverride(Clash.OverrideSlot.Persist)
|
||||
}
|
||||
|
||||
service.sideloadGeoip = ""
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
OverrideSettingsDesign.Request.EditSideloadGeoip -> {
|
||||
withContext(Dispatchers.IO) {
|
||||
val list = querySideloadProviders()
|
||||
val initial = service.sideloadGeoip
|
||||
val exist = list.any { info -> info.packageName == initial }
|
||||
|
||||
service.sideloadGeoip =
|
||||
design.requestSelectSideload(if (exist) initial else "", list)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun querySideloadProviders(): List<AppInfo> {
|
||||
val apps = packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
|
||||
.filter {
|
||||
it.applicationInfo.metaData?.containsKey(Metadata.GEOIP_FILE_NAME)
|
||||
?: false
|
||||
}
|
||||
.map { it.toAppInfo(packageManager) }
|
||||
|
||||
return listOf(
|
||||
AppInfo(
|
||||
packageName = "",
|
||||
label = getString(R.string.use_built_in),
|
||||
icon = getDrawableCompat(R.drawable.ic_baseline_work)!!,
|
||||
installTime = 0,
|
||||
updateDate = 0,
|
||||
)
|
||||
) + apps
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.common.util.setUUID
|
||||
import com.github.kr328.clash.common.util.ticker
|
||||
import com.github.kr328.clash.design.ProfilesDesign
|
||||
import com.github.kr328.clash.design.ui.ToastDuration
|
||||
import com.github.kr328.clash.service.model.Profile
|
||||
import com.github.kr328.clash.util.withProfile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
class ProfilesActivity : BaseActivity<ProfilesDesign>() {
|
||||
override suspend fun main() {
|
||||
@@ -34,9 +44,16 @@ class ProfilesActivity : BaseActivity<ProfilesDesign>() {
|
||||
startActivity(NewProfileActivity::class.intent)
|
||||
ProfilesDesign.Request.UpdateAll ->
|
||||
withProfile {
|
||||
queryAll().forEach { p ->
|
||||
if (p.imported && p.type != Profile.Type.File)
|
||||
update(p.uuid)
|
||||
try {
|
||||
queryAll().forEach { p ->
|
||||
if (p.imported && p.type != Profile.Type.File)
|
||||
update(p.uuid)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
design.finishUpdateAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
is ProfilesDesign.Request.Update ->
|
||||
@@ -74,4 +91,37 @@ class ProfilesActivity : BaseActivity<ProfilesDesign>() {
|
||||
patchProfiles(queryAll())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProfileUpdateCompleted(uuid: UUID?) {
|
||||
if(uuid == null)
|
||||
return;
|
||||
launch {
|
||||
var name: String? = null;
|
||||
withProfile {
|
||||
name = queryByUUID(uuid)?.name
|
||||
}
|
||||
design?.showToast(
|
||||
getString(R.string.toast_profile_updated_complete, name),
|
||||
ToastDuration.Long
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun onProfileUpdateFailed(uuid: UUID?, reason: String?) {
|
||||
if(uuid == null)
|
||||
return;
|
||||
launch {
|
||||
var name: String? = null;
|
||||
withProfile {
|
||||
name = queryByUUID(uuid)?.name
|
||||
}
|
||||
design?.showToast(
|
||||
getString(R.string.toast_profile_updated_failed, name, reason),
|
||||
ToastDuration.Long
|
||||
){
|
||||
setAction(R.string.edit) {
|
||||
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,11 @@ import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
class PropertiesActivity : BaseActivity<PropertiesDesign>() {
|
||||
private var canceled: Boolean = false
|
||||
private lateinit var original: Profile
|
||||
|
||||
override suspend fun main() {
|
||||
setResult(RESULT_CANCELED)
|
||||
@@ -22,7 +24,7 @@ class PropertiesActivity : BaseActivity<PropertiesDesign>() {
|
||||
val uuid = intent.uuid ?: return finish()
|
||||
val design = PropertiesDesign(this)
|
||||
|
||||
val original = withProfile { queryByUUID(uuid) } ?: return finish()
|
||||
original = withProfile { queryByUUID(uuid) } ?: return finish()
|
||||
|
||||
design.profile = original
|
||||
|
||||
@@ -71,7 +73,7 @@ class PropertiesActivity : BaseActivity<PropertiesDesign>() {
|
||||
design?.apply {
|
||||
launch {
|
||||
if (!progressing) {
|
||||
if (requestExitWithoutSaving())
|
||||
if (original == profile || requestExitWithoutSaving())
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.github.kr328.clash.design.R
|
||||
|
||||
class ProvidersActivity : BaseActivity<ProvidersDesign>() {
|
||||
override suspend fun main() {
|
||||
|
||||
@@ -5,15 +5,12 @@ import com.github.kr328.clash.core.Clash
|
||||
import com.github.kr328.clash.core.model.Proxy
|
||||
import com.github.kr328.clash.design.ProxyDesign
|
||||
import com.github.kr328.clash.design.model.ProxyState
|
||||
import com.github.kr328.clash.store.TipsStore
|
||||
import com.github.kr328.clash.util.withClash
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ProxyActivity : BaseActivity<ProxyDesign>() {
|
||||
override suspend fun main() {
|
||||
@@ -22,7 +19,6 @@ class ProxyActivity : BaseActivity<ProxyDesign>() {
|
||||
val states = List(names.size) { ProxyState("?") }
|
||||
val unorderedStates = names.indices.map { names[it] to states[it] }.toMap()
|
||||
val reloadLock = Semaphore(10)
|
||||
val tips = TipsStore(this)
|
||||
|
||||
val design = ProxyDesign(
|
||||
this,
|
||||
@@ -33,17 +29,6 @@ class ProxyActivity : BaseActivity<ProxyDesign>() {
|
||||
|
||||
setContentDesign(design)
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val pkg = packageManager.getPackageInfo(packageName, 0)
|
||||
val validate = System.currentTimeMillis() - pkg.firstInstallTime > TimeUnit.DAYS.toMillis(5)
|
||||
|
||||
if (tips.requestDonate && validate) {
|
||||
tips.requestDonate = false
|
||||
|
||||
design.requestDonate()
|
||||
}
|
||||
}
|
||||
|
||||
design.requests.send(ProxyDesign.Request.ReloadAll)
|
||||
|
||||
while (isActive) {
|
||||
@@ -73,7 +58,7 @@ class ProxyActivity : BaseActivity<ProxyDesign>() {
|
||||
}
|
||||
ProxyDesign.Request.ReloadAll -> {
|
||||
names.indices.forEach { idx ->
|
||||
design.requests.offer(ProxyDesign.Request.Reload(idx))
|
||||
design.requests.trySend(ProxyDesign.Request.Reload(idx))
|
||||
}
|
||||
}
|
||||
is ProxyDesign.Request.Reload -> {
|
||||
|
||||
@@ -24,6 +24,8 @@ class SettingsActivity : BaseActivity<SettingsDesign>() {
|
||||
startActivity(NetworkSettingsActivity::class.intent)
|
||||
SettingsDesign.Request.StartOverride ->
|
||||
startActivity(OverrideSettingsActivity::class.intent)
|
||||
SettingsDesign.Request.StartMetaFeature ->
|
||||
startActivity(MetaFeatureSettingsActivity::class.intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.github.kr328.clash.common.compat.registerReceiverCompat
|
||||
import com.github.kr328.clash.common.constants.Intents
|
||||
import com.github.kr328.clash.common.constants.Permissions
|
||||
import com.github.kr328.clash.remote.StatusClient
|
||||
import com.github.kr328.clash.util.startClashService
|
||||
import com.github.kr328.clash.util.stopClashService
|
||||
import com.github.kr328.clash.service.R
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class TileService : TileService() {
|
||||
@@ -36,7 +38,7 @@ class TileService : TileService() {
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
|
||||
registerReceiver(
|
||||
registerReceiverCompat(
|
||||
receiver,
|
||||
IntentFilter().apply {
|
||||
addAction(Intents.ACTION_CLASH_STARTED)
|
||||
|
||||
@@ -16,16 +16,28 @@ class LogcatReader(context: Context, file: LogFile) : AutoCloseable {
|
||||
}
|
||||
|
||||
fun readAll(): List<LogMessage> {
|
||||
var lastTime = Date(0)
|
||||
return reader.lineSequence()
|
||||
.map { it.trim() }
|
||||
.filter { !it.startsWith("#") }
|
||||
.map { it.split(":", limit = 3) }
|
||||
.map {
|
||||
LogMessage(
|
||||
time = Date(it[0].toLong()),
|
||||
level = LogMessage.Level.valueOf(it[1]),
|
||||
message = it[2]
|
||||
)
|
||||
val time = it[0].toLongOrNull()?.let { Date(it) } ?: lastTime
|
||||
val logMessage = if (it[0].toLongOrNull() != null) {
|
||||
LogMessage(
|
||||
time = time,
|
||||
level = LogMessage.Level.valueOf(it[1]),
|
||||
message = it[2]
|
||||
)
|
||||
} else {
|
||||
LogMessage(
|
||||
time = time,
|
||||
level = LogMessage.Level.Warning, // or any default level
|
||||
message = it.joinToString(":")
|
||||
)
|
||||
}
|
||||
lastTime = time
|
||||
logMessage
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ object SystemLogcat {
|
||||
"Go",
|
||||
"DEBUG",
|
||||
"AndroidRuntime",
|
||||
"ClashForAndroid"
|
||||
"ClashMetaForAndroid",
|
||||
"LwIP",
|
||||
)
|
||||
|
||||
fun dumpCrash(): String {
|
||||
|
||||
@@ -5,8 +5,10 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.github.kr328.clash.common.compat.registerReceiverCompat
|
||||
import com.github.kr328.clash.common.constants.Intents
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import java.util.*
|
||||
|
||||
class Broadcasts(private val context: Application) {
|
||||
interface Observer {
|
||||
@@ -14,6 +16,8 @@ class Broadcasts(private val context: Application) {
|
||||
fun onStarted()
|
||||
fun onStopped(cause: String?)
|
||||
fun onProfileChanged()
|
||||
fun onProfileUpdateCompleted(uuid: UUID?)
|
||||
fun onProfileUpdateFailed(uuid: UUID?, reason: String?)
|
||||
fun onProfileLoaded()
|
||||
}
|
||||
|
||||
@@ -52,6 +56,17 @@ class Broadcasts(private val context: Application) {
|
||||
receivers.forEach {
|
||||
it.onProfileChanged()
|
||||
}
|
||||
Intents.ACTION_PROFILE_UPDATE_COMPLETED ->
|
||||
receivers.forEach {
|
||||
it.onProfileUpdateCompleted(
|
||||
UUID.fromString(intent.getStringExtra(Intents.EXTRA_UUID)))
|
||||
}
|
||||
Intents.ACTION_PROFILE_UPDATE_FAILED ->
|
||||
receivers.forEach {
|
||||
it.onProfileUpdateFailed(
|
||||
UUID.fromString(intent.getStringExtra(Intents.EXTRA_UUID)),
|
||||
intent.getStringExtra(Intents.EXTRA_FAIL_REASON))
|
||||
}
|
||||
Intents.ACTION_PROFILE_LOADED -> {
|
||||
receivers.forEach {
|
||||
it.onProfileLoaded()
|
||||
@@ -74,11 +89,13 @@ class Broadcasts(private val context: Application) {
|
||||
return
|
||||
|
||||
try {
|
||||
context.registerReceiver(broadcastReceiver, IntentFilter().apply {
|
||||
context.registerReceiverCompat(broadcastReceiver, IntentFilter().apply {
|
||||
addAction(Intents.ACTION_SERVICE_RECREATED)
|
||||
addAction(Intents.ACTION_CLASH_STARTED)
|
||||
addAction(Intents.ACTION_CLASH_STOPPED)
|
||||
addAction(Intents.ACTION_PROFILE_CHANGED)
|
||||
addAction(Intents.ACTION_PROFILE_UPDATE_COMPLETED)
|
||||
addAction(Intents.ACTION_PROFILE_UPDATE_FAILED)
|
||||
addAction(Intents.ACTION_PROFILE_LOADED)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,18 +5,18 @@ import android.content.Intent
|
||||
import com.github.kr328.clash.ApkBrokenActivity
|
||||
import com.github.kr328.clash.AppCrashedActivity
|
||||
import com.github.kr328.clash.common.Global
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.store.AppStore
|
||||
import com.github.kr328.clash.util.ApplicationObserver
|
||||
import com.github.kr328.clash.util.verifyApk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object Remote {
|
||||
val broadcasts: Broadcasts = Broadcasts(Global.application)
|
||||
val services: Services = Services(Global.application) {
|
||||
val service: Service = Service(Global.application) {
|
||||
ApplicationObserver.createdActivities.forEach { it.finish() }
|
||||
|
||||
val intent = AppCrashedActivity::class.intent
|
||||
@@ -30,14 +30,25 @@ object Remote {
|
||||
fun launch() {
|
||||
ApplicationObserver.attach(Global.application)
|
||||
|
||||
ApplicationObserver.onVisibleChanged(visible::offer)
|
||||
ApplicationObserver.onVisibleChanged {
|
||||
if(it) {
|
||||
Log.d("App becomes visible")
|
||||
service.bind()
|
||||
broadcasts.register()
|
||||
}
|
||||
else {
|
||||
Log.d("App becomes invisible")
|
||||
service.unbind()
|
||||
broadcasts.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
run()
|
||||
Global.launch(Dispatchers.IO) {
|
||||
verifyApp()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun run() {
|
||||
private suspend fun verifyApp() {
|
||||
val context = Global.application
|
||||
val store = AppStore(context)
|
||||
val updatedAt = getLastUpdated(context)
|
||||
@@ -54,16 +65,6 @@ object Remote {
|
||||
store.updatedAt = updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (visible.receive()) {
|
||||
services.bind()
|
||||
broadcasts.register()
|
||||
} else {
|
||||
services.unbind()
|
||||
broadcasts.unregister()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLastUpdated(context: Context): Long {
|
||||
|
||||
59
app/src/main/java/com/github/kr328/clash/remote/Service.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.github.kr328.clash.remote
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.service.RemoteService
|
||||
import com.github.kr328.clash.service.remote.IRemoteService
|
||||
import com.github.kr328.clash.service.remote.unwrap
|
||||
import com.github.kr328.clash.util.unbindServiceSilent
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class Service(private val context: Application, val crashed: () -> Unit) {
|
||||
val remote = Resource<IRemoteService>()
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
private var lastCrashed: Long = -1
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
||||
remote.set(service.unwrap(IRemoteService::class))
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
remote.set(null)
|
||||
|
||||
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
|
||||
unbind()
|
||||
|
||||
crashed()
|
||||
}
|
||||
|
||||
lastCrashed = System.currentTimeMillis()
|
||||
Log.w("RemoteService killed or crashed")
|
||||
}
|
||||
}
|
||||
|
||||
fun bind() {
|
||||
try {
|
||||
context.bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
|
||||
} catch (e: Exception) {
|
||||
unbind()
|
||||
|
||||
crashed()
|
||||
}
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
context.unbindServiceSilent(connection)
|
||||
|
||||
remote.set(null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TOGGLE_CRASHED_INTERVAL = TimeUnit.SECONDS.toMillis(10)
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package com.github.kr328.clash.remote
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import com.github.kr328.clash.common.util.intent
|
||||
import com.github.kr328.clash.service.ClashManager
|
||||
import com.github.kr328.clash.service.ProfileService
|
||||
import com.github.kr328.clash.service.remote.IClashManager
|
||||
import com.github.kr328.clash.service.remote.IProfileManager
|
||||
import com.github.kr328.clash.service.remote.unwrap
|
||||
import com.github.kr328.clash.util.unbindServiceSilent
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class Services(private val context: Application, val crashed: () -> Unit) {
|
||||
val clash = Resource<IClashManager>()
|
||||
val profile = Resource<IProfileManager>()
|
||||
|
||||
private val clashConnection = object : ServiceConnection {
|
||||
private var lastCrashed: Long = -1
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
clash.set(service?.unwrap(IClashManager::class))
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
clash.set(null)
|
||||
|
||||
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
|
||||
unbind()
|
||||
|
||||
crashed()
|
||||
}
|
||||
|
||||
lastCrashed = System.currentTimeMillis()
|
||||
|
||||
Log.w("ClashManager crashed")
|
||||
}
|
||||
}
|
||||
|
||||
private val profileConnection = object : ServiceConnection {
|
||||
private var lastCrashed: Long = -1
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
profile.set(service?.unwrap(IProfileManager::class))
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
profile.set(null)
|
||||
|
||||
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
|
||||
unbind()
|
||||
|
||||
crashed()
|
||||
}
|
||||
|
||||
lastCrashed = System.currentTimeMillis()
|
||||
|
||||
Log.w("ProfileService crashed")
|
||||
}
|
||||
}
|
||||
|
||||
fun bind() {
|
||||
try {
|
||||
context.bindService(ClashManager::class.intent, clashConnection, Context.BIND_AUTO_CREATE)
|
||||
context.bindService(ProfileService::class.intent, profileConnection, Context.BIND_AUTO_CREATE)
|
||||
} catch (e: Exception) {
|
||||
unbind()
|
||||
|
||||
crashed()
|
||||
}
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
context.unbindServiceSilent(clashConnection)
|
||||
context.unbindServiceSilent(profileConnection)
|
||||
|
||||
clash.set(null)
|
||||
profile.set(null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TOGGLE_CRASHED_INTERVAL = TimeUnit.SECONDS.toMillis(10)
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,12 @@ import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ActivityResultLifecycle : LifecycleOwner {
|
||||
private val lifecycle = LifecycleRegistry(this)
|
||||
override val lifecycle = LifecycleRegistry(this)
|
||||
|
||||
init {
|
||||
lifecycle.currentState = Lifecycle.State.INITIALIZED
|
||||
}
|
||||
|
||||
override fun getLifecycle(): Lifecycle {
|
||||
return lifecycle
|
||||
}
|
||||
|
||||
suspend fun <T> use(block: suspend (lifecycle: ActivityResultLifecycle, start: () -> Unit) -> T): T {
|
||||
return try {
|
||||
markCreated()
|
||||
|
||||
@@ -9,7 +9,8 @@ import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
object ApplicationObserver {
|
||||
private val activities: MutableSet<Activity> = mutableSetOf()
|
||||
private val _createdActivities: MutableSet<Activity> = mutableSetOf()
|
||||
private val _visibleActivities: MutableSet<Activity> = mutableSetOf()
|
||||
|
||||
private var visibleChanged: (Boolean) -> Unit = {}
|
||||
|
||||
@@ -23,25 +24,31 @@ object ApplicationObserver {
|
||||
}
|
||||
|
||||
val createdActivities: Set<Activity>
|
||||
get() = activities
|
||||
get() = _createdActivities
|
||||
|
||||
private val activityObserver = object : Application.ActivityLifecycleCallbacks {
|
||||
@Synchronized
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
activities.add(activity)
|
||||
|
||||
appVisible = true
|
||||
_createdActivities.add(activity)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
activities.remove(activity)
|
||||
|
||||
appVisible = activities.isNotEmpty()
|
||||
_createdActivities.remove(activity)
|
||||
_visibleActivities.remove(activity)
|
||||
appVisible = _visibleActivities.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
_visibleActivities.add(activity)
|
||||
appVisible = true
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
_visibleActivities.remove(activity)
|
||||
appVisible = _visibleActivities.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
|
||||
@@ -4,4 +4,7 @@ import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
val Context.logsDir: File
|
||||
get() = cacheDir.resolve("logs")
|
||||
get() = cacheDir.resolve("logs")
|
||||
|
||||
val Context.clashDir: File
|
||||
get() = filesDir.resolve("clash")
|
||||
@@ -14,14 +14,15 @@ suspend fun <T> withClash(
|
||||
block: suspend IClashManager.() -> T
|
||||
): T {
|
||||
while (true) {
|
||||
val client = Remote.services.clash.get()
|
||||
val remote = Remote.service.remote.get()
|
||||
val client = remote.clash()
|
||||
|
||||
try {
|
||||
return withContext(context) { client.block() }
|
||||
} catch (e: DeadObjectException) {
|
||||
Log.w("Remote services panic")
|
||||
|
||||
Remote.services.clash.reset(client)
|
||||
Remote.service.remote.reset(remote)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,14 +32,15 @@ suspend fun <T> withProfile(
|
||||
block: suspend IProfileManager.() -> T
|
||||
): T {
|
||||
while (true) {
|
||||
val client = Remote.services.profile.get()
|
||||
val remote = Remote.service.remote.get()
|
||||
val client = remote.profile()
|
||||
|
||||
try {
|
||||
return withContext(context) { client.block() }
|
||||
} catch (e: DeadObjectException) {
|
||||
Log.w("Remote services panic")
|
||||
|
||||
Remote.services.profile.reset(client)
|
||||
Remote.service.remote.reset(remote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="406.92642"
|
||||
android:viewportHeight="406.92642">
|
||||
<group
|
||||
android:translateX="103.4632"
|
||||
android:translateY="103.4632">
|
||||
android:viewportWidth="403"
|
||||
android:viewportHeight="403"
|
||||
android:name="vector">
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#1E4376"
|
||||
android:pathData="M47.211,168.128C70.531,-34.962 67.471,13.788 94.071,43.818c13.45,-1.52 27.24,-3.47 40.82,-0.67c2.64,0.13 5.42,1.86 7.71,0.18c4.12,-6.27 7.35,-13.54 11.35,-20c12.19,-24.44 12.85,19.54 15.48,26.52c5.23,32.99 10.89,64.46 14.67,97.59c0.31,10.72 5.74,32.92 1.08,33.56c-49.36,5.23 -147.71,3.91 -160.84,-6.3c-15.85,-10.5 -15.18,-35.33 2.03,-43.72c3.63,-2.03 10.68,-3.72 11.94,0.7c-2.41,4.99 -8.79,5.77 -12.12,11.17C16.621,158.948 33.111,168.888 47.211,168.128zM87.841,74.008c-10.42,0.52 -9.59,14.89 -0.07,15.18C98.191,88.668 97.361,74.298 87.841,74.008zM149.121,89.188c10.46,-0.34 9.85,-14.71 0.38,-15.18C139.031,74.348 139.651,88.718 149.121,89.188zM107.871,99.228c2.16,3.48 5.28,3.29 9.79,0.16c3.81,3.17 8.06,3.28 9.18,-0.19c-3.78,1.17 -7.04,0.79 -9.4,-3.49C115.371,100.108 112.071,100.428 107.871,99.228z"
|
||||
tools:ignore="VectorPath" />
|
||||
android:pathData="M 141.08 128.47 C 141.88 128.42 142.72 128.38 143.41 128.8 C 144.51 129.46 145.39 130.41 146.33 131.25 C 160.38 144.1 174.48 156.9 188.53 169.75 C 189.48 170.59 190.37 171.56 191.54 172.11 C 192.45 172.55 193.49 172.33 194.46 172.2 C 197.04 171.76 199.66 171.74 202.26 171.74 C 204.77 171.78 207.29 171.86 209.77 172.27 C 210.77 172.38 211.95 172.71 212.71 171.84 C 228.19 157.7 243.73 143.58 259.23 129.43 C 260.54 128.06 262.55 128.36 264.26 128.57 C 268.58 129.21 272.94 129.51 277.28 130 C 278 130.14 279.02 130.19 279.27 131.05 C 279.61 132.54 279.42 134.07 279.46 135.58 C 279.44 166.17 279.44 196.77 279.42 227.36 C 279.41 228.92 279.51 230.47 279.35 232.01 C 279.29 232.56 279.22 233.21 278.76 233.59 C 277.8 234.01 276.74 234.17 275.72 234.42 C 271.39 235.43 266.98 236.16 262.66 237.26 C 260.49 237.83 258.25 237.61 256.04 237.64 C 254.91 237.63 253.74 237.73 252.61 237.54 C 251.95 237.49 251.48 236.9 251.43 236.26 C 251.19 234.74 251.33 233.2 251.31 231.67 C 251.29 216.61 251.28 201.54 251.29 186.48 C 251.27 185 251.38 183.49 251.17 182.01 C 250.9 180.3 249.11 179.1 247.44 179.31 C 246.51 179.32 245.72 179.9 245.05 180.49 C 237.57 187.38 230 194.15 222.54 201.02 C 221.68 201.76 220.7 202.63 219.48 202.51 C 217.88 202.37 216.37 201.8 214.82 201.44 C 208.61 199.89 202.12 199.7 195.77 200.24 C 192.68 200.67 189.56 201.02 186.59 202 C 185.63 202.27 184.66 202.57 183.67 202.52 C 182.65 202.45 181.86 201.78 181.14 201.15 C 173.72 194.33 166.23 187.63 158.81 180.81 C 158.09 180.18 157.31 179.49 156.33 179.34 C 155.1 179.19 153.83 179.69 152.99 180.59 C 152.26 181.42 152.23 182.56 152.19 183.61 C 152.23 200.72 152.18 217.81 152.21 234.91 C 152.18 235.57 152.18 236.24 151.92 236.87 C 151.53 237.58 150.62 237.61 149.91 237.64 C 147.99 237.68 146.08 237.63 144.16 237.64 C 141.99 237.68 139.9 237.05 137.81 236.62 C 133.97 235.77 130.14 234.91 126.29 234.1 C 125.48 233.9 124.35 233.66 124.2 232.66 C 123.96 230.91 124.13 229.14 124.1 227.36 C 124.06 196.88 124.08 166.41 124.08 135.93 C 124.08 134.49 124 133.06 124.13 131.64 C 124.18 131.12 124.33 130.51 124.84 130.24 C 125.38 130 125.99 129.97 126.56 129.88 C 131.42 129.55 136.25 128.96 141.08 128.47 Z M 198.81 240.82 C 200.99 240.64 203.19 240.7 205.35 240.82 C 207.04 240.8 208.08 242.85 207.31 244.28 C 206.36 246.17 205.38 248.03 204.27 249.84 C 203.56 251.14 201.5 251.39 200.57 250.19 C 199.67 249.09 199.15 247.78 198.44 246.56 C 197.9 245.55 197.21 244.59 196.96 243.44 C 196.7 242.24 197.62 240.99 198.81 240.82 Z"
|
||||
android:fillColor="#3372b6"/>
|
||||
<path
|
||||
android:pathData="M 125.67 244.75 C 126.09 244.7 126.51 244.67 126.95 244.67 C 139.02 244.7 151.09 244.69 163.19 244.69 C 164.26 244.67 165.5 244.94 166.14 245.92 C 166.8 247.02 166.66 248.72 165.52 249.45 C 164.74 249.99 163.74 249.99 162.83 250.02 C 150.88 249.99 138.9 250.01 126.95 250.02 C 126.28 249.99 125.6 249.99 124.97 249.77 C 123.94 249.38 123.25 248.25 123.4 247.15 C 123.49 245.97 124.43 244.86 125.67 244.76 L 125.67 244.75 Z M 239.67 244.75 C 241.29 244.59 242.93 244.72 244.57 244.69 C 255.33 244.67 266.1 244.72 276.86 244.67 C 277.55 244.7 278.27 244.69 278.92 244.97 C 279.88 245.4 280.42 246.44 280.42 247.47 C 280.44 248.67 279.44 249.8 278.24 249.92 C 276.7 250.11 275.15 249.99 273.61 250 L 243.85 250 C 242.33 249.99 240.79 250.12 239.29 249.87 C 238.15 249.67 237.23 248.64 237.31 247.46 C 237.16 246.07 238.34 244.85 239.67 244.75 Z M 162.61 257.67 C 163.41 257.45 164.3 257.33 165.06 257.72 C 166.65 258.45 167.02 260.98 165.6 262.08 C 164.76 262.67 163.74 262.91 162.8 263.24 C 159.22 264.36 155.74 265.76 152.16 266.87 C 148.04 268.17 144 269.71 139.9 271.09 C 136.91 272.06 133.94 273.07 130.97 274.1 C 129.73 274.52 128.54 275.06 127.27 275.33 C 126.49 275.5 125.63 275.35 125.02 274.83 C 124.01 274.03 123.82 272.5 124.4 271.4 C 124.87 270.6 125.82 270.32 126.63 270 C 129.2 269.1 131.73 268.17 134.31 267.36 C 140.78 265.27 147.14 262.84 153.63 260.8 C 156.65 259.81 159.59 258.62 162.61 257.67 Z M 239.3 257.54 C 240.23 257.32 241.14 257.67 242.02 257.92 C 244.84 258.89 247.61 259.97 250.45 260.88 C 257.12 262.99 263.65 265.51 270.34 267.62 C 272.53 268.33 274.71 269.14 276.91 269.9 C 277.73 270.22 278.64 270.49 279.25 271.18 C 280.21 272.5 279.64 274.64 278.1 275.23 C 277.11 275.62 276.06 275.23 275.1 274.93 C 271.13 273.46 267.11 272.14 263.09 270.83 C 259.18 269.47 255.29 268.04 251.36 266.79 C 247.78 265.66 244.32 264.26 240.74 263.16 C 239.79 262.82 238.78 262.59 238.02 261.91 C 236.65 260.58 237.39 257.88 239.3 257.54 Z"
|
||||
android:fillColor="#f39800"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/color_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/color_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
4
app/src/main/res/values/ic_banner_background.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_banner_background">#FFFFFF</color>
|
||||
</resources>
|
||||
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.app.Application
|
||||
import com.microsoft.appcenter.AppCenter
|
||||
import com.microsoft.appcenter.analytics.Analytics
|
||||
import com.microsoft.appcenter.crashes.Crashes
|
||||
import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog
|
||||
|
||||
object Tracker {
|
||||
fun initialize(application: Application) {
|
||||
if (BuildConfig.APP_CENTER_KEY != null && !BuildConfig.DEBUG) {
|
||||
AppCenter.start(
|
||||
application,
|
||||
BuildConfig.APP_CENTER_KEY,
|
||||
Analytics::class.java, Crashes::class.java
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadLogcat(logcat: String) {
|
||||
if (BuildConfig.APP_CENTER_KEY != null && !BuildConfig.DEBUG) {
|
||||
if (logcat.isNotBlank()) {
|
||||
Crashes.trackError(
|
||||
RuntimeException(),
|
||||
mapOf("type" to "app_crashed"),
|
||||
listOf(ErrorAttachmentLog.attachmentWithText(logcat, "logcat.txt"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
197
build.gradle.kts
@@ -1,11 +1,202 @@
|
||||
@file:Suppress("UNUSED_VARIABLE")
|
||||
|
||||
import com.android.build.gradle.AppExtension
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
allprojects {
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven("https://raw.githubusercontent.com/MetaCubeX/maven-backup/main/releases")
|
||||
}
|
||||
dependencies {
|
||||
classpath(libs.build.android)
|
||||
classpath(libs.build.kotlin.common)
|
||||
classpath(libs.build.kotlin.serialization)
|
||||
classpath(libs.build.ksp)
|
||||
classpath(libs.build.golang)
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven("https://raw.githubusercontent.com/MetaCubeX/maven-backup/main/releases")
|
||||
}
|
||||
|
||||
val isApp = name == "app"
|
||||
|
||||
apply(plugin = if (isApp) "com.android.application" else "com.android.library")
|
||||
|
||||
fun queryConfigProperty(key: String): Any? {
|
||||
val localProperties = Properties()
|
||||
val localPropertiesFile = rootProject.file("local.properties")
|
||||
if (localPropertiesFile.exists()) {
|
||||
localProperties.load(localPropertiesFile.inputStream())
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return localProperties.getProperty(key)
|
||||
}
|
||||
|
||||
extensions.configure<BaseExtension> {
|
||||
buildFeatures.buildConfig = true
|
||||
defaultConfig {
|
||||
if (isApp) {
|
||||
val customApplicationId = queryConfigProperty("custom.application.id") as? String?
|
||||
applicationId = customApplicationId.takeIf { it?.isNotBlank() == true } ?: "com.github.metacubex.clash"
|
||||
}
|
||||
|
||||
project.name.let { name ->
|
||||
namespace = if (name == "app") "com.github.kr328.clash"
|
||||
else "com.github.kr328.clash.$name"
|
||||
}
|
||||
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
|
||||
versionName = "2.11.25"
|
||||
versionCode = 211025
|
||||
|
||||
resValue("string", "release_name", "v$versionName")
|
||||
resValue("integer", "release_code", "$versionCode")
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
abiFilters("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
}
|
||||
}
|
||||
|
||||
if (!isApp) {
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
} else {
|
||||
setProperty("archivesBaseName", "cmfa-$versionName")
|
||||
}
|
||||
}
|
||||
|
||||
ndkVersion = "29.0.14206865"
|
||||
|
||||
compileSdkVersion(defaultConfig.targetSdk!!)
|
||||
|
||||
if (isApp) {
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes.add("DebugProbesKt.bin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
flavorDimensions("feature")
|
||||
|
||||
val removeSuffix = (queryConfigProperty("remove.suffix") as? String)?.toBoolean() == true
|
||||
|
||||
create("alpha") {
|
||||
isDefault = true
|
||||
dimension = flavorDimensionList[0]
|
||||
if (!removeSuffix) {
|
||||
versionNameSuffix = ".Alpha"
|
||||
}
|
||||
|
||||
|
||||
buildConfigField("boolean", "PREMIUM", "Boolean.parseBoolean(\"false\")")
|
||||
|
||||
resValue("string", "launch_name", "@string/launch_name_alpha")
|
||||
resValue("string", "application_name", "@string/application_name_alpha")
|
||||
|
||||
if (isApp && !removeSuffix) {
|
||||
applicationIdSuffix = ".alpha"
|
||||
}
|
||||
}
|
||||
|
||||
create("meta") {
|
||||
|
||||
dimension = flavorDimensionList[0]
|
||||
if (!removeSuffix) {
|
||||
versionNameSuffix = ".Meta"
|
||||
}
|
||||
|
||||
buildConfigField("boolean", "PREMIUM", "Boolean.parseBoolean(\"false\")")
|
||||
|
||||
resValue("string", "launch_name", "@string/launch_name_meta")
|
||||
resValue("string", "application_name", "@string/application_name_meta")
|
||||
|
||||
if (isApp && !removeSuffix) {
|
||||
applicationIdSuffix = ".meta"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("meta") {
|
||||
java.srcDirs("src/foss/java")
|
||||
}
|
||||
getByName("alpha") {
|
||||
java.srcDirs("src/foss/java")
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
val keystore = rootProject.file("signing.properties")
|
||||
if (keystore.exists()) {
|
||||
create("release") {
|
||||
val prop = Properties().apply {
|
||||
keystore.inputStream().use(this::load)
|
||||
}
|
||||
|
||||
storeFile = rootProject.file("release.keystore")
|
||||
storePassword = prop.getProperty("keystore.password")!!
|
||||
keyAlias = prop.getProperty("key.alias")!!
|
||||
keyPassword = prop.getProperty("key.password")!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("release") {
|
||||
isMinifyEnabled = isApp
|
||||
isShrinkResources = isApp
|
||||
signingConfig = signingConfigs.findByName("release") ?: signingConfigs["debug"]
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
named("debug") {
|
||||
versionNameSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures.apply {
|
||||
dataBinding {
|
||||
isEnabled = name != "hideapi"
|
||||
}
|
||||
}
|
||||
|
||||
if (isApp) {
|
||||
this as AppExtension
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
isUniversalApk = true
|
||||
reset()
|
||||
include("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +204,7 @@ task("clean", type = Delete::class) {
|
||||
delete(rootProject.buildDir)
|
||||
}
|
||||
|
||||
tasks.named<Wrapper>("wrapper") {
|
||||
tasks.wrapper {
|
||||
distributionType = Wrapper.DistributionType.ALL
|
||||
|
||||
doLast {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.5.0"
|
||||
`java-gradle-plugin`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
|
||||
compileOnly(gradleApi())
|
||||
|
||||
api(kotlin("gradle-plugin"))
|
||||
api(kotlin("serialization"))
|
||||
api("com.android.tools.build:gradle:4.2.1") {
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-reflect")
|
||||
}
|
||||
api("com.google.devtools.ksp:symbol-processing-gradle-plugin:1.5.0-1.0.0-alpha10") {
|
||||
exclude("com.android.tools.build", "gradle")
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("golang") {
|
||||
id = "clash-build"
|
||||
implementationClass = "com.github.kr328.clash.tools.ClashBuildPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import org.gradle.api.Project
|
||||
|
||||
const val buildVersionCode = 204002
|
||||
const val buildVersionName = "2.4.2"
|
||||
|
||||
const val buildMinSdkVersion = 21
|
||||
const val buildTargetSdkVersion = 30
|
||||
|
||||
const val buildNdkVersion = "23.0.7123448"
|
||||
|
||||
val Project.buildFlavor: String
|
||||
get() {
|
||||
return if (project(":core").file("src/main/golang/clash/script/script.go").exists())
|
||||
"premium"
|
||||
else
|
||||
"foss"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
const val activityVersion = "1.2.3"
|
||||
const val coroutineVersion = "1.4.3"
|
||||
const val roomVersion = "2.3.0"
|
||||
const val ktxVersion = "1.3.2"
|
||||
const val appcompatVersion = "1.2.0"
|
||||
const val muiltprocessVersion = "1.0.0"
|
||||
const val appcenterVersion = "4.1.1"
|
||||
const val serializationVersion = "1.2.1"
|
||||
const val materialVersion = "1.3.0"
|
||||
const val coordinatorlayoutVersion = "1.1.0"
|
||||
const val recyclerviewVersion = "1.2.0"
|
||||
const val fragmentVersion = "1.3.3"
|
||||
const val viewpagerVersion = "1.0.0"
|
||||
@@ -1,8 +0,0 @@
|
||||
import org.gradle.api.Project
|
||||
import java.io.File
|
||||
|
||||
val Project.golangSource: File
|
||||
get() = file("src/main/golang")
|
||||
|
||||
val Project.golangBuild: File
|
||||
get() = buildDir.resolve("intermediates/golang")
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.github.kr328.clash.tools
|
||||
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import com.android.build.gradle.api.BaseVariant
|
||||
import java.io.Serializable
|
||||
|
||||
data class BuildConfig(
|
||||
val debug: Boolean,
|
||||
val premium: Boolean,
|
||||
val abis: List<NativeAbi>,
|
||||
val minSdkVersion: Int,
|
||||
) : Serializable {
|
||||
companion object {
|
||||
fun of(extension: BaseExtension, variant: BaseVariant): BuildConfig {
|
||||
return BuildConfig(
|
||||
debug = variant.buildType.isDebuggable,
|
||||
premium = variant.flavorName == "premium",
|
||||
abis = extension.defaultConfig.externalNativeBuild.cmake.abiFilters
|
||||
.map { NativeAbi.parse(it) }
|
||||
.distinct(),
|
||||
minSdkVersion = extension.defaultConfig.minSdkVersion!!.apiLevel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.github.kr328.clash.tools
|
||||
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import golangBuild
|
||||
import golangSource
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import java.util.*
|
||||
|
||||
class ClashBuildPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
target.afterEvaluate {
|
||||
target.extensions.getByType(LibraryExtension::class.java).apply {
|
||||
libraryVariants.forEach { variant ->
|
||||
val config = BuildConfig.of(this, variant)
|
||||
val buildDir = target.golangBuild.resolve(variant.name)
|
||||
val capitalize = variant.name.capitalize(Locale.getDefault())
|
||||
|
||||
val task = target.tasks.register(
|
||||
"externalGolangBuild$capitalize",
|
||||
ClashBuildTask::class.java
|
||||
) {
|
||||
it.config.set(config)
|
||||
it.ndkDirectory.set(ndkDirectory)
|
||||
it.inputDirectory.set(target.golangSource)
|
||||
it.outputDirectory.set(buildDir)
|
||||
}
|
||||
|
||||
sourceSets.named(variant.name) {
|
||||
it.jniLibs {
|
||||
srcDir(buildDir)
|
||||
}
|
||||
}
|
||||
|
||||
variant.externalNativeBuildProviders.forEach {
|
||||
it.get().dependsOn(task)
|
||||
}
|
||||
target.tasks.filter { it.name.startsWith("buildCMake") }.forEach {
|
||||
it.mustRunAfter(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.github.kr328.clash.tools
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputDirectory
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.io.File
|
||||
|
||||
abstract class ClashBuildTask : DefaultTask() {
|
||||
abstract val config: Property<BuildConfig>
|
||||
@Input get
|
||||
|
||||
abstract val ndkDirectory: DirectoryProperty
|
||||
@InputDirectory get
|
||||
|
||||
abstract val inputDirectory: DirectoryProperty
|
||||
@InputDirectory get
|
||||
|
||||
abstract val outputDirectory: DirectoryProperty
|
||||
@OutputDirectory get
|
||||
|
||||
@TaskAction
|
||||
fun build() {
|
||||
val input = inputDirectory.file
|
||||
val output = outputDirectory.file
|
||||
|
||||
val config = config.get()
|
||||
val environment = Environment(ndkDirectory.file, config.minSdkVersion)
|
||||
|
||||
val tags = listOf("without_gvisor", "without_system") +
|
||||
(if (config.debug) listOf("debug") else emptyList()) +
|
||||
(if (config.premium) listOf("premium") else emptyList())
|
||||
|
||||
Command.ofGoModuleTidy(input).exec()
|
||||
|
||||
config.abis.forEach {
|
||||
Command.ofGoRun(
|
||||
"make/make.go",
|
||||
listOf("bridge", "native", "build", "android", it.goArch),
|
||||
input.resolve("tun2socket/bridge"),
|
||||
environment.ofLwipBuild(it)
|
||||
).exec()
|
||||
|
||||
Command.ofGoBuild(
|
||||
"c-shared",
|
||||
output.resolve("${it.value}/libclash.so"),
|
||||
tags,
|
||||
!config.debug,
|
||||
input,
|
||||
environment.ofCoreBuild(it)
|
||||
).exec()
|
||||
}
|
||||
}
|
||||
|
||||
private val DirectoryProperty.file: File
|
||||
get() = get().asFile
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package com.github.kr328.clash.tools
|
||||
|
||||
import org.gradle.api.GradleException
|
||||
import java.io.File
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class Command(
|
||||
private val command: Array<String>,
|
||||
workingDir: File,
|
||||
environments: Map<String, String>
|
||||
) {
|
||||
private val processBuilder: ProcessBuilder = ProcessBuilder(*command)
|
||||
.redirectErrorStream(true)
|
||||
.directory(workingDir)
|
||||
.apply { environment().putAll(environments) }
|
||||
|
||||
fun exec() {
|
||||
val process = processBuilder.start()
|
||||
|
||||
thread {
|
||||
process.inputStream.copyTo(System.out)
|
||||
}
|
||||
|
||||
val result = process.waitFor()
|
||||
|
||||
if (result != 0) {
|
||||
throw GradleException("exec ${command.joinToString(" ")}: exit with $result")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun ofGoModuleTidy(workingDir: File): Command {
|
||||
return Command(arrayOf("go", "mod", "tidy"), workingDir, System.getenv())
|
||||
}
|
||||
|
||||
fun ofGoBuild(
|
||||
mode: String,
|
||||
output: File,
|
||||
tags: List<String>,
|
||||
strip: Boolean,
|
||||
workingDir: File,
|
||||
environments: Map<String, String>
|
||||
): Command {
|
||||
val command = mutableListOf("go", "build")
|
||||
|
||||
// go build mode
|
||||
command += "-buildmode"
|
||||
command += mode
|
||||
|
||||
// output file
|
||||
command += "-o"
|
||||
command += output.absolutePath
|
||||
|
||||
// trim path prefix
|
||||
command += "-trimpath"
|
||||
|
||||
if (tags.isNotEmpty()) {
|
||||
command += "-tags"
|
||||
command += tags.joinToString(",")
|
||||
}
|
||||
|
||||
if (strip) {
|
||||
command += "-ldflags"
|
||||
command += "-s -w"
|
||||
}
|
||||
|
||||
return Command(command.toTypedArray(), workingDir, environments)
|
||||
}
|
||||
|
||||
fun ofGoRun(
|
||||
file: String,
|
||||
args: List<String>,
|
||||
workingDir: File,
|
||||
environments: Map<String, String>
|
||||
): Command {
|
||||
val command = mutableListOf("go", "run")
|
||||
|
||||
command += file
|
||||
command += args
|
||||
|
||||
return Command(command.toTypedArray(), workingDir, environments)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.github.kr328.clash.tools
|
||||
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.api.GradleException
|
||||
import java.io.File
|
||||
|
||||
class Environment(
|
||||
private val ndkDirectory: File,
|
||||
private val minSdkVersion: Int,
|
||||
) {
|
||||
fun ofCoreBuild(abi: NativeAbi): Map<String, String> {
|
||||
val host = when {
|
||||
Os.isFamily(Os.FAMILY_WINDOWS) ->
|
||||
"windows"
|
||||
Os.isFamily(Os.FAMILY_MAC) ->
|
||||
"darwin"
|
||||
Os.isFamily(Os.FAMILY_UNIX) ->
|
||||
"linux"
|
||||
else ->
|
||||
throw GradleException("Unsupported host: ${System.getProperty("os.name")}")
|
||||
}
|
||||
|
||||
val compiler = ndkDirectory.resolve("toolchains/llvm/prebuilt/$host-x86_64/bin")
|
||||
.resolve("${abi.compiler}${minSdkVersion}-clang")
|
||||
|
||||
return mapOf(
|
||||
"CC" to compiler.absolutePath,
|
||||
"GOOS" to "android",
|
||||
"GOARCH" to abi.goArch,
|
||||
"GOARM" to abi.goArm,
|
||||
"CGO_ENABLED" to "1",
|
||||
"CFLAGS" to "-O3 -Werror",
|
||||
)
|
||||
}
|
||||
|
||||
fun ofLwipBuild(abi: NativeAbi): Map<String, String> {
|
||||
return mapOf(
|
||||
"CMAKE_SYSTEM_NAME" to "Android",
|
||||
"CMAKE_ANDROID_NDK" to ndkDirectory.absolutePath,
|
||||
"CMAKE_ANDROID_ARCH_ABI" to abi.value,
|
||||
"CMAKE_SYSTEM_VERSION" to minSdkVersion.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.github.kr328.clash.tools
|
||||
|
||||
enum class NativeAbi(
|
||||
val value: String,
|
||||
val compiler: String,
|
||||
val goArch: String,
|
||||
val goArm: String
|
||||
) {
|
||||
ArmeabiV7a("armeabi-v7a", "armv7a-linux-androideabi", "arm", "7"),
|
||||
Arm64V8a("arm64-v8a", "aarch64-linux-android", "arm64", ""),
|
||||
X86("x86", "i686-linux-android", "386", ""),
|
||||
X64("x86_64", "x86_64-linux-android", "amd64", "");
|
||||
|
||||
companion object {
|
||||
fun parse(value: String): NativeAbi {
|
||||
return when (value) {
|
||||
ArmeabiV7a.value -> ArmeabiV7a
|
||||
Arm64V8a.value -> Arm64V8a
|
||||
X86.value -> X86
|
||||
X64.value -> X64
|
||||
else -> throw IllegalArgumentException("unsupported abi $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,11 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = buildTargetSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdk = buildMinSdkVersion
|
||||
targetSdk = buildTargetSdkVersion
|
||||
|
||||
versionCode = buildVersionCode
|
||||
versionName = buildVersionName
|
||||
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("release") {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":hideapi"))
|
||||
|
||||
implementation(kotlin("stdlib-jdk7"))
|
||||
implementation("androidx.core:core-ktx:$ktxVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
implementation(libs.kotlin.coroutine)
|
||||
implementation(libs.androidx.core)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.kr328.clash.common">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.github.kr328.clash.common
|
||||
|
||||
import android.app.Application
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
object Global {
|
||||
object Global : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
val application: Application
|
||||
get() = application_
|
||||
|
||||
@@ -11,4 +14,8 @@ object Global {
|
||||
fun init(application: Application) {
|
||||
this.application_ = application
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.github.kr328.clash.common.compat
|
||||
|
||||
import android.app.ActivityThread
|
||||
import android.app.Application
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
|
||||
@@ -18,3 +20,12 @@ val Application.currentProcessName: String
|
||||
packageName
|
||||
}
|
||||
}
|
||||
|
||||
fun Drawable.foreground(): Drawable {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
this is AdaptiveIconDrawable && this.background == null
|
||||
) {
|
||||
return this.foreground
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -2,21 +2,36 @@
|
||||
|
||||
package com.github.kr328.clash.common.compat
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
fun Context.getColorCompat(@ColorRes id: Int): Int {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
this.getColor(id)
|
||||
} else {
|
||||
resources.getColor(id)
|
||||
}
|
||||
return ContextCompat.getColor(this, id)
|
||||
}
|
||||
|
||||
fun Context.getDrawableCompat(@DrawableRes id: Int): Drawable? {
|
||||
return ContextCompat.getDrawable(this, id)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
fun Context.registerReceiverCompat(
|
||||
receiver: BroadcastReceiver,
|
||||
filter: IntentFilter,
|
||||
permission: String? = null,
|
||||
handler: Handler? = null
|
||||
) =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
registerReceiver(receiver, filter, permission, handler,
|
||||
if (permission == null) Context.RECEIVER_EXPORTED else Context.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
else
|
||||
registerReceiver(receiver, filter, permission, handler)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.text.Html
|
||||
import android.text.Spanned
|
||||
|
||||
fun fromHtmlCompat(content: String): Spanned {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return if (Build.VERSION.SDK_INT >= 24) {
|
||||
Html.fromHtml(content, Html.FROM_HTML_MODE_COMPACT)
|
||||
} else {
|
||||
Html.fromHtml(content)
|
||||
|
||||
@@ -3,9 +3,13 @@ package com.github.kr328.clash.common.compat
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
|
||||
fun pendingIntentFlags(flags: Int, immutable: Boolean = false): Int {
|
||||
return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M && immutable) {
|
||||
flags or PendingIntent.FLAG_IMMUTABLE
|
||||
fun pendingIntentFlags(flags: Int, mutable: Boolean = false): Int {
|
||||
return if (Build.VERSION.SDK_INT >= 24) {
|
||||
if (Build.VERSION.SDK_INT > 30 && mutable) {
|
||||
flags or PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
} else {
|
||||
flags
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.pm.PackageInfo
|
||||
|
||||
val PackageInfo.versionCodeCompat: Long
|
||||
get() {
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||
return if (android.os.Build.VERSION.SDK_INT >= 28) {
|
||||
longVersionCode
|
||||
} else {
|
||||
versionCode.toLong()
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.*
|
||||
|
||||
val Configuration.preferredLocale: Locale
|
||||
get() {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return if (Build.VERSION.SDK_INT >= 24) {
|
||||
locales[0]
|
||||
} else {
|
||||
locale
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
package com.github.kr328.clash.common.compat
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
|
||||
fun Context.startForegroundServiceCompat(intent: Intent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Service.startForegroundCompat(id: Int, notification: Notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(id, notification)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import com.github.kr328.clash.common.util.packageName
|
||||
object Intents {
|
||||
// Public
|
||||
val ACTION_PROVIDE_URL = "$packageName.action.PROVIDE_URL"
|
||||
val ACTION_START_CLASH = "$packageName.action.START_CLASH"
|
||||
val ACTION_STOP_CLASH = "$packageName.action.STOP_CLASH"
|
||||
val ACTION_TOGGLE_CLASH = "$packageName.action.TOGGLE_CLASH"
|
||||
|
||||
const val EXTRA_NAME = "name"
|
||||
|
||||
@@ -14,6 +17,8 @@ object Intents {
|
||||
val ACTION_CLASH_STOPPED = "$packageName.intent.action.CLASH_STOPPED"
|
||||
val ACTION_CLASH_REQUEST_STOP = "$packageName.intent.action.CLASH_REQUEST_STOP"
|
||||
val ACTION_PROFILE_CHANGED = "$packageName.intent.action.PROFILE_CHANGED"
|
||||
val ACTION_PROFILE_UPDATE_COMPLETED = "$packageName.intent.action.PROFILE_UPDATE_COMPLETED"
|
||||
val ACTION_PROFILE_UPDATE_FAILED = "$packageName.intent.action.PROFILE_UPDATE_FAILED"
|
||||
val ACTION_PROFILE_REQUEST_UPDATE = "$packageName.intent.action.REQUEST_UPDATE"
|
||||
val ACTION_PROFILE_SCHEDULE_UPDATES = "$packageName.intent.action.SCHEDULE_UPDATES"
|
||||
val ACTION_PROFILE_LOADED = "$packageName.intent.action.PROFILE_LOADED"
|
||||
@@ -21,4 +26,5 @@ object Intents {
|
||||
|
||||
const val EXTRA_STOP_REASON = "stop_reason"
|
||||
const val EXTRA_UUID = "uuid"
|
||||
const val EXTRA_FAIL_REASON = "fail_reason"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.github.kr328.clash.common.log
|
||||
|
||||
object Log {
|
||||
private const val TAG = "ClashForAndroid"
|
||||
private const val TAG = "ClashMetaForAndroid"
|
||||
|
||||
fun i(message: String, throwable: Throwable? = null) =
|
||||
android.util.Log.i(TAG, message, throwable)
|
||||
|
||||
5
common/src/main/res/values-ru/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="receive_clash_broadcasts">Получать оповещения от Clash</string>
|
||||
<string name="receive_broadcasts_of_clash">Получать оповещения от сервисов Clash</string>
|
||||
</resources>
|
||||
5
common/src/main/res/values-zh-rTW/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="receive_clash_broadcasts">接收 Clash 廣播</string>
|
||||
<string name="receive_broadcasts_of_clash">接收來自 Clash 內部的廣播</string>
|
||||
</resources>
|
||||
1
core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/src/main/cpp/version.h
|
||||
@@ -1,75 +1,46 @@
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URL
|
||||
import java.time.Duration
|
||||
import android.databinding.tool.ext.capitalizeUS
|
||||
import com.github.kr328.golang.GolangBuildTask
|
||||
import com.github.kr328.golang.GolangPlugin
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
id("com.android.library")
|
||||
id("kotlinx-serialization")
|
||||
id("clash-build")
|
||||
id("golang-android")
|
||||
}
|
||||
|
||||
val geoipDatabaseUrl =
|
||||
"https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb"
|
||||
val geoipInvalidate = Duration.ofDays(7)!!
|
||||
val geoipOutput = buildDir.resolve("intermediates/golang_blob")
|
||||
val golangSource = file("src/main/golang/native")
|
||||
|
||||
golang {
|
||||
sourceSets {
|
||||
create("alpha") {
|
||||
tags.set(listOf("foss","with_gvisor","cmfa"))
|
||||
srcDir.set(file("src/foss/golang"))
|
||||
}
|
||||
create("meta") {
|
||||
tags.set(listOf("foss","with_gvisor","cmfa"))
|
||||
srcDir.set(file("src/foss/golang"))
|
||||
}
|
||||
all {
|
||||
fileName.set("libclash.so")
|
||||
packageName.set("cfa/native")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = buildTargetSdkVersion
|
||||
|
||||
ndkVersion = buildNdkVersion
|
||||
|
||||
flavorDimensions(buildFlavor)
|
||||
|
||||
defaultConfig {
|
||||
minSdk = buildMinSdkVersion
|
||||
targetSdk = buildTargetSdkVersion
|
||||
|
||||
versionCode = buildVersionCode
|
||||
versionName = buildVersionName
|
||||
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
abiFilters("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
arguments(
|
||||
"-DGO_SOURCE:STRING=$golangSource",
|
||||
"-DGO_OUTPUT:STRING=$golangBuild",
|
||||
"-DFLAVOR_NAME=$buildFlavor",
|
||||
)
|
||||
productFlavors {
|
||||
all {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments("-DGO_SOURCE:STRING=${golangSource}")
|
||||
arguments("-DGO_OUTPUT:STRING=${GolangPlugin.outputDirOf(project, null, null)}")
|
||||
arguments("-DFLAVOR_NAME:STRING=$name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("release") {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
create("foss") {
|
||||
dimension = "foss"
|
||||
}
|
||||
create("premium") {
|
||||
dimension = "premium"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
@@ -78,60 +49,30 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":common"))
|
||||
implementation(project(":common"))
|
||||
|
||||
implementation(kotlin("stdlib-jdk7"))
|
||||
implementation("androidx.core:core-ktx:$ktxVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
task("downloadGeoipDatabase") {
|
||||
val databaseFile = geoipOutput.resolve("Country.mmdb")
|
||||
val moduleFile = geoipOutput.resolve("go.mod")
|
||||
val sourceFile = geoipOutput.resolve("blob.go")
|
||||
|
||||
val moduleContent = """
|
||||
module "cfa/blob"
|
||||
""".trimIndent()
|
||||
|
||||
val sourceContent = """
|
||||
package blob
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed Country.mmdb
|
||||
var GeoipDatabase []byte
|
||||
""".trimIndent()
|
||||
|
||||
onlyIf {
|
||||
System.currentTimeMillis() - databaseFile.lastModified() > geoipInvalidate.toMillis()
|
||||
}
|
||||
|
||||
doLast {
|
||||
geoipOutput.mkdirs()
|
||||
|
||||
moduleFile.writeText(moduleContent)
|
||||
sourceFile.writeText(sourceContent)
|
||||
|
||||
URL(geoipDatabaseUrl).openConnection().getInputStream().use { input ->
|
||||
FileOutputStream(databaseFile).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.kotlin.coroutine)
|
||||
implementation(libs.kotlin.serialization.json)
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
val downloadTask = tasks["downloadGeoipDatabase"]
|
||||
tasks.withType(GolangBuildTask::class.java).forEach {
|
||||
it.inputs.dir(golangSource)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.forEach {
|
||||
if (it.name.startsWith("externalGolangBuild")) {
|
||||
it.dependsOn(downloadTask)
|
||||
val abis = listOf("arm64-v8a" to "Arm64V8a", "armeabi-v7a" to "ArmeabiV7a", "x86" to "X86", "x86_64" to "X8664")
|
||||
|
||||
androidComponents.onVariants { variant ->
|
||||
val cmakeName = if (variant.buildType == "debug") "Debug" else "RelWithDebInfo"
|
||||
|
||||
abis.forEach { (abi, goAbi) ->
|
||||
tasks.configureEach {
|
||||
if (name.startsWith("buildCMake$cmakeName[$abi]")) {
|
||||
dependsOn("externalGolangBuild${variant.name.capitalizeUS()}$goAbi")
|
||||
println("Set up dependency: $name -> externalGolangBuild${variant.name.capitalizeUS()}$goAbi")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||