Compare commits
462 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
447e712a9d | ||
|
|
8bb03f189d | ||
|
|
3b0554cd9b | ||
|
|
858101b0d9 | ||
|
|
a7cf8bee28 | ||
|
|
af1ec7bea9 | ||
|
|
002bf7ef22 | ||
|
|
6919e2336d | ||
|
|
5a5bd22073 | ||
|
|
a726f00f35 | ||
|
|
79297f8a42 | ||
|
|
e3f70ac253 | ||
|
|
838b346fcc | ||
|
|
eba9545ccf | ||
|
|
890ade9495 | ||
|
|
518ef1e0ec | ||
|
|
bdcecfca72 | ||
|
|
1363846ac4 | ||
|
|
30347546a2 | ||
|
|
ac7eb28e91 | ||
|
|
748405473b | ||
|
|
1080390bed | ||
|
|
c48725c7dd | ||
|
|
a5287dbadc | ||
|
|
ee5a3b0dd9 | ||
|
|
3d001541e5 | ||
|
|
b376b229b9 | ||
|
|
33b6203978 | ||
|
|
2d803e009c | ||
|
|
2ddbe38781 | ||
|
|
96181a2b8d | ||
|
|
8308b8eaf2 | ||
|
|
00f26ff529 | ||
|
|
49dcdf3ae5 | ||
|
|
409b431d1c | ||
|
|
6da988e3db | ||
|
|
fd8f8306ee | ||
|
|
74b342f5c6 | ||
|
|
2504ec79ee | ||
|
|
3e7b211b17 | ||
|
|
13d5514a4c | ||
|
|
f0f9da0f1b | ||
|
|
f6d2c5f473 | ||
|
|
6e8dd5b250 | ||
|
|
f4779bc50c | ||
|
|
432baf262d | ||
|
|
a1d68fcde3 | ||
|
|
e053db3dff | ||
|
|
f624bd651e | ||
|
|
84964c7f91 | ||
|
|
96f56b468e | ||
|
|
bdc3212f38 | ||
|
|
508ddf6df2 | ||
|
|
17af24d179 | ||
|
|
c260e447ea | ||
|
|
0eb40ae993 | ||
|
|
18f3e39346 | ||
|
|
311af02726 | ||
|
|
b799969de8 | ||
|
|
93541883bb | ||
|
|
33430bff8d | ||
|
|
3bc2081540 | ||
|
|
a3b1feabff | ||
|
|
7af8d3843e | ||
|
|
2235805800 | ||
|
|
364b521ec3 | ||
|
|
3cdaf4a8ee | ||
|
|
b2fc6dcdd0 | ||
|
|
70e9320463 | ||
|
|
304c7ed068 | ||
|
|
ddb908f937 | ||
|
|
accd17afd4 | ||
|
|
9f598b77b4 | ||
|
|
d82fa974b1 | ||
|
|
fcbd4a0d48 | ||
|
|
5cd2b8845e | ||
|
|
723ab70170 | ||
|
|
a26bf3eeda | ||
|
|
c33b6463c6 | ||
|
|
df995a3ab2 | ||
|
|
817f844212 | ||
|
|
fa8113b8d7 | ||
|
|
99b95d8369 | ||
|
|
fc2e4ff210 | ||
|
|
1063bf71d6 | ||
|
|
f2af5c45e9 | ||
|
|
b132b0d2f0 | ||
|
|
7869f99fc8 | ||
|
|
122f2eb400 | ||
|
|
3c0f6eeb21 | ||
|
|
19a109355b | ||
|
|
703965a0dd | ||
|
|
66f92c6c60 | ||
|
|
a1cdf6b7a5 | ||
|
|
554c7b5687 | ||
|
|
2d987313a7 | ||
|
|
9eebe32bdf | ||
|
|
8e9da0ad6f | ||
|
|
2f20dea611 | ||
|
|
167baf64a9 | ||
|
|
bd7a214f7f | ||
|
|
f16d2d9a74 | ||
|
|
e984d2c274 | ||
|
|
8720d087ea | ||
|
|
a1e19b9fcd | ||
|
|
6b9728dc84 | ||
|
|
1ce9b7c0c8 | ||
|
|
ff75d3fdc2 | ||
|
|
8154812570 | ||
|
|
1dcd2478fc | ||
|
|
0515806e92 | ||
|
|
f286506ba4 | ||
|
|
48be736275 | ||
|
|
af0faeab16 | ||
|
|
fb6edee842 | ||
|
|
1209c4b92a | ||
|
|
e4e668b492 | ||
|
|
92d3136a23 | ||
|
|
f6e74ddb45 | ||
|
|
c34f332771 | ||
|
|
eb1d71bda6 | ||
|
|
3cdd5822cb | ||
|
|
8f924b5ce1 | ||
|
|
2ae645dce5 | ||
|
|
f848a7119f | ||
|
|
2fa41db32f | ||
|
|
9af4516761 | ||
|
|
189b07124d | ||
|
|
e6e5cdcdcd | ||
|
|
a31e4dab85 | ||
|
|
b2a9397dc7 | ||
|
|
a536df1a02 | ||
|
|
1a2751563d | ||
|
|
2dc82b7e83 | ||
|
|
b02d81ae53 | ||
|
|
773bcc5658 | ||
|
|
9142b9cfb4 | ||
|
|
376882d975 | ||
|
|
f330c32ccc | ||
|
|
58dbb71b53 | ||
|
|
ebd0257f6e | ||
|
|
945c584fc6 | ||
|
|
19cd24c37a | ||
|
|
3994810c4e | ||
|
|
d86e68c77a | ||
|
|
cc7bdefe54 | ||
|
|
51b32a030a | ||
|
|
6c76ddd145 | ||
|
|
0d12cc5dc8 | ||
|
|
cfb756723c | ||
|
|
0e9f198341 | ||
|
|
1fa1325630 | ||
|
|
dc79d3a897 | ||
|
|
486f3ffc96 | ||
|
|
bf030e12f5 | ||
|
|
e80cce9696 | ||
|
|
5bb9ecce47 | ||
|
|
5eb09aa54e | ||
|
|
ff261d9939 | ||
|
|
1624ec87b2 | ||
|
|
f1062f2f45 | ||
|
|
39341c27bc | ||
|
|
31a90cec2b | ||
|
|
617fc63393 | ||
|
|
802f2cf3eb | ||
|
|
c7efcde868 | ||
|
|
e858179204 | ||
|
|
6871b0b950 | ||
|
|
e9a27a1585 | ||
|
|
976b765629 | ||
|
|
126b9b6516 | ||
|
|
94dab02b54 | ||
|
|
a893b87730 | ||
|
|
7db2ddd1f7 | ||
|
|
748980aa1a | ||
|
|
96d416066e | ||
|
|
3955bb16bc | ||
|
|
de9bbf842f | ||
|
|
336b673746 | ||
|
|
f1b6b1e871 | ||
|
|
fba4c03bb5 | ||
|
|
a32ae5b53f | ||
|
|
589e0f38fd | ||
|
|
e21116680e | ||
|
|
e1960f5aff | ||
|
|
327ba57088 | ||
|
|
04e1b024e2 | ||
|
|
b59fe9b57b | ||
|
|
fab0b756de | ||
|
|
aa2727d0d0 | ||
|
|
ce3dd73a81 | ||
|
|
2eab209fea | ||
|
|
02476657bf | ||
|
|
35602120e8 | ||
|
|
9e800e08ab | ||
|
|
09fc20794e | ||
|
|
3cd95fbfdb | ||
|
|
c243fadacf | ||
|
|
9c06412ceb | ||
|
|
8d1f0d5df9 | ||
|
|
a035f42008 | ||
|
|
838fb041aa | ||
|
|
7fbab63227 | ||
|
|
ad871723fe | ||
|
|
8887b44bf7 | ||
|
|
176beced3a | ||
|
|
a3561ddc6c | ||
|
|
097bd06021 | ||
|
|
dfdd1efcc8 | ||
|
|
1ac5a410d4 | ||
|
|
8c44e849c9 | ||
|
|
2684bd2af4 | ||
|
|
01a860aab5 | ||
|
|
e1faf4e54e | ||
|
|
14dd4d6b99 | ||
|
|
bab21bc8a5 | ||
|
|
8393b3ce86 | ||
|
|
e15eec9cff | ||
|
|
32741ed7ab | ||
|
|
0d77a65bbb | ||
|
|
6eaac2d7e9 | ||
|
|
496a0ec92c | ||
|
|
826329e996 | ||
|
|
4f57da4a38 | ||
|
|
b3f49d0a34 | ||
|
|
b78b370408 | ||
|
|
82afcdddd0 | ||
|
|
a33a698f38 | ||
|
|
3aff1800cd | ||
|
|
ea816ca981 | ||
|
|
a2bace4ede | ||
|
|
5589d1058b | ||
|
|
a12fc32ff0 | ||
|
|
ce2d1c5e0d | ||
|
|
eb75666c85 | ||
|
|
4b970cedcc | ||
|
|
fbd9d92f5e | ||
|
|
cdaff4da06 | ||
|
|
fbb17390f2 | ||
|
|
52273db482 | ||
|
|
a6af25ae88 | ||
|
|
3ac89d68fb | ||
|
|
267a43fd97 | ||
|
|
6d0384b6f1 | ||
|
|
7ae4be402f | ||
|
|
46f0b7be5b | ||
|
|
b253f2d947 | ||
|
|
444ade8afe | ||
|
|
522dbdd170 | ||
|
|
d89238e1aa | ||
|
|
52710020ea | ||
|
|
a18b8f4f2b | ||
|
|
01e0a6570d | ||
|
|
cd6ef8f062 | ||
|
|
ea383c43bd | ||
|
|
68e08e3866 | ||
|
|
329ed26e85 | ||
|
|
c830a4cea4 | ||
|
|
e7e088ec83 | ||
|
|
cf6c814eb4 | ||
|
|
8f62e42ae8 | ||
|
|
89b4060ba2 | ||
|
|
e4d4b329a0 | ||
|
|
09c5f41995 | ||
|
|
02b25787c1 | ||
|
|
cdb050b6c5 | ||
|
|
21f47b536d | ||
|
|
f14484c986 | ||
|
|
358dd78dc3 | ||
|
|
d79e072316 | ||
|
|
c24dee567a | ||
|
|
9f75bafcea | ||
|
|
2150452629 | ||
|
|
507a32a08e | ||
|
|
44dee8e850 | ||
|
|
5e4d9246c2 | ||
|
|
9fe7419467 | ||
|
|
41b2251dfe | ||
|
|
73fad43573 | ||
|
|
4676717582 | ||
|
|
834766e6e7 | ||
|
|
e304dce347 | ||
|
|
61654aefeb | ||
|
|
1664aaa25b | ||
|
|
68f1f64f3d | ||
|
|
7bad57ca52 | ||
|
|
1c8e1f0993 | ||
|
|
6037ae6fc4 | ||
|
|
dc1c5400b8 | ||
|
|
9ae4688171 | ||
|
|
e3f39234b2 | ||
|
|
0df0b2d6ac | ||
|
|
9ce96d0591 | ||
|
|
a7ef0618ba | ||
|
|
cc9b083e5d | ||
|
|
5af322552c | ||
|
|
fc852281dd | ||
|
|
0b2f036a22 | ||
|
|
cf2becb5e9 | ||
|
|
82d8eba1b9 | ||
|
|
286ad34d94 | ||
|
|
ff0bc6594d | ||
|
|
7b8113aef1 | ||
|
|
81d2ef5db5 | ||
|
|
bdea3ef88c | ||
|
|
c62c86fc29 | ||
|
|
59f698f755 | ||
|
|
9ac979006e | ||
|
|
da6291a965 | ||
|
|
bf6555e57c | ||
|
|
35b114220e | ||
|
|
800bb6a4e9 | ||
|
|
2c80521f5b | ||
|
|
6351ce5991 | ||
|
|
683362f0ee | ||
|
|
84fc909339 | ||
|
|
74171e26db | ||
|
|
f25c0cc890 | ||
|
|
29848053a4 | ||
|
|
0d9856919e | ||
|
|
0ae7f2f7b3 | ||
|
|
3dde6b0ca3 | ||
|
|
6b28208044 | ||
|
|
13f855e3c4 | ||
|
|
c870595e98 | ||
|
|
ae19a3f68d | ||
|
|
545afc41b3 | ||
|
|
7177d88144 | ||
|
|
294ed50afd | ||
|
|
0105fe48f7 | ||
|
|
c7ff23e3d5 | ||
|
|
23e9d7fde5 | ||
|
|
c93edd8875 | ||
|
|
bde37e38a7 | ||
|
|
c401d63d2f | ||
|
|
52416dd43d | ||
|
|
59bd7128ae | ||
|
|
87f16467bb | ||
|
|
bed0fd00bd | ||
|
|
0672af98f8 | ||
|
|
2341eceb65 | ||
|
|
ec5f7245bf | ||
|
|
caa2edcf05 | ||
|
|
dbe78d0aa5 | ||
|
|
71f2f590a7 | ||
|
|
ed26120581 | ||
|
|
71bd684b46 | ||
|
|
02ae19f0c7 | ||
|
|
a46b0d58eb | ||
|
|
d1262d169b | ||
|
|
4a85c95b02 | ||
|
|
a368927b9f | ||
|
|
80feb69af5 | ||
|
|
c09f0d5787 | ||
|
|
ff667546b8 | ||
|
|
6a255cdfa4 | ||
|
|
2b5784df6f | ||
|
|
a9665dd2d8 | ||
|
|
1e52877e93 | ||
|
|
062c0d8ddb | ||
|
|
a7a70b448f | ||
|
|
3e52aeb804 | ||
|
|
4b4f5d145b | ||
|
|
775fa5ea62 | ||
|
|
34d8329c8a | ||
|
|
2e7ae732aa | ||
|
|
424287e258 | ||
|
|
40d03bbb96 | ||
|
|
d768694445 | ||
|
|
56c7f2ef69 | ||
|
|
3839b0c59c | ||
|
|
77cab14ae8 | ||
|
|
b0dbd4c7ca | ||
|
|
50ca2e0e69 | ||
|
|
0bf8beda94 | ||
|
|
4d334929c9 | ||
|
|
ccfdf096f9 | ||
|
|
b4f2af2778 | ||
|
|
32b9e4855c | ||
|
|
bdbce5147e | ||
|
|
a4833506ae | ||
|
|
d4e8072248 | ||
|
|
207ca93735 | ||
|
|
99307ab8f0 | ||
|
|
a5bb39ac8a | ||
|
|
073c7c0410 | ||
|
|
7e88e3ba4f | ||
|
|
1ec23a7b39 | ||
|
|
685f9e220c | ||
|
|
e77b7eb52e | ||
|
|
29014704e0 | ||
|
|
b3570d9c0b | ||
|
|
6e427cee82 | ||
|
|
0a1b6d00d9 | ||
|
|
00ffe66f36 | ||
|
|
63661fbdaa | ||
|
|
4d4a4543c5 | ||
|
|
2349805968 | ||
|
|
bdbd0b154e | ||
|
|
653137ec58 | ||
|
|
39f65850be | ||
|
|
2d8db97417 | ||
|
|
17a6f80798 | ||
|
|
381fe859ff | ||
|
|
e2bdf17b82 | ||
|
|
d20afc6801 | ||
|
|
54046a27e6 | ||
|
|
36c000d18a | ||
|
|
2f3c2cf4d5 | ||
|
|
892358d5d8 | ||
|
|
6dced903cd | ||
|
|
f8a98a426e | ||
|
|
011506e99f | ||
|
|
966151d3fe | ||
|
|
247f4db77e | ||
|
|
9a661bc401 | ||
|
|
f5f1b3816c | ||
|
|
3971c9badc | ||
|
|
390fbf046b | ||
|
|
ec7ba59528 | ||
|
|
838941c6a4 | ||
|
|
a0ec764b64 | ||
|
|
26f1f7099b | ||
|
|
613e9ac8bf | ||
|
|
ea979f02b5 | ||
|
|
383f55f809 | ||
|
|
ce61f177dc | ||
|
|
6a64157537 | ||
|
|
de83302c8a | ||
|
|
68553d3807 | ||
|
|
4cfe3394b1 | ||
|
|
ae9aa75ba0 | ||
|
|
3d7ed12d4b | ||
|
|
f70be5bce9 | ||
|
|
405667697e | ||
|
|
3aeda7de81 | ||
|
|
8a775d662a | ||
|
|
ec0ccbca76 | ||
|
|
6cdcbb0096 | ||
|
|
79e3881704 | ||
|
|
4cf2d429f0 | ||
|
|
fa51952bfa | ||
|
|
b2c4c0a67e | ||
|
|
cf2c58637b | ||
|
|
a2c262441e | ||
|
|
237be79680 | ||
|
|
441a64b8c6 | ||
|
|
2128488658 | ||
|
|
d9dc8317f5 | ||
|
|
6eda2180e4 | ||
|
|
57a2f0bebf | ||
|
|
1fd3ccaa0c | ||
|
|
528597d0c7 | ||
|
|
37411e3e6b | ||
|
|
6d681b92aa | ||
|
|
73fae63905 | ||
|
|
216710974a | ||
|
|
f3bbef7412 | ||
|
|
fa74d9c598 | ||
|
|
113364af26 | ||
|
|
2990db305e | ||
|
|
db8942fe5a |
63
.github/workflows/build.yml
vendored
Normal file
63
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: Build APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
XRAY_CORE_VERSION:
|
||||||
|
description: 'Xray core version or commit hash'
|
||||||
|
required: false
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Golang
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22.2'
|
||||||
|
|
||||||
|
- name: Install gomobile
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
|
||||||
|
- name: Setup Android environment
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build dependencies
|
||||||
|
run: |
|
||||||
|
mkdir ${{ github.workspace }}/build
|
||||||
|
cd ${{ github.workspace }}/build
|
||||||
|
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
|
||||||
|
cd AndroidLibXrayLite
|
||||||
|
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
|
||||||
|
gomobile init
|
||||||
|
go mod tidy -v
|
||||||
|
gomobile bind -v -androidapi 19 -ldflags='-s -w' ./
|
||||||
|
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
|
||||||
|
|
||||||
|
- name: Build APK
|
||||||
|
run: |
|
||||||
|
cd ${{ github.workspace }}/V2rayNG
|
||||||
|
chmod 755 gradlew
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apk
|
||||||
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/
|
||||||
41
CR.md
41
CR.md
@@ -1,29 +1,40 @@
|
|||||||
v2rayNG 隐私条款
|
**v2rayNG 隐私权政策**
|
||||||
|
|
||||||
最后更新 2017-11-22
|
本政策自2023年11月17日起施行
|
||||||
|
|
||||||
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。
|
||||||
|
|
||||||
1. 信息收集
|
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
||||||
|
|
||||||
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
|
**信息收集**
|
||||||
|
|
||||||
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。
|
||||||
|
|
||||||
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
||||||
|
|
||||||
2. 信息共享
|
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
||||||
|
|
||||||
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
**信息共享**
|
||||||
|
|
||||||
3. 信息存留
|
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
||||||
|
|
||||||
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
**信息存留**
|
||||||
|
|
||||||
4. 信息泄露
|
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
||||||
|
|
||||||
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
**信息泄露**
|
||||||
|
|
||||||
5. 条款修改
|
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
||||||
|
|
||||||
|
**儿童隐私**
|
||||||
|
|
||||||
|
这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。
|
||||||
|
|
||||||
|
**条款修改**
|
||||||
|
|
||||||
|
我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。**
|
||||||
|
|
||||||
|
**联系我们**
|
||||||
|
|
||||||
|
如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。
|
||||||
|
|
||||||
我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
|
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
Properties props = new Properties()
|
|
||||||
props.load(new FileInputStream(new File('local.properties')))
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion Integer.parseInt("$compileSdkVer")
|
|
||||||
buildToolsVersion "$buildToolsVer"
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
targetCompatibility = "8"
|
|
||||||
sourceCompatibility = "8"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.v2ray.ang"
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
|
||||||
multiDexEnabled true
|
|
||||||
versionCode 495
|
|
||||||
versionName "1.7.33"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props["sign"]) {
|
|
||||||
signingConfigs {
|
|
||||||
release {
|
|
||||||
storeFile file("../key.jks")
|
|
||||||
keyAlias 'ang'
|
|
||||||
keyPassword '123456'
|
|
||||||
storePassword '123456'
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
storeFile file("../key.jks")
|
|
||||||
keyAlias 'ang'
|
|
||||||
keyPassword '123456'
|
|
||||||
storePassword '123456'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
zipAlignEnabled false
|
|
||||||
shrinkResources false
|
|
||||||
if (props["sign"]) {
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
|
||||||
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
minifyEnabled false
|
|
||||||
zipAlignEnabled false
|
|
||||||
shrinkResources false
|
|
||||||
if (props["sign"]) {
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
jniLibs.srcDirs = ['libs']
|
|
||||||
java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
enable true
|
|
||||||
reset()
|
|
||||||
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
|
|
||||||
universalApk true //generate an additional APK that contains all the ABIs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// map for the version code
|
|
||||||
project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
|
||||||
|
|
||||||
android.applicationVariants.all { variant ->
|
|
||||||
// assign different version code for each output
|
|
||||||
variant.outputs.each { output ->
|
|
||||||
output.outputFileName = "v2rayNG_" + variant.versionName + "_" + output.getFilter(com.android.build.OutputFile.ABI) + ".apk"
|
|
||||||
|
|
||||||
output.versionCodeOverride =
|
|
||||||
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
|
|
||||||
1000000 + android.defaultConfig.versionCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
|
|
||||||
// Androidx
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
|
||||||
implementation 'com.google.android.material:material:1.6.1'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.2'
|
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
|
||||||
|
|
||||||
// Androidx ktx
|
|
||||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
|
|
||||||
|
|
||||||
//kotlin
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
|
||||||
|
|
||||||
implementation 'com.tencent:mmkv-static:1.2.12'
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.9'
|
|
||||||
implementation 'io.reactivex:rxjava:1.3.4'
|
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
|
||||||
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
|
|
||||||
implementation 'me.dm7.barcodescanner:core:1.9.8'
|
|
||||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
|
||||||
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
|
|
||||||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
|
||||||
implementation 'com.blacksquircle.ui:editorkit:2.1.1'
|
|
||||||
implementation 'com.blacksquircle.ui:language-base:2.1.1'
|
|
||||||
implementation 'com.blacksquircle.ui:language-json:2.1.1'
|
|
||||||
}
|
|
||||||
|
|
||||||
//buildscript {
|
|
||||||
// repositories {
|
|
||||||
// google()
|
|
||||||
// mavenCentral()
|
|
||||||
// maven { url 'https://maven.google.com' }
|
|
||||||
// maven { url 'https://jitpack.io' }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
139
V2rayNG/app/build.gradle.kts
Normal file
139
V2rayNG/app/build.gradle.kts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.v2ray.ang"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.v2ray.ang"
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = 34
|
||||||
|
versionCode = 575
|
||||||
|
versionName = "1.8.31"
|
||||||
|
multiDexEnabled = true
|
||||||
|
splits.abi {
|
||||||
|
reset()
|
||||||
|
include(
|
||||||
|
"arm64-v8a",
|
||||||
|
"armeabi-v7a",
|
||||||
|
"x86_64",
|
||||||
|
"x86"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") {
|
||||||
|
jniLibs.srcDirs("libs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
isEnable = true
|
||||||
|
isUniversalApk = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all {
|
||||||
|
val variant = this
|
||||||
|
val versionCodes =
|
||||||
|
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
|
||||||
|
|
||||||
|
variant.outputs
|
||||||
|
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||||
|
.forEach { output ->
|
||||||
|
val abi = if (output.getFilter("ABI") != null)
|
||||||
|
output.getFilter("ABI")
|
||||||
|
else
|
||||||
|
"universal"
|
||||||
|
|
||||||
|
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
||||||
|
if(versionCodes.containsKey(abi))
|
||||||
|
{
|
||||||
|
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar","*.jar"))))
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
|
||||||
|
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
||||||
|
// Androidx
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
|
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||||
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
|
implementation("androidx.fragment:fragment-ktx:1.8.1")
|
||||||
|
implementation("androidx.multidex:multidex:2.0.1")
|
||||||
|
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||||
|
|
||||||
|
// Androidx ktx
|
||||||
|
implementation("androidx.activity:activity-ktx:1.9.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
|
||||||
|
|
||||||
|
//kotlin
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
|
||||||
|
|
||||||
|
implementation("com.tencent:mmkv-static:1.3.4")
|
||||||
|
implementation("com.google.code.gson:gson:2.11.0")
|
||||||
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
|
implementation("io.reactivex:rxandroid:1.2.1")
|
||||||
|
implementation("com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar")
|
||||||
|
implementation("me.drakeet.support:toastcompat:1.1.0")
|
||||||
|
implementation("com.blacksquircle.ui:editorkit:2.9.0")
|
||||||
|
implementation("com.blacksquircle.ui:language-base:2.9.0")
|
||||||
|
implementation("com.blacksquircle.ui:language-json:2.9.0")
|
||||||
|
implementation("io.github.g00fy2.quickie:quickie-bundled:1.9.0")
|
||||||
|
implementation("com.google.zxing:core:3.5.3")
|
||||||
|
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.8.1")
|
||||||
|
implementation("androidx.work:work-multiprocess:2.8.1")
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.v2ray.ang;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.test.ApplicationTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
|
||||||
*/
|
|
||||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
|
||||||
public ApplicationTest() {
|
|
||||||
super(Application.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
V2rayNG/app/src/dev/res/values/strings.xml
Normal file
4
V2rayNG/app/src/dev/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item name="app_name" type="string">v2rayNG (DEV)</item>
|
||||||
|
</resources>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.v2ray.ang">
|
tools:ignore="MissingLeanbackLauncher">
|
||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
@@ -10,8 +10,12 @@
|
|||||||
android:largeScreens="true"
|
android:largeScreens="true"
|
||||||
android:xlargeScreens="true"/>
|
android:xlargeScreens="true"/>
|
||||||
|
|
||||||
|
<uses-sdk android:minSdkVersion="21" tools:overrideLibrary="com.blacksquircle.ui.editorkit"/>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
|
||||||
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
@@ -23,24 +27,34 @@
|
|||||||
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||||
|
android:minSdkVersion="34" />
|
||||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AngApplication"
|
android:name=".AngApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:banner="@mipmap/ic_banner"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppThemeLight"
|
android:theme="@style/AppThemeDayNight"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="m">
|
tools:targetApi="m">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:exported="true"
|
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:launchMode="singleTask">
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@style/AppThemeDayNight.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
@@ -79,6 +93,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:name=".ui.UserAssetActivity" />
|
android:name=".ui.UserAssetActivity" />
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.UserAssetUrlActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -105,10 +122,14 @@
|
|||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<data android:scheme="v2rayng"
|
<data android:scheme="v2rayng"/>
|
||||||
android:host="install-config" />
|
<data android:host="install-config"/>
|
||||||
|
<data android:host="install-sub"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.AboutActivity" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.V2RayVpnService"
|
android:name=".service.V2RayVpnService"
|
||||||
@@ -116,6 +137,7 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService" />
|
<action android:name="android.net.VpnService" />
|
||||||
@@ -123,12 +145,19 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="vpn" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".service.V2RayProxyOnlyService"
|
<service android:name=".service.V2RayProxyOnlyService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="proxy" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".service.V2RayTestService"
|
<service android:name=".service.V2RayTestService"
|
||||||
@@ -155,11 +184,15 @@
|
|||||||
android:name=".service.QSTileService"
|
android:name=".service.QSTileService"
|
||||||
android:icon="@drawable/ic_stat_name"
|
android:icon="@drawable/ic_stat_name"
|
||||||
android:label="@string/app_tile_name"
|
android:label="@string/app_tile_name"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="tile" />
|
||||||
</service>
|
</service>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<activity
|
<activity
|
||||||
@@ -181,6 +214,28 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.cache"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/cache_paths"/>
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +1,2 @@
|
|||||||
domain:12306.com,
|
geosite:cn,
|
||||||
domain:51ym.me,
|
geosite:geolocation-cn
|
||||||
domain:52pojie.cn,
|
|
||||||
domain:8686c.com,
|
|
||||||
domain:abercrombie.com,
|
|
||||||
domain:adobesc.com,
|
|
||||||
domain:air-matters.com,
|
|
||||||
domain:air-matters.io,
|
|
||||||
domain:airtable.com,
|
|
||||||
domain:akadns.net,
|
|
||||||
domain:apache.org,
|
|
||||||
domain:api.crisp.chat,
|
|
||||||
domain:api.termius.com,
|
|
||||||
domain:appshike.com,
|
|
||||||
domain:appstore.com,
|
|
||||||
domain:aweme.snssdk.com,
|
|
||||||
domain:bababian.com,
|
|
||||||
domain:battle.net,
|
|
||||||
domain:beatsbydre.com,
|
|
||||||
domain:bet365.com,
|
|
||||||
domain:bilibili.cn,
|
|
||||||
domain:ccgslb.com,
|
|
||||||
domain:ccgslb.net,
|
|
||||||
domain:chunbo.com,
|
|
||||||
domain:chunboimg.com,
|
|
||||||
domain:clashroyaleapp.com,
|
|
||||||
domain:cloudsigma.com,
|
|
||||||
domain:cloudxns.net,
|
|
||||||
domain:cmfu.com,
|
|
||||||
domain:culturedcode.com,
|
|
||||||
domain:dct-cloud.com,
|
|
||||||
domain:didialift.com,
|
|
||||||
domain:douyutv.com,
|
|
||||||
domain:duokan.com,
|
|
||||||
domain:dytt8.net,
|
|
||||||
domain:easou.com,
|
|
||||||
domain:ecitic.net,
|
|
||||||
domain:eclipse.org,
|
|
||||||
domain:eudic.net,
|
|
||||||
domain:ewqcxz.com,
|
|
||||||
domain:fir.im,
|
|
||||||
domain:frdic.com,
|
|
||||||
domain:fresh-ideas.cc,
|
|
||||||
domain:godic.net,
|
|
||||||
domain:goodread.com,
|
|
||||||
domain:haibian.com,
|
|
||||||
domain:hdslb.net,
|
|
||||||
domain:hollisterco.com,
|
|
||||||
domain:hongxiu.com,
|
|
||||||
domain:hxcdn.net,
|
|
||||||
domain:images.unsplash.com,
|
|
||||||
domain:img4me.com,
|
|
||||||
domain:ipify.org,
|
|
||||||
domain:ixdzs.com,
|
|
||||||
domain:jd.hk,
|
|
||||||
domain:jianshuapi.com,
|
|
||||||
domain:jomodns.com,
|
|
||||||
domain:jsboxbbs.com,
|
|
||||||
domain:knewone.com,
|
|
||||||
domain:kuaidi100.com,
|
|
||||||
domain:lemicp.com,
|
|
||||||
domain:letvcloud.com,
|
|
||||||
domain:lizhi.io,
|
|
||||||
domain:localizecdn.com,
|
|
||||||
domain:lucifr.com,
|
|
||||||
domain:luoo.net,
|
|
||||||
domain:mai.tn,
|
|
||||||
domain:maven.org,
|
|
||||||
domain:miwifi.com,
|
|
||||||
domain:moji.com,
|
|
||||||
domain:moke.com,
|
|
||||||
domain:mtalk.google.com,
|
|
||||||
domain:mxhichina.com,
|
|
||||||
domain:myqcloud.com,
|
|
||||||
domain:myunlu.com,
|
|
||||||
domain:netease.com,
|
|
||||||
domain:nfoservers.com,
|
|
||||||
domain:nssurge.com,
|
|
||||||
domain:nuomi.com,
|
|
||||||
domain:ourdvs.com,
|
|
||||||
domain:overcast.fm,
|
|
||||||
domain:paypal.com,
|
|
||||||
domain:paypalobjects.com,
|
|
||||||
domain:pgyer.com,
|
|
||||||
domain:qdaily.com,
|
|
||||||
domain:qdmm.com,
|
|
||||||
domain:qin.io,
|
|
||||||
domain:qingmang.me,
|
|
||||||
domain:qingmang.mobi,
|
|
||||||
domain:qqurl.com,
|
|
||||||
domain:rarbg.to,
|
|
||||||
domain:rrmj.tv,
|
|
||||||
domain:ruguoapp.com,
|
|
||||||
domain:sm.ms,
|
|
||||||
domain:snwx.com,
|
|
||||||
domain:soku.com,
|
|
||||||
domain:startssl.com,
|
|
||||||
domain:store.steampowered.com,
|
|
||||||
domain:symcd.com,
|
|
||||||
domain:teamviewer.com,
|
|
||||||
domain:tmzvps.com,
|
|
||||||
domain:trello.com,
|
|
||||||
domain:trellocdn.com,
|
|
||||||
domain:ttmeiju.com,
|
|
||||||
domain:udache.com,
|
|
||||||
domain:uxengine.net,
|
|
||||||
domain:weather.bjango.com,
|
|
||||||
domain:weather.com,
|
|
||||||
domain:webqxs.com,
|
|
||||||
domain:weico.cc,
|
|
||||||
domain:wenku8.net,
|
|
||||||
domain:werewolf.53site.com,
|
|
||||||
domain:windowsupdate.com,
|
|
||||||
domain:wkcdn.com,
|
|
||||||
domain:workflowy.com,
|
|
||||||
domain:xdrig.com,
|
|
||||||
domain:xiaojukeji.com,
|
|
||||||
domain:xiaomi.net,
|
|
||||||
domain:xiaomicp.com,
|
|
||||||
domain:ximalaya.com,
|
|
||||||
domain:xitek.com,
|
|
||||||
domain:xmcdn.com,
|
|
||||||
domain:xslb.net,
|
|
||||||
domain:xteko.com,
|
|
||||||
domain:yach.me,
|
|
||||||
domain:yixia.com,
|
|
||||||
domain:yunjiasu-cdn.net,
|
|
||||||
domain:zealer.com,
|
|
||||||
domain:zgslb.net,
|
|
||||||
domain:zimuzu.tv,
|
|
||||||
domain:zmz002.com,
|
|
||||||
domain:samsungdm.com,
|
|
||||||
@@ -1,33 +1 @@
|
|||||||
geosite:google,
|
geosite:geolocation-!cn
|
||||||
geosite:github,
|
|
||||||
geosite:netflix,
|
|
||||||
geosite:steam,
|
|
||||||
geosite:telegram,
|
|
||||||
geosite:tumblr,
|
|
||||||
geosite:speedtest,
|
|
||||||
geosite:bbc,
|
|
||||||
domain:gvt1.com,
|
|
||||||
domain:textnow.com,
|
|
||||||
domain:twitch.tv,
|
|
||||||
domain:wikileaks.org,
|
|
||||||
domain:naver.com,
|
|
||||||
91.108.4.0/22,
|
|
||||||
91.108.8.0/22,
|
|
||||||
91.108.12.0/22,
|
|
||||||
91.108.20.0/22,
|
|
||||||
91.108.36.0/23,
|
|
||||||
91.108.38.0/23,
|
|
||||||
91.108.56.0/22,
|
|
||||||
149.154.160.0/20,
|
|
||||||
149.154.164.0/22,
|
|
||||||
149.154.172.0/22,
|
|
||||||
74.125.0.0/16,
|
|
||||||
173.194.0.0/16,
|
|
||||||
172.217.0.0/16,
|
|
||||||
216.58.200.0/24,
|
|
||||||
216.58.220.0/24,
|
|
||||||
91.108.56.116,
|
|
||||||
91.108.56.0/24,
|
|
||||||
109.239.140.0/24,
|
|
||||||
149.154.167.0/24,
|
|
||||||
149.154.175.0/24,
|
|
||||||
@@ -1,28 +1,44 @@
|
|||||||
package com.v2ray.ang
|
package com.v2ray.ang
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.work.Configuration
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication() {
|
class AngApplication : MultiDexApplication(), Configuration.Provider {
|
||||||
companion object {
|
companion object {
|
||||||
const val PREF_LAST_VERSION = "pref_last_version"
|
//const val PREF_LAST_VERSION = "pref_last_version"
|
||||||
|
lateinit var application: AngApplication
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstRun = false
|
override fun attachBaseContext(base: Context?) {
|
||||||
private set
|
super.attachBaseContext(base)
|
||||||
|
application = this
|
||||||
|
}
|
||||||
|
|
||||||
|
//var firstRun = false
|
||||||
|
// private set
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// LeakCanary.install(this)
|
// LeakCanary.install(this)
|
||||||
|
|
||||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
||||||
if (firstRun)
|
// if (firstRun)
|
||||||
defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||||
|
|
||||||
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
|
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
|
||||||
MMKV.initialize(this)
|
MMKV.initialize(this)
|
||||||
|
|
||||||
|
Utils.setNightMode(application)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getWorkManagerConfiguration(): Configuration {
|
||||||
|
return Configuration.Builder()
|
||||||
|
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,43 +5,73 @@ package com.v2ray.ang
|
|||||||
* App Config Const
|
* App Config Const
|
||||||
*/
|
*/
|
||||||
object AppConfig {
|
object AppConfig {
|
||||||
const val ANG_PACKAGE = "com.v2ray.ang"
|
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
|
||||||
const val DIR_ASSETS = "assets"
|
const val DIR_ASSETS = "assets"
|
||||||
|
const val DIR_BACKUPS = "backups"
|
||||||
|
|
||||||
// legacy
|
// legacy
|
||||||
const val ANG_CONFIG = "ang_config"
|
const val ANG_CONFIG = "ang_config"
|
||||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
||||||
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
|
||||||
|
|
||||||
// Preferences mapped to MMKV
|
// Preferences mapped to MMKV
|
||||||
const val PREF_MODE = "pref_mode"
|
|
||||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
|
||||||
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
||||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
|
||||||
|
|
||||||
|
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
||||||
|
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
||||||
|
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
||||||
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
||||||
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
||||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
|
||||||
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
|
||||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
|
||||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
||||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
const val PREF_VPN_DNS = "pref_vpn_dns"
|
||||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
|
||||||
const val PREF_HTTP_PORT = "pref_http_port"
|
|
||||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
|
||||||
const val PREF_LANGUAGE = "pref_language"
|
|
||||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
|
||||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||||
const val PREF_ROUTING_MODE = "pref_routing_mode"
|
const val PREF_ROUTING_MODE = "pref_routing_mode"
|
||||||
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
|
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
|
||||||
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
|
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
|
||||||
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
|
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
|
||||||
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
||||||
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
|
||||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
|
||||||
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
|
||||||
|
|
||||||
const val HTTP_PROTOCOL: String = "http://"
|
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
||||||
const val HTTPS_PROTOCOL: String = "https://"
|
const val PREF_MUX_CONCURRENCY = "pref_mux_concurency"
|
||||||
|
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurency"
|
||||||
|
const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
|
||||||
|
|
||||||
|
const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
|
||||||
|
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
|
||||||
|
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
|
||||||
|
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
|
||||||
|
|
||||||
|
const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
|
||||||
|
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
|
||||||
|
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // 24 hours
|
||||||
|
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
|
||||||
|
|
||||||
|
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||||
|
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
||||||
|
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
|
||||||
|
const val PREF_LANGUAGE = "pref_language"
|
||||||
|
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
||||||
|
|
||||||
|
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||||
|
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
||||||
|
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||||
|
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||||
|
const val PREF_HTTP_PORT = "pref_http_port"
|
||||||
|
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
||||||
|
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
||||||
|
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
|
||||||
|
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||||
|
const val PREF_MODE = "pref_mode"
|
||||||
|
|
||||||
|
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||||
|
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
||||||
|
|
||||||
|
//Preferences mapped to MMKV End
|
||||||
|
|
||||||
|
const val PROTOCOL_HTTP: String = "http://"
|
||||||
|
const val PROTOCOL_HTTPS: String = "https://"
|
||||||
|
const val PROTOCOL_FREEDOM: String = "freedom"
|
||||||
|
|
||||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||||
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
||||||
@@ -53,23 +83,35 @@ object AppConfig {
|
|||||||
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
||||||
const val TASKER_DEFAULT_GUID = "Default"
|
const val TASKER_DEFAULT_GUID = "Default"
|
||||||
|
|
||||||
const val TAG_AGENT = "proxy"
|
const val TAG_PROXY = "proxy"
|
||||||
const val TAG_DIRECT = "direct"
|
const val TAG_DIRECT = "direct"
|
||||||
const val TAG_BLOCKED = "block"
|
const val TAG_BLOCKED = "block"
|
||||||
|
const val TAG_FRAGMENT = "fragment"
|
||||||
|
|
||||||
const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
const val androidpackagenamelistUrl =
|
||||||
const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
||||||
const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
|
const val v2rayCustomRoutingListUrl =
|
||||||
const val v2rayNGWikiMode = "https://github.com/2dust/v2rayNG/wiki/Mode"
|
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
||||||
const val promotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
|
||||||
const val geoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/";
|
const val v2rayNGIssues = "$v2rayNGUrl/issues"
|
||||||
|
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
|
||||||
|
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
|
||||||
|
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||||
|
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
|
||||||
|
const val TgChannelUrl = "https://t.me/github_2dust"
|
||||||
|
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
|
||||||
|
const val DelayTestUrl2 = "https://www.google.com/generate_204"
|
||||||
|
|
||||||
const val DNS_AGENT = "1.1.1.1"
|
const val DNS_PROXY = "1.1.1.1"
|
||||||
const val DNS_DIRECT = "223.5.5.5"
|
const val DNS_DIRECT = "223.5.5.5"
|
||||||
|
const val DNS_VPN = "1.1.1.1"
|
||||||
|
|
||||||
const val PORT_LOCAL_DNS = "10853"
|
const val PORT_LOCAL_DNS = "10853"
|
||||||
const val PORT_SOCKS = "10808"
|
const val PORT_SOCKS = "10808"
|
||||||
const val PORT_HTTP = "10809"
|
const val PORT_HTTP = "10809"
|
||||||
|
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
||||||
|
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
||||||
|
const val WIREGUARD_LOCAL_MTU = "1420"
|
||||||
|
|
||||||
const val MSG_REGISTER_CLIENT = 1
|
const val MSG_REGISTER_CLIENT = 1
|
||||||
const val MSG_STATE_RUNNING = 11
|
const val MSG_STATE_RUNNING = 11
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class AngConfig(
|
|
||||||
var index: Int,
|
|
||||||
var vmess: ArrayList<VmessBean>,
|
|
||||||
var subItem: ArrayList<SubItemBean>
|
|
||||||
) {
|
|
||||||
data class VmessBean(var guid: String = "123456",
|
|
||||||
var address: String = "v2ray.cool",
|
|
||||||
var port: Int = 10086,
|
|
||||||
var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
|
||||||
var alterId: Int = 64,
|
|
||||||
var security: String = "aes-128-cfb",
|
|
||||||
var network: String = "tcp",
|
|
||||||
var remarks: String = "def",
|
|
||||||
var headerType: String = "",
|
|
||||||
var requestHost: String = "",
|
|
||||||
var path: String = "",
|
|
||||||
var streamSecurity: String = "",
|
|
||||||
var allowInsecure: String = "",
|
|
||||||
var configType: Int = 1,
|
|
||||||
var configVersion: Int = 1,
|
|
||||||
var testResult: String = "",
|
|
||||||
var subid: String = "",
|
|
||||||
var flow: String = "",
|
|
||||||
var sni: String = "")
|
|
||||||
|
|
||||||
data class SubItemBean(var id: String = "",
|
|
||||||
var remarks: String = "",
|
|
||||||
var url: String = "",
|
|
||||||
var enabled: Boolean = true)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class AssetUrlItem(
|
||||||
|
var remarks: String = "",
|
||||||
|
var url: String = "",
|
||||||
|
val addedTime: Long = System.currentTimeMillis(),
|
||||||
|
var lastUpdated: Long = -1
|
||||||
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_AGENT
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
@@ -26,7 +26,7 @@ data class ServerConfig(
|
|||||||
vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
||||||
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
|
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
|
||||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
||||||
EConfigType.CUSTOM, EConfigType.WIREGUARD ->
|
EConfigType.CUSTOM ->
|
||||||
return ServerConfig(configType = configType)
|
return ServerConfig(configType = configType)
|
||||||
EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
|
EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
|
||||||
return ServerConfig(
|
return ServerConfig(
|
||||||
@@ -36,6 +36,15 @@ data class ServerConfig(
|
|||||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||||
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
|
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
|
||||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
||||||
|
EConfigType.WIREGUARD ->
|
||||||
|
return ServerConfig(
|
||||||
|
configType = configType,
|
||||||
|
outboundBean = V2rayConfig.OutboundBean(
|
||||||
|
protocol = configType.name.lowercase(),
|
||||||
|
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||||
|
secretKey = "",
|
||||||
|
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +58,7 @@ data class ServerConfig(
|
|||||||
|
|
||||||
fun getAllOutboundTags(): MutableList<String> {
|
fun getAllOutboundTags(): MutableList<String> {
|
||||||
if (configType != EConfigType.CUSTOM) {
|
if (configType != EConfigType.CUSTOM) {
|
||||||
return mutableListOf(TAG_AGENT, TAG_DIRECT, TAG_BLOCKED)
|
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
||||||
}
|
}
|
||||||
fullConfig?.let { config ->
|
fullConfig?.let { config ->
|
||||||
return config.outbounds.map { it.tag }.toMutableList()
|
return config.outbounds.map { it.tag }.toMutableList()
|
||||||
@@ -60,10 +69,6 @@ data class ServerConfig(
|
|||||||
fun getV2rayPointDomainAndPort(): String {
|
fun getV2rayPointDomainAndPort(): String {
|
||||||
val address = getProxyOutbound()?.getServerAddress().orEmpty()
|
val address = getProxyOutbound()?.getServerAddress().orEmpty()
|
||||||
val port = getProxyOutbound()?.getServerPort()
|
val port = getProxyOutbound()?.getServerPort()
|
||||||
return if (Utils.isIpv6Address(address)) {
|
return Utils.getIpv6Address(address) + ":" + port
|
||||||
String.format("[%s]:%s", address, port)
|
|
||||||
} else {
|
|
||||||
String.format("%s:%s", address, port)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,9 @@ data class SubscriptionItem(
|
|||||||
var remarks: String = "",
|
var remarks: String = "",
|
||||||
var url: String = "",
|
var url: String = "",
|
||||||
var enabled: Boolean = true,
|
var enabled: Boolean = true,
|
||||||
val addedTime: Long = System.currentTimeMillis()) {
|
val addedTime: Long = System.currentTimeMillis(),
|
||||||
}
|
var lastUpdated: Long = -1,
|
||||||
|
var autoUpdate: Boolean = false,
|
||||||
|
val updateInterval: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.google.gson.reflect.TypeToken
|
|||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
data class V2rayConfig(
|
data class V2rayConfig(
|
||||||
|
var remarks: String? = null,
|
||||||
var stats: Any? = null,
|
var stats: Any? = null,
|
||||||
val log: LogBean,
|
val log: LogBean,
|
||||||
var policy: PolicyBean?,
|
var policy: PolicyBean?,
|
||||||
@@ -21,7 +22,9 @@ data class V2rayConfig(
|
|||||||
val transport: Any? = null,
|
val transport: Any? = null,
|
||||||
val reverse: Any? = null,
|
val reverse: Any? = null,
|
||||||
var fakedns: Any? = null,
|
var fakedns: Any? = null,
|
||||||
val browserForwarder: Any? = null) {
|
val browserForwarder: Any? = null,
|
||||||
|
var observatory: Any? = null,
|
||||||
|
var burstObservatory: Any? = null) {
|
||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_PORT = 443
|
const val DEFAULT_PORT = 443
|
||||||
const val DEFAULT_SECURITY = "auto"
|
const val DEFAULT_SECURITY = "auto"
|
||||||
@@ -29,7 +32,7 @@ data class V2rayConfig(
|
|||||||
const val DEFAULT_NETWORK = "tcp"
|
const val DEFAULT_NETWORK = "tcp"
|
||||||
|
|
||||||
const val TLS = "tls"
|
const val TLS = "tls"
|
||||||
const val XTLS = "xtls"
|
const val REALITY = "reality"
|
||||||
const val HTTP = "http"
|
const val HTTP = "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,24 +60,26 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class SniffingBean(var enabled: Boolean,
|
data class SniffingBean(var enabled: Boolean,
|
||||||
val destOverride: ArrayList<String>,
|
val destOverride: ArrayList<String>,
|
||||||
val metadataOnly: Boolean? = null)
|
val metadataOnly: Boolean? = null,
|
||||||
|
var routeOnly: Boolean? = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class OutboundBean(val tag: String = "proxy",
|
data class OutboundBean(var tag: String = "proxy",
|
||||||
var protocol: String,
|
var protocol: String,
|
||||||
var settings: OutSettingsBean? = null,
|
var settings: OutSettingsBean? = null,
|
||||||
var streamSettings: StreamSettingsBean? = null,
|
var streamSettings: StreamSettingsBean? = null,
|
||||||
val proxySettings: Any? = null,
|
val proxySettings: Any? = null,
|
||||||
val sendThrough: String? = null,
|
val sendThrough: String? = null,
|
||||||
val mux: MuxBean? = MuxBean(false)) {
|
var mux: MuxBean? = MuxBean(false)) {
|
||||||
|
|
||||||
data class OutSettingsBean(var vnext: List<VnextBean>? = null,
|
data class OutSettingsBean(var vnext: List<VnextBean>? = null,
|
||||||
|
var fragment: FragmentBean? = null,
|
||||||
var servers: List<ServersBean>? = null,
|
var servers: List<ServersBean>? = null,
|
||||||
/*Blackhole*/
|
/*Blackhole*/
|
||||||
var response: Response? = null,
|
var response: Response? = null,
|
||||||
/*DNS*/
|
/*DNS*/
|
||||||
val network: String? = null,
|
val network: String? = null,
|
||||||
val address: Any? = null,
|
var address: Any? = null,
|
||||||
val port: Int? = null,
|
val port: Int? = null,
|
||||||
/*Freedom*/
|
/*Freedom*/
|
||||||
var domainStrategy: String? = null,
|
var domainStrategy: String? = null,
|
||||||
@@ -83,8 +88,10 @@ data class V2rayConfig(
|
|||||||
/*Loopback*/
|
/*Loopback*/
|
||||||
val inboundTag: String? = null,
|
val inboundTag: String? = null,
|
||||||
/*Wireguard*/
|
/*Wireguard*/
|
||||||
val secretKey: String? = null,
|
var secretKey: String? = null,
|
||||||
val peers: List<WireGuardBean>? = null,
|
val peers: List<WireGuardBean>? = null,
|
||||||
|
var reserved: List<Int>? = null,
|
||||||
|
var mtu :Int? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class VnextBean(var address: String = "",
|
data class VnextBean(var address: String = "",
|
||||||
@@ -99,6 +106,10 @@ data class V2rayConfig(
|
|||||||
var flow: String = "")
|
var flow: String = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class FragmentBean(var packets: String? = null,
|
||||||
|
var length: String? = null,
|
||||||
|
var interval: String? = null)
|
||||||
|
|
||||||
data class ServersBean(var address: String = "",
|
data class ServersBean(var address: String = "",
|
||||||
var method: String = "chacha20-poly1305",
|
var method: String = "chacha20-poly1305",
|
||||||
var ota: Boolean = false,
|
var ota: Boolean = false,
|
||||||
@@ -127,13 +138,15 @@ data class V2rayConfig(
|
|||||||
var tcpSettings: TcpSettingsBean? = null,
|
var tcpSettings: TcpSettingsBean? = null,
|
||||||
var kcpSettings: KcpSettingsBean? = null,
|
var kcpSettings: KcpSettingsBean? = null,
|
||||||
var wsSettings: WsSettingsBean? = null,
|
var wsSettings: WsSettingsBean? = null,
|
||||||
|
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
||||||
|
var splithttpSettings: SplithttpSettingsBean? = null,
|
||||||
var httpSettings: HttpSettingsBean? = null,
|
var httpSettings: HttpSettingsBean? = null,
|
||||||
var tlsSettings: TlsSettingsBean? = null,
|
var tlsSettings: TlsSettingsBean? = null,
|
||||||
var quicSettings: QuicSettingBean? = null,
|
var quicSettings: QuicSettingBean? = null,
|
||||||
var xtlsSettings: TlsSettingsBean? = null,
|
var realitySettings: TlsSettingsBean? = null,
|
||||||
var grpcSettings: GrpcSettingsBean? = null,
|
var grpcSettings: GrpcSettingsBean? = null,
|
||||||
val dsSettings: Any? = null,
|
val dsSettings: Any? = null,
|
||||||
val sockopt: Any? = null
|
var sockopt: SockoptBean? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
|
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
|
||||||
@@ -145,7 +158,7 @@ data class V2rayConfig(
|
|||||||
var headers: HeadersBean = HeadersBean(),
|
var headers: HeadersBean = HeadersBean(),
|
||||||
val version: String? = null,
|
val version: String? = null,
|
||||||
val method: String? = null) {
|
val method: String? = null) {
|
||||||
data class HeadersBean(var Host: List<String> = ArrayList(),
|
data class HeadersBean(var Host: List<String>? = ArrayList(),
|
||||||
@SerializedName("User-Agent")
|
@SerializedName("User-Agent")
|
||||||
val userAgent: List<String>? = null,
|
val userAgent: List<String>? = null,
|
||||||
@SerializedName("Accept-Encoding")
|
@SerializedName("Accept-Encoding")
|
||||||
@@ -176,9 +189,24 @@ data class V2rayConfig(
|
|||||||
data class HeadersBean(var Host: String = "")
|
data class HeadersBean(var Host: String = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class HttpupgradeSettingsBean(var path: String = "",
|
||||||
|
var host: String = "",
|
||||||
|
val acceptProxyProtocol: Boolean? = null)
|
||||||
|
|
||||||
|
data class SplithttpSettingsBean(var path: String = "",
|
||||||
|
var host: String = "",
|
||||||
|
val maxUploadSize: Int? = null,
|
||||||
|
val maxConcurrentUploads: Int? = null)
|
||||||
data class HttpSettingsBean(var host: List<String> = ArrayList(),
|
data class HttpSettingsBean(var host: List<String> = ArrayList(),
|
||||||
var path: String = "")
|
var path: String = "")
|
||||||
|
|
||||||
|
data class SockoptBean(var TcpNoDelay: Boolean? = null,
|
||||||
|
var tcpKeepAliveIdle: Int? = null,
|
||||||
|
var tcpFastOpen: Boolean? = null,
|
||||||
|
var tproxy: String? = null,
|
||||||
|
var mark: Int? = null,
|
||||||
|
var dialerProxy: String? = null)
|
||||||
|
|
||||||
data class TlsSettingsBean(var allowInsecure: Boolean = false,
|
data class TlsSettingsBean(var allowInsecure: Boolean = false,
|
||||||
var serverName: String = "",
|
var serverName: String = "",
|
||||||
val alpn: List<String>? = null,
|
val alpn: List<String>? = null,
|
||||||
@@ -189,7 +217,12 @@ data class V2rayConfig(
|
|||||||
val fingerprint: String? = null,
|
val fingerprint: String? = null,
|
||||||
val certificates: List<Any>? = null,
|
val certificates: List<Any>? = null,
|
||||||
val disableSystemRoot: Boolean? = null,
|
val disableSystemRoot: Boolean? = null,
|
||||||
val enableSessionResumption: Boolean? = null)
|
val enableSessionResumption: Boolean? = null,
|
||||||
|
// REALITY settings
|
||||||
|
val show: Boolean = false,
|
||||||
|
var publicKey: String? = null,
|
||||||
|
var shortId: String? = null,
|
||||||
|
var spiderX: String? = null)
|
||||||
|
|
||||||
data class QuicSettingBean(var security: String = "none",
|
data class QuicSettingBean(var security: String = "none",
|
||||||
var key: String = "",
|
var key: String = "",
|
||||||
@@ -198,10 +231,15 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class GrpcSettingsBean(var serviceName: String = "",
|
data class GrpcSettingsBean(var serviceName: String = "",
|
||||||
var multiMode: Boolean? = null)
|
var authority: String? = null,
|
||||||
|
var multiMode: Boolean? = null,
|
||||||
|
var idle_timeout: Int? = null,
|
||||||
|
var health_check_timeout: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
||||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String {
|
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
||||||
|
authority: String?): String {
|
||||||
var sni = ""
|
var sni = ""
|
||||||
network = transport
|
network = transport
|
||||||
when (network) {
|
when (network) {
|
||||||
@@ -214,7 +252,7 @@ data class V2rayConfig(
|
|||||||
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
tcpSetting.header.request = requestObj
|
tcpSetting.header.request = requestObj
|
||||||
sni = requestObj.headers.Host.getOrNull(0) ?: sni
|
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tcpSetting.header.type = "none"
|
tcpSetting.header.type = "none"
|
||||||
@@ -239,6 +277,20 @@ data class V2rayConfig(
|
|||||||
wssetting.path = path ?: "/"
|
wssetting.path = path ?: "/"
|
||||||
wsSettings = wssetting
|
wsSettings = wssetting
|
||||||
}
|
}
|
||||||
|
"httpupgrade" -> {
|
||||||
|
val httpupgradeSetting = HttpupgradeSettingsBean()
|
||||||
|
httpupgradeSetting.host = host ?: ""
|
||||||
|
sni = httpupgradeSetting.host
|
||||||
|
httpupgradeSetting.path = path ?: "/"
|
||||||
|
httpupgradeSettings = httpupgradeSetting
|
||||||
|
}
|
||||||
|
"splithttp" -> {
|
||||||
|
val splithttpSetting = SplithttpSettingsBean()
|
||||||
|
splithttpSetting.host = host ?: ""
|
||||||
|
sni = splithttpSetting.host
|
||||||
|
splithttpSetting.path = path ?: "/"
|
||||||
|
splithttpSettings = splithttpSetting
|
||||||
|
}
|
||||||
"h2", "http" -> {
|
"h2", "http" -> {
|
||||||
network = "h2"
|
network = "h2"
|
||||||
val h2Setting = HttpSettingsBean()
|
val h2Setting = HttpSettingsBean()
|
||||||
@@ -258,32 +310,42 @@ data class V2rayConfig(
|
|||||||
val grpcSetting = GrpcSettingsBean()
|
val grpcSetting = GrpcSettingsBean()
|
||||||
grpcSetting.multiMode = mode == "multi"
|
grpcSetting.multiMode = mode == "multi"
|
||||||
grpcSetting.serviceName = serviceName ?: ""
|
grpcSetting.serviceName = serviceName ?: ""
|
||||||
sni = host ?: ""
|
grpcSetting.authority = authority ?: ""
|
||||||
|
grpcSetting.idle_timeout = 60
|
||||||
|
grpcSetting.health_check_timeout = 20
|
||||||
|
sni = authority ?: ""
|
||||||
grpcSettings = grpcSetting
|
grpcSettings = grpcSetting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sni
|
return sni
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?) {
|
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
|
||||||
|
publicKey: String?, shortId: String?, spiderX: String?) {
|
||||||
security = streamSecurity
|
security = streamSecurity
|
||||||
val tlsSetting = TlsSettingsBean(
|
val tlsSetting = TlsSettingsBean(
|
||||||
allowInsecure = allowInsecure,
|
allowInsecure = allowInsecure,
|
||||||
serverName = sni,
|
serverName = sni,
|
||||||
fingerprint = fingerprint,
|
fingerprint = fingerprint,
|
||||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
||||||
|
publicKey = publicKey,
|
||||||
|
shortId = shortId,
|
||||||
|
spiderX = spiderX
|
||||||
)
|
)
|
||||||
if (security == TLS) {
|
if (security == TLS) {
|
||||||
tlsSettings = tlsSetting
|
tlsSettings = tlsSetting
|
||||||
xtlsSettings = null
|
realitySettings = null
|
||||||
} else if (security == XTLS) {
|
} else if (security == REALITY) {
|
||||||
tlsSettings = null
|
tlsSettings = null
|
||||||
xtlsSettings = tlsSetting
|
realitySettings = tlsSetting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MuxBean(var enabled: Boolean, var concurrency: Int = 8)
|
data class MuxBean(var enabled: Boolean,
|
||||||
|
var concurrency: Int = 8,
|
||||||
|
var xudpConcurrency: Int = 8,
|
||||||
|
var xudpProxyUDP443: String = "",)
|
||||||
|
|
||||||
fun getServerAddress(): String? {
|
fun getServerAddress(): String? {
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
@@ -340,7 +402,8 @@ data class V2rayConfig(
|
|||||||
fun getTransportSettingDetails(): List<String>? {
|
fun getTransportSettingDetails(): List<String>? {
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)) {
|
||||||
val transport = streamSettings?.network ?: return null
|
val transport = streamSettings?.network ?: return null
|
||||||
return when (transport) {
|
return when (transport) {
|
||||||
"tcp" -> {
|
"tcp" -> {
|
||||||
@@ -361,6 +424,18 @@ data class V2rayConfig(
|
|||||||
wsSetting.headers.Host,
|
wsSetting.headers.Host,
|
||||||
wsSetting.path)
|
wsSetting.path)
|
||||||
}
|
}
|
||||||
|
"httpupgrade" -> {
|
||||||
|
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
||||||
|
listOf("",
|
||||||
|
httpupgradeSetting.host,
|
||||||
|
httpupgradeSetting.path)
|
||||||
|
}
|
||||||
|
"splithttp" -> {
|
||||||
|
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
|
||||||
|
listOf("",
|
||||||
|
splithttpSetting.host,
|
||||||
|
splithttpSetting.path)
|
||||||
|
}
|
||||||
"h2" -> {
|
"h2" -> {
|
||||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
val h2Setting = streamSettings?.httpSettings ?: return null
|
||||||
listOf("",
|
listOf("",
|
||||||
@@ -376,7 +451,7 @@ data class V2rayConfig(
|
|||||||
"grpc" -> {
|
"grpc" -> {
|
||||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
||||||
listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
|
listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
|
||||||
"",
|
grpcSetting.authority ?: "",
|
||||||
grpcSetting.serviceName)
|
grpcSetting.serviceName)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
@@ -405,7 +480,7 @@ data class V2rayConfig(
|
|||||||
var rules: ArrayList<RulesBean>,
|
var rules: ArrayList<RulesBean>,
|
||||||
val balancers: List<Any>? = null) {
|
val balancers: List<Any>? = null) {
|
||||||
|
|
||||||
data class RulesBean(var type: String = "",
|
data class RulesBean(
|
||||||
var ip: ArrayList<String>? = null,
|
var ip: ArrayList<String>? = null,
|
||||||
var domain: ArrayList<String>? = null,
|
var domain: ArrayList<String>? = null,
|
||||||
var outboundTag: String = "",
|
var outboundTag: String = "",
|
||||||
@@ -438,8 +513,8 @@ data class V2rayConfig(
|
|||||||
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
|
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
|
||||||
|
|
||||||
fun getProxyOutbound(): OutboundBean? {
|
fun getProxyOutbound(): OutboundBean? {
|
||||||
outbounds.forEach { outbound ->
|
outbounds?.forEach { outbound ->
|
||||||
EConfigType.values().forEach {
|
EConfigType.entries.forEach {
|
||||||
if (outbound.protocol.equals(it.name, true)) {
|
if (outbound.protocol.equals(it.name, true)) {
|
||||||
return outbound
|
return outbound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ data class VmessQRCode(var v: String = "",
|
|||||||
var path: String = "",
|
var path: String = "",
|
||||||
var tls: String = "",
|
var tls: String = "",
|
||||||
var sni: String = "",
|
var sni: String = "",
|
||||||
var alpn: String = "")
|
var alpn: String = "",
|
||||||
|
var fp: String = "")
|
||||||
@@ -9,72 +9,61 @@ import org.json.JSONObject
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
|
|
||||||
/**
|
|
||||||
* Some extensions
|
|
||||||
*/
|
|
||||||
|
|
||||||
val Context.v2RayApplication: AngApplication
|
val Context.v2RayApplication: AngApplication
|
||||||
get() = applicationContext as AngApplication
|
get() = applicationContext as AngApplication
|
||||||
|
|
||||||
fun Context.toast(message: Int): Toast = ToastCompat
|
fun Context.toast(message: Int) {
|
||||||
.makeText(this, message, Toast.LENGTH_SHORT)
|
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).apply { show() }
|
||||||
.apply {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.toast(message: CharSequence): Toast = ToastCompat
|
|
||||||
.makeText(this, message, Toast.LENGTH_SHORT)
|
|
||||||
.apply {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun JSONObject.putOpt(pair: Pair<String, Any>) = putOpt(pair.first, pair.second)
|
|
||||||
fun JSONObject.putOpt(pairs: Map<String, Any>) = pairs.forEach { putOpt(it.key to it.value) }
|
|
||||||
|
|
||||||
const val threshold = 1000
|
|
||||||
const val divisor = 1024F
|
|
||||||
|
|
||||||
fun Long.toSpeedString() = toTrafficString() + "/s"
|
|
||||||
|
|
||||||
fun Long.toTrafficString(): String {
|
|
||||||
if (this == 0L)
|
|
||||||
return "\t\t\t0\t B"
|
|
||||||
|
|
||||||
if (this < threshold)
|
|
||||||
return "${this.toFloat().toShortString()}\t B"
|
|
||||||
|
|
||||||
val kib = this / divisor
|
|
||||||
if (kib < threshold)
|
|
||||||
return "${kib.toShortString()}\t KB"
|
|
||||||
|
|
||||||
val mib = kib / divisor
|
|
||||||
if (mib < threshold)
|
|
||||||
return "${mib.toShortString()}\t MB"
|
|
||||||
|
|
||||||
val gib = mib / divisor
|
|
||||||
if (gib < threshold)
|
|
||||||
return "${gib.toShortString()}\t GB"
|
|
||||||
|
|
||||||
val tib = gib / divisor
|
|
||||||
if (tib < threshold)
|
|
||||||
return "${tib.toShortString()}\t TB"
|
|
||||||
|
|
||||||
val pib = tib / divisor
|
|
||||||
if (pib < threshold)
|
|
||||||
return "${pib.toShortString()}\t PB"
|
|
||||||
|
|
||||||
return "∞"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Float.toShortString(): String {
|
fun Context.toast(message: CharSequence) {
|
||||||
val s = "%.2f".format(this)
|
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).apply { show() }
|
||||||
if (s.length <= 4)
|
}
|
||||||
return s
|
|
||||||
return s.substring(0, 4).removeSuffix(".")
|
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
|
||||||
|
put(pair.first, pair.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun JSONObject.putOpt(pairs: Map<String, Any?>) {
|
||||||
|
pairs.forEach { put(it.key, it.value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const val THRESHOLD = 1000L
|
||||||
|
const val DIVISOR = 1024.0
|
||||||
|
|
||||||
|
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
||||||
|
|
||||||
|
fun Long.toTrafficString(): String {
|
||||||
|
if (this < THRESHOLD) {
|
||||||
|
return "$this B"
|
||||||
|
}
|
||||||
|
val kb = this / DIVISOR
|
||||||
|
if (kb < THRESHOLD) {
|
||||||
|
return "${String.format("%.1f KB", kb)}"
|
||||||
|
}
|
||||||
|
val mb = kb / DIVISOR
|
||||||
|
if (mb < THRESHOLD) {
|
||||||
|
return "${String.format("%.1f MB", mb)}"
|
||||||
|
}
|
||||||
|
val gb = mb / DIVISOR
|
||||||
|
if (gb < THRESHOLD) {
|
||||||
|
return "${String.format("%.1f GB", gb)}"
|
||||||
|
}
|
||||||
|
val tb = gb / DIVISOR
|
||||||
|
if (tb < THRESHOLD) {
|
||||||
|
return "${String.format("%.1f TB", tb)}"
|
||||||
|
}
|
||||||
|
return String.format("%.1f PB", tb / DIVISOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
val URLConnection.responseLength: Long
|
val URLConnection.responseLength: Long
|
||||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) contentLengthLong else contentLength.toLong()
|
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
contentLengthLong
|
||||||
|
} else {
|
||||||
|
contentLength.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
val URI.idnHost: String
|
val URI.idnHost: String
|
||||||
get() = (host!!).replace("[", "").replace("]", "")
|
get() = host?.replace("[", "")?.replace("]", "") ?: ""
|
||||||
|
|
||||||
|
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.google.zxing.WriterException
|
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
@@ -34,7 +33,7 @@ class TaskerReceiver : BroadcastReceiver() {
|
|||||||
} else {
|
} else {
|
||||||
Utils.stopVService(context)
|
Utils.stopVService(context)
|
||||||
}
|
}
|
||||||
} catch (e: WriterException) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
@@ -38,17 +38,11 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
})
|
})
|
||||||
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
|
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
remoteViews.setInt(
|
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
|
||||||
R.id.layout_switch,
|
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_active)
|
||||||
"setBackgroundResource",
|
|
||||||
R.drawable.ic_rounded_corner_theme
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
remoteViews.setInt(
|
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_play_24dp)
|
||||||
R.id.layout_switch,
|
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_inactive)
|
||||||
"setBackgroundResource",
|
|
||||||
R.drawable.ic_rounded_corner_grey
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (appWidgetId in appWidgetIds) {
|
for (appWidgetId in appWidgetIds) {
|
||||||
|
|||||||
@@ -36,7 +36,12 @@ class QSTileService : TileService() {
|
|||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
setState(Tile.STATE_INACTIVE)
|
setState(Tile.STATE_INACTIVE)
|
||||||
mMsgReceive = ReceiveMessageHandler(this)
|
mMsgReceive = ReceiveMessageHandler(this)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), Context.RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
||||||
|
}
|
||||||
|
|
||||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
object SubscriptionUpdater {
|
||||||
|
|
||||||
|
const val notificationChannel = "subscription_update_channel"
|
||||||
|
|
||||||
|
class UpdateTask(context: Context, params: WorkerParameters) :
|
||||||
|
CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
private val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
|
private val notification =
|
||||||
|
NotificationCompat.Builder(applicationContext, notificationChannel)
|
||||||
|
.setWhen(0)
|
||||||
|
.setTicker("Update")
|
||||||
|
.setContentTitle(context.getString(R.string.title_pref_auto_update_subscription))
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_name)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, "subscription automatic update starting")
|
||||||
|
|
||||||
|
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
|
||||||
|
|
||||||
|
for (i in subs) {
|
||||||
|
val subscription = i.second
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
notification.setChannelId(notificationChannel)
|
||||||
|
val channel =
|
||||||
|
NotificationChannel(
|
||||||
|
notificationChannel,
|
||||||
|
"Subscription Update Service",
|
||||||
|
NotificationManager.IMPORTANCE_MIN
|
||||||
|
)
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
notificationManager.notify(3, notification.build())
|
||||||
|
Log.d(
|
||||||
|
AppConfig.ANG_PACKAGE,
|
||||||
|
"subscription automatic update: ---${subscription.remarks}"
|
||||||
|
)
|
||||||
|
val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url)
|
||||||
|
AngConfigManager.importBatchConfig(configs, i.first, false)
|
||||||
|
notification.setContentText("Updating ${subscription.remarks}")
|
||||||
|
}
|
||||||
|
notificationManager.cancel(3)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.v2ray.ang.service
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
import android.app.*
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -50,7 +53,7 @@ object V2RayServiceManager {
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()))
|
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
||||||
}
|
}
|
||||||
var currentConfig: ServerConfig? = null
|
var currentConfig: ServerConfig? = null
|
||||||
|
|
||||||
@@ -60,6 +63,11 @@ object V2RayServiceManager {
|
|||||||
private var mNotificationManager: NotificationManager? = null
|
private var mNotificationManager: NotificationManager? = null
|
||||||
|
|
||||||
fun startV2Ray(context: Context) {
|
fun startV2Ray(context: Context) {
|
||||||
|
if (v2rayPoint.isRunning) return
|
||||||
|
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
|
||||||
|
val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||||
|
if (!result.status) return
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
|
||||||
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
||||||
} else {
|
} else {
|
||||||
@@ -123,7 +131,9 @@ object V2RayServiceManager {
|
|||||||
val service = serviceControl?.get()?.getService() ?: return
|
val service = serviceControl?.get()?.getService() ?: return
|
||||||
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
|
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||||
if (!v2rayPoint.isRunning) {
|
if (v2rayPoint.isRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
|
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
|
||||||
if (!result.status)
|
if (!result.status)
|
||||||
return
|
return
|
||||||
@@ -133,7 +143,11 @@ object V2RayServiceManager {
|
|||||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
service.registerReceiver(mMsgReceive, mFilter)
|
service.registerReceiver(mMsgReceive, mFilter)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
@@ -156,7 +170,6 @@ object V2RayServiceManager {
|
|||||||
cancelNotification()
|
cancelNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun stopV2rayPoint() {
|
fun stopV2rayPoint() {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
val service = serviceControl?.get()?.getService() ?: return
|
||||||
@@ -230,11 +243,19 @@ object V2RayServiceManager {
|
|||||||
var errstr = ""
|
var errstr = ""
|
||||||
if (v2rayPoint.isRunning) {
|
if (v2rayPoint.isRunning) {
|
||||||
try {
|
try {
|
||||||
time = v2rayPoint.measureDelay()
|
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
||||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
}
|
}
|
||||||
|
if (time == -1L) {
|
||||||
|
try {
|
||||||
|
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(true))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
||||||
|
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val result = if (time == -1L) {
|
val result = if (time == -1L) {
|
||||||
service.getString(R.string.connection_test_error, errstr)
|
service.getString(R.string.connection_test_error, errstr)
|
||||||
@@ -286,7 +307,7 @@ object V2RayServiceManager {
|
|||||||
.setShowWhen(false)
|
.setShowWhen(false)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setContentIntent(contentPendingIntent)
|
.setContentIntent(contentPendingIntent)
|
||||||
.addAction(R.drawable.ic_close_grey_800_24dp,
|
.addAction(R.drawable.ic_delete_24dp,
|
||||||
service.getString(R.string.notification_action_stop_v2ray),
|
service.getString(R.string.notification_action_stop_v2ray),
|
||||||
stopV2RayPendingIntent)
|
stopV2RayPendingIntent)
|
||||||
//.build()
|
//.build()
|
||||||
@@ -364,7 +385,7 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink")
|
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink")
|
||||||
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink")
|
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink")
|
||||||
val zeroSpeed = (proxyTotal == 0L && directUplink == 0L && directDownlink == 0L)
|
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
||||||
if (!zeroSpeed || !lastZeroSpeed) {
|
if (!zeroSpeed || !lastZeroSpeed) {
|
||||||
if (proxyTotal == 0L) {
|
if (proxyTotal == 0L) {
|
||||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class V2RayTestService : Service() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Seq.setContext(this)
|
Seq.setContext(this)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(this))
|
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
val builder = Builder()
|
val builder = Builder()
|
||||||
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||||
|
|
||||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
|
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
||||||
|
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
||||||
|
|
||||||
builder.setMtu(VPN_MTU)
|
builder.setMtu(VPN_MTU)
|
||||||
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||||
|
|||||||
177
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/AboutActivity.kt
Normal file
177
V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/AboutActivity.kt
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityAboutBinding
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.util.SpeedtestUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.util.ZipUtil
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class AboutActivity : BaseActivity() {
|
||||||
|
private lateinit var binding: ActivityAboutBinding
|
||||||
|
private val extDir by lazy { File(Utils.backupPath(this)) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
title = getString(R.string.title_about)
|
||||||
|
|
||||||
|
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
|
||||||
|
binding.layoutBackup.setOnClickListener {
|
||||||
|
val ret = backupConfiguration(extDir.absolutePath)
|
||||||
|
if (ret.first) {
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.layoutShare.setOnClickListener {
|
||||||
|
val ret = backupConfiguration(cacheDir.absolutePath)
|
||||||
|
if (ret.first) {
|
||||||
|
startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_SEND).setType("application/zip")
|
||||||
|
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
.putExtra(
|
||||||
|
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
|
||||||
|
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
|
||||||
|
)
|
||||||
|
), getString(R.string.title_configuration_share)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.layoutRestore.setOnClickListener {
|
||||||
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
|
} else {
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
|
RxPermissions(this)
|
||||||
|
.request(permission)
|
||||||
|
.subscribe {
|
||||||
|
if (it) {
|
||||||
|
try {
|
||||||
|
showFileChooser()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.layoutSoureCcode.setOnClickListener {
|
||||||
|
Utils.openUri(this, AppConfig.v2rayNGUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.layoutFeedback.setOnClickListener {
|
||||||
|
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.layoutTgChannel.setOnClickListener {
|
||||||
|
Utils.openUri(this, AppConfig.TgChannelUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.layoutPrivacyPolicy.setOnClickListener {
|
||||||
|
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
"v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})".also {
|
||||||
|
binding.tvVersion.text = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
|
||||||
|
val dateFormated = SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd-HH-mm-ss",
|
||||||
|
Locale.getDefault()
|
||||||
|
).format(System.currentTimeMillis())
|
||||||
|
val folderName = "${getString(R.string.app_name)}_${dateFormated}"
|
||||||
|
val backupDir = this.cacheDir.absolutePath + "/$folderName"
|
||||||
|
val outputZipFilePath = "$outputZipFilePos/$folderName.zip"
|
||||||
|
|
||||||
|
val count = MMKV.backupAllToDirectory(backupDir)
|
||||||
|
if (count <= 0) {
|
||||||
|
return Pair(false, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) {
|
||||||
|
return Pair(true, outputZipFilePath)
|
||||||
|
} else {
|
||||||
|
return Pair(false, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreConfiguration(zipFile: File): Boolean {
|
||||||
|
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
|
||||||
|
|
||||||
|
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val count = MMKV.restoreAllFromDirectory(backupDir)
|
||||||
|
return count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFileChooser() {
|
||||||
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
|
intent.type = "*/*"
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
|
try {
|
||||||
|
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||||
|
} catch (ex: android.content.ActivityNotFoundException) {
|
||||||
|
toast(R.string.toast_require_file_manager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chooseFile =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
val uri = it.data?.data
|
||||||
|
if (it.resultCode == RESULT_OK && uri != null) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
val targetFile =
|
||||||
|
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
||||||
|
contentResolver.openInputStream(uri).use { input ->
|
||||||
|
targetFile.outputStream().use { fileOut ->
|
||||||
|
input?.copyTo(fileOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (restoreConfiguration(targetFile)) {
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
toast(e.message.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,24 @@ package com.v2ray.ang.ui
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
if (!Utils.getDarkModeStatus(this)) {
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
isAppearanceLightStatusBars = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
@@ -19,27 +29,6 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
checkDarkMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkDarkMode() {
|
|
||||||
if (Utils.getDarkModeStatus(this)) {
|
|
||||||
if (this.javaClass.simpleName == "MainActivity") {
|
|
||||||
setTheme(R.style.AppThemeDark_NoActionBar)
|
|
||||||
} else {
|
|
||||||
setTheme(R.style.AppThemeDark)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.javaClass.simpleName == "MainActivity") {
|
|
||||||
setTheme(R.style.AppThemeLight_NoActionBar)
|
|
||||||
} else {
|
|
||||||
setTheme(R.style.AppThemeLight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val context = newBase?.let {
|
val context = newBase?.let {
|
||||||
@@ -47,7 +36,4 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
super.attachBaseContext(context)
|
super.attachBaseContext(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,6 @@ class LogcatActivity : BaseActivity() {
|
|||||||
|
|
||||||
title = getString(R.string.title_logcat)
|
title = getString(R.string.title_logcat)
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
logcat(false)
|
logcat(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,50 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.*
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import android.os.Build
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import com.tbruyelle.rxpermissions.RxPermissions
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.v2ray.ang.AppConfig
|
import android.view.Menu
|
||||||
import android.content.res.ColorStateList
|
import android.view.MenuItem
|
||||||
import com.google.android.material.navigation.NavigationView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.navigation.NavigationView
|
||||||
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||||
|
import com.v2ray.ang.databinding.LayoutProgressBinding
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.drakeet.support.toast.ToastCompat
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
|
||||||
import com.v2ray.ang.util.*
|
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import me.drakeet.support.toast.ToastCompat
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
@@ -68,7 +71,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
binding.fab.setOnClickListener {
|
binding.fab.setOnClickListener {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
Utils.stopVService(this)
|
Utils.stopVService(this)
|
||||||
} else if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
|
} else if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") {
|
||||||
val intent = VpnService.prepare(this)
|
val intent = VpnService.prepare(this)
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
@@ -102,11 +105,30 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
binding.drawerLayout.addDrawerListener(toggle)
|
binding.drawerLayout.addDrawerListener(toggle)
|
||||||
toggle.syncState()
|
toggle.syncState()
|
||||||
binding.navView.setNavigationItemSelectedListener(this)
|
binding.navView.setNavigationItemSelectedListener(this)
|
||||||
binding.version.text = "v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})"
|
|
||||||
|
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
copyAssets()
|
mainViewModel.copyAssets(assets)
|
||||||
migrateLegacy()
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
RxPermissions(this)
|
||||||
|
.request(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
.subscribe {
|
||||||
|
if (!it)
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
|
} else {
|
||||||
|
//super.onBackPressed()
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViewModel() {
|
private fun setupViewModel() {
|
||||||
@@ -121,66 +143,25 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||||
adapter.isRunning = isRunning
|
adapter.isRunning = isRunning
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorSelected))
|
binding.fab.setImageResource(R.drawable.ic_stop_24dp)
|
||||||
|
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
|
||||||
setTestState(getString(R.string.connection_connected))
|
setTestState(getString(R.string.connection_connected))
|
||||||
binding.layoutTest.isFocusable = true
|
binding.layoutTest.isFocusable = true
|
||||||
} else {
|
} else {
|
||||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorUnselected))
|
binding.fab.setImageResource(R.drawable.ic_play_24dp)
|
||||||
|
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||||
setTestState(getString(R.string.connection_not_connected))
|
setTestState(getString(R.string.connection_not_connected))
|
||||||
binding.layoutTest.isFocusable = false
|
binding.layoutTest.isFocusable = false
|
||||||
}
|
}
|
||||||
hideCircle()
|
|
||||||
}
|
}
|
||||||
mainViewModel.startListenBroadcast()
|
mainViewModel.startListenBroadcast()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyAssets() {
|
|
||||||
val extFolder = Utils.userAssetPath(this)
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val geo = arrayOf("geosite.dat", "geoip.dat")
|
|
||||||
assets.list("")
|
|
||||||
?.filter { geo.contains(it) }
|
|
||||||
?.filter { !File(extFolder, it).exists() }
|
|
||||||
?.forEach {
|
|
||||||
val target = File(extFolder, it)
|
|
||||||
assets.open(it).use { input ->
|
|
||||||
FileOutputStream(target).use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.i(ANG_PACKAGE, "Copied from apk assets folder to ${target.absolutePath}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrateLegacy() {
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = AngConfigManager.migrateLegacyConfig(this@MainActivity)
|
|
||||||
if (result != null) {
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (result) {
|
|
||||||
toast(getString(R.string.migration_success))
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
} else {
|
|
||||||
toast(getString(R.string.migration_fail))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startV2Ray() {
|
fun startV2Ray() {
|
||||||
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
showCircle()
|
|
||||||
// toast(R.string.toast_services_start)
|
|
||||||
V2RayServiceManager.startV2Ray(this)
|
V2RayServiceManager.startV2Ray(this)
|
||||||
hideCircle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartV2Ray() {
|
fun restartV2Ray() {
|
||||||
@@ -237,6 +218,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
importManually(EConfigType.TROJAN.value)
|
importManually(EConfigType.TROJAN.value)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.import_manually_wireguard -> {
|
||||||
|
importManually(EConfigType.WIREGUARD.value)
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.import_config_custom_clipboard -> {
|
R.id.import_config_custom_clipboard -> {
|
||||||
importConfigCustomClipboard()
|
importConfigCustomClipboard()
|
||||||
true
|
true
|
||||||
@@ -254,11 +239,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// R.id.sub_setting -> {
|
|
||||||
// startActivity<SubSettingActivity>()
|
|
||||||
// true
|
|
||||||
// }
|
|
||||||
|
|
||||||
R.id.sub_update -> {
|
R.id.sub_update -> {
|
||||||
importConfigViaSub()
|
importConfigViaSub()
|
||||||
true
|
true
|
||||||
@@ -294,16 +274,33 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
MmkvManager.removeAllServer()
|
MmkvManager.removeAllServer()
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.del_duplicate_config-> {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mainViewModel.removeDuplicateServer()
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.del_invalid_config -> {
|
R.id.del_invalid_config -> {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
MmkvManager.removeInvalidServer()
|
MmkvManager.removeInvalidServer()
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -332,7 +329,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
/**
|
/**
|
||||||
* import config from qrcode
|
* import config from qrcode
|
||||||
*/
|
*/
|
||||||
fun importQRcode(forConfig: Boolean): Boolean {
|
private fun importQRcode(forConfig: Boolean): Boolean {
|
||||||
// try {
|
// try {
|
||||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
||||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
// .addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
@@ -368,7 +365,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
/**
|
/**
|
||||||
* import config from clipboard
|
* import config from clipboard
|
||||||
*/
|
*/
|
||||||
fun importClipboard()
|
private fun importClipboard()
|
||||||
: Boolean {
|
: Boolean {
|
||||||
try {
|
try {
|
||||||
val clipboard = Utils.getClipboard(this)
|
val clipboard = Utils.getClipboard(this)
|
||||||
@@ -380,27 +377,28 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importBatchConfig(server: String?, subid: String = "") {
|
private fun importBatchConfig(server: String?) {
|
||||||
val subid2 = if(subid.isNullOrEmpty()){
|
val dialog = AlertDialog.Builder(this)
|
||||||
mainViewModel.subscriptionId
|
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
||||||
}else{
|
.setCancelable(false)
|
||||||
subid
|
.show()
|
||||||
}
|
|
||||||
val append = subid.isNullOrEmpty()
|
|
||||||
|
|
||||||
var count = AngConfigManager.importBatchConfig(server, subid2, append)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if (count <= 0) {
|
val count = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
|
||||||
count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid2, append)
|
delay(500L)
|
||||||
}
|
launch(Dispatchers.Main) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importConfigCustomClipboard()
|
private fun importConfigCustomClipboard()
|
||||||
: Boolean {
|
: Boolean {
|
||||||
try {
|
try {
|
||||||
val configText = Utils.getClipboard(this)
|
val configText = Utils.getClipboard(this)
|
||||||
@@ -419,7 +417,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
/**
|
/**
|
||||||
* import config from local config file
|
* import config from local config file
|
||||||
*/
|
*/
|
||||||
fun importConfigCustomLocal(): Boolean {
|
private fun importConfigCustomLocal(): Boolean {
|
||||||
try {
|
try {
|
||||||
showFileChooser()
|
showFileChooser()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -429,7 +427,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importConfigCustomUrlClipboard()
|
private fun importConfigCustomUrlClipboard()
|
||||||
: Boolean {
|
: Boolean {
|
||||||
try {
|
try {
|
||||||
val url = Utils.getClipboard(this)
|
val url = Utils.getClipboard(this)
|
||||||
@@ -447,7 +445,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
/**
|
/**
|
||||||
* import config from url
|
* import config from url
|
||||||
*/
|
*/
|
||||||
fun importConfigCustomUrl(url: String?): Boolean {
|
private fun importConfigCustomUrl(url: String?): Boolean {
|
||||||
try {
|
try {
|
||||||
if (!Utils.isValidUrl(url)) {
|
if (!Utils.isValidUrl(url)) {
|
||||||
toast(R.string.toast_invalid_url)
|
toast(R.string.toast_invalid_url)
|
||||||
@@ -474,43 +472,24 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
/**
|
/**
|
||||||
* import config from sub
|
* import config from sub
|
||||||
*/
|
*/
|
||||||
fun importConfigViaSub()
|
private fun importConfigViaSub() : Boolean {
|
||||||
: Boolean {
|
val dialog = AlertDialog.Builder(this)
|
||||||
try {
|
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
||||||
toast(R.string.title_sub_update)
|
.setCancelable(false)
|
||||||
MmkvManager.decodeSubscriptions().forEach {
|
.show()
|
||||||
if (TextUtils.isEmpty(it.first)
|
|
||||||
|| TextUtils.isEmpty(it.second.remarks)
|
|
||||||
|| TextUtils.isEmpty(it.second.url)
|
|
||||||
) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
if (!it.second.enabled) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
val url = Utils.idnToASCII(it.second.url)
|
|
||||||
if (!Utils.isValidUrl(url)) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
Log.d(ANG_PACKAGE, url)
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val configText = try {
|
val count = AngConfigManager.updateConfigViaSubAll()
|
||||||
Utils.getUrlContentWithCustomUserAgent(url)
|
delay(500L)
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure))
|
if (count > 0) {
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
return@launch
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
importBatchConfig(configText, it.first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -541,8 +520,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
* read content from uri
|
* read content from uri
|
||||||
*/
|
*/
|
||||||
private fun readContentFromUri(uri: Uri) {
|
private fun readContentFromUri(uri: Uri) {
|
||||||
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
|
} else {
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
RxPermissions(this)
|
RxPermissions(this)
|
||||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
.request(permission)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (it) {
|
if (it) {
|
||||||
try {
|
try {
|
||||||
@@ -560,15 +544,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
/**
|
/**
|
||||||
* import customize config
|
* import customize config
|
||||||
*/
|
*/
|
||||||
fun importCustomizeConfig(server: String?) {
|
private fun importCustomizeConfig(server: String?) {
|
||||||
try {
|
try {
|
||||||
if (server == null || TextUtils.isEmpty(server)) {
|
if (server == null || TextUtils.isEmpty(server)) {
|
||||||
toast(R.string.toast_none_data)
|
toast(R.string.toast_none_data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mainViewModel.appendCustomConfigServer(server)
|
if (mainViewModel.appendCustomConfigServer(server)) {
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
|
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
||||||
@@ -577,7 +564,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTestState(content: String?) {
|
private fun setTestState(content: String?) {
|
||||||
binding.tvTestState.text = content
|
binding.tvTestState.text = content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,43 +578,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
|
||||||
moveTaskToBack(false)
|
moveTaskToBack(false)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return super.onKeyDown(keyCode, event)
|
return super.onKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCircle() {
|
|
||||||
binding.fabProgressCircle.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideCircle() {
|
|
||||||
try {
|
|
||||||
Observable.timer(300, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
try {
|
|
||||||
if (binding.fabProgressCircle.isShown) {
|
|
||||||
binding.fabProgressCircle.hide()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(ANG_PACKAGE, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
|
||||||
} else {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||||
// Handle navigation view item clicks here.
|
// Handle navigation view item clicks here.
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
@@ -642,15 +599,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
R.id.user_asset_setting -> {
|
R.id.user_asset_setting -> {
|
||||||
startActivity(Intent(this, UserAssetActivity::class.java))
|
startActivity(Intent(this, UserAssetActivity::class.java))
|
||||||
}
|
}
|
||||||
R.id.feedback -> {
|
|
||||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
|
||||||
}
|
|
||||||
R.id.promotion -> {
|
R.id.promotion -> {
|
||||||
Utils.openUri(this, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
|
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||||
}
|
}
|
||||||
R.id.logcat -> {
|
R.id.logcat -> {
|
||||||
startActivity(Intent(this, LogcatActivity::class.java))
|
startActivity(Intent(this, LogcatActivity::class.java))
|
||||||
}
|
}
|
||||||
|
R.id.about-> {
|
||||||
|
startActivity(Intent(this, AboutActivity::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ package com.v2ray.ang.ui
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AngApplication.Companion.application
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
@@ -72,9 +73,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
||||||
}
|
}
|
||||||
if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorSelected)
|
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorUnselected)
|
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||||
}
|
}
|
||||||
holder.itemMainBinding.tvSubscription.text = ""
|
holder.itemMainBinding.tvSubscription.text = ""
|
||||||
val json = subStorage?.decodeString(config.subscriptionId)
|
val json = subStorage?.decodeString(config.subscriptionId)
|
||||||
@@ -96,7 +97,14 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
|
holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
|
|
||||||
|
val strState = try{
|
||||||
|
"${outbound?.getServerAddress()?.dropLast(3)}*** : ${outbound?.getServerPort()}"
|
||||||
|
}catch(e: Exception){
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemMainBinding.tvStatistics.text = strState
|
||||||
|
|
||||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
holder.itemMainBinding.layoutShare.setOnClickListener {
|
||||||
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
||||||
@@ -143,10 +151,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
removeServer(guid, position)
|
removeServer(guid, position)
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
removeServer(guid, position)
|
removeServer(guid, position)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
application.toast(R.string.toast_action_not_allowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,17 +168,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
if (guid != selected) {
|
if (guid != selected) {
|
||||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||||
if (!TextUtils.isEmpty(selected)) {
|
if (!TextUtils.isEmpty(selected)) {
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
|
notifyItemChanged(mActivity.mainViewModel.getPosition(selected?:""))
|
||||||
}
|
}
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
mActivity.showCircle()
|
|
||||||
Utils.stopVService(mActivity)
|
Utils.stopVService(mActivity)
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe {
|
||||||
V2RayServiceManager.startV2Ray(mActivity)
|
V2RayServiceManager.startV2Ray(mActivity)
|
||||||
mActivity.hideCircle()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +188,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
||||||
} else {
|
} else {
|
||||||
holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
||||||
Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
|
Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,7 +239,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
|
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
|
||||||
|
|
||||||
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
||||||
BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
|
BaseViewHolder(itemFooterBinding.root)
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
|
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -19,21 +19,20 @@ import com.v2ray.ang.dto.AppInfo
|
|||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.v2RayApplication
|
import com.v2ray.ang.extension.v2RayApplication
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class PerAppProxyActivity : BaseActivity() {
|
class PerAppProxyActivity : BaseActivity() {
|
||||||
private lateinit var binding: ActivityBypassListBinding
|
private lateinit var binding: ActivityBypassListBinding
|
||||||
|
|
||||||
private var adapter: PerAppProxyAdapter? = null
|
private var adapter: PerAppProxyAdapter? = null
|
||||||
private var appsAll: List<AppInfo>? = null
|
private var appsAll: List<AppInfo>? = null
|
||||||
private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -41,19 +40,17 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
val view = binding.root
|
val view = binding.root
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||||
binding.recyclerView.addItemDecoration(dividerItemDecoration)
|
binding.recyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
|
|
||||||
val blacklist = defaultSharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, null)
|
val blacklist = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
|
|
||||||
AppManagerUtil.rxLoadNetworkAppList(this)
|
AppManagerUtil.rxLoadNetworkAppList(this)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.map {
|
.map {
|
||||||
if (blacklist != null) {
|
if (blacklist != null) {
|
||||||
it.forEach { one ->
|
it.forEach { one ->
|
||||||
if ((blacklist.contains(one.packageName))) {
|
if (blacklist.contains(one.packageName)) {
|
||||||
one.isSelected = 1
|
one.isSelected = 1
|
||||||
} else {
|
} else {
|
||||||
one.isSelected = 0
|
one.isSelected = 0
|
||||||
@@ -137,14 +134,14 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
***/
|
***/
|
||||||
|
|
||||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||||
defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_PER_APP_PROXY, isChecked).apply()
|
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
||||||
}
|
}
|
||||||
binding.switchPerAppProxy.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
binding.switchPerAppProxy.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
||||||
|
|
||||||
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
|
||||||
defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_BYPASS_APPS, isChecked).apply()
|
settingsStorage.encode(AppConfig.PREF_BYPASS_APPS, isChecked)
|
||||||
}
|
}
|
||||||
binding.switchBypassApps.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
|
binding.switchBypassApps.isChecked = settingsStorage.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
|
||||||
|
|
||||||
/***
|
/***
|
||||||
et_search.setOnEditorActionListener { v, actionId, event ->
|
et_search.setOnEditorActionListener { v, actionId, event ->
|
||||||
@@ -180,7 +177,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
adapter?.let {
|
adapter?.let {
|
||||||
defaultSharedPreferences.edit().putStringSet(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist).apply()
|
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +193,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
filterProxyApp(newText!!)
|
filterProxyApp(newText?:"")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -212,12 +209,12 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
if (it.blacklist.containsAll(pkgNames)) {
|
if (it.blacklist.containsAll(pkgNames)) {
|
||||||
it.apps.forEach {
|
it.apps.forEach {
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
adapter?.blacklist!!.remove(packageName)
|
adapter?.blacklist?.remove(packageName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
it.apps.forEach {
|
it.apps.forEach {
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
adapter?.blacklist!!.add(packageName)
|
adapter?.blacklist?.add(packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
it.notifyDataSetChanged()
|
it.notifyDataSetChanged()
|
||||||
@@ -281,7 +278,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter?.blacklist!!.clear()
|
adapter?.blacklist?.clear()
|
||||||
|
|
||||||
if (binding.switchBypassApps.isChecked) {
|
if (binding.switchBypassApps.isChecked) {
|
||||||
adapter?.let {
|
adapter?.let {
|
||||||
@@ -289,7 +286,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
Log.d(ANG_PACKAGE, packageName)
|
Log.d(ANG_PACKAGE, packageName)
|
||||||
if (!inProxyApps(proxyApps, packageName, force)) {
|
if (!inProxyApps(proxyApps, packageName, force)) {
|
||||||
adapter?.blacklist!!.add(packageName)
|
adapter?.blacklist?.add(packageName)
|
||||||
println(packageName)
|
println(packageName)
|
||||||
return@block
|
return@block
|
||||||
}
|
}
|
||||||
@@ -302,7 +299,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
Log.d(ANG_PACKAGE, packageName)
|
Log.d(ANG_PACKAGE, packageName)
|
||||||
if (inProxyApps(proxyApps, packageName, force)) {
|
if (inProxyApps(proxyApps, packageName, force)) {
|
||||||
adapter?.blacklist!!.add(packageName)
|
adapter?.blacklist?.add(packageName)
|
||||||
println(packageName)
|
println(packageName)
|
||||||
return@block
|
return@block
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class RoutingSettingsActivity : BaseActivity() {
|
|||||||
setContentView(view)
|
setContentView(view)
|
||||||
|
|
||||||
title = getString(R.string.title_pref_routing_custom)
|
title = getString(R.string.title_pref_routing_custom)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
val fragments = ArrayList<Fragment>()
|
val fragments = ArrayList<Fragment>()
|
||||||
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT))
|
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity.RESULT_OK
|
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
@@ -11,14 +11,15 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.tbruyelle.rxpermissions.RxPermissions
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
|
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.v2RayApplication
|
import com.v2ray.ang.extension.v2RayApplication
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class RoutingSettingsFragment : Fragment() {
|
class RoutingSettingsFragment : Fragment() {
|
||||||
@@ -27,7 +28,7 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
private const val routing_arg = "routing_arg"
|
private const val routing_arg = "routing_arg"
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
|
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?): View? {
|
savedInstanceState: Bundle?): View? {
|
||||||
@@ -47,8 +48,8 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val content = defaultSharedPreferences.getString(requireArguments().getString(routing_arg), "")
|
val content = settingsStorage?.getString(requireArguments().getString(routing_arg), "")
|
||||||
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
binding.etRoutingContent.text = Utils.getEditable(content)
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
@@ -84,7 +85,7 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
|
|
||||||
private fun saveRouting() {
|
private fun saveRouting() {
|
||||||
val content = binding.etRoutingContent.text.toString()
|
val content = binding.etRoutingContent.text.toString()
|
||||||
defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply()
|
settingsStorage?.encode(requireArguments().getString(routing_arg), content)
|
||||||
activity?.toast(R.string.toast_success)
|
activity?.toast(R.string.toast_success)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == RESULT_OK) {
|
if (it.resultCode == RESULT_OK) {
|
||||||
val content = it.data?.getStringExtra("SCAN_RESULT")
|
val content = it.data?.getStringExtra("SCAN_RESULT")
|
||||||
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
binding.etRoutingContent.text = Utils.getEditable(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
var tag = ""
|
var tag = ""
|
||||||
when (requireArguments().getString(routing_arg)) {
|
when (requireArguments().getString(routing_arg)) {
|
||||||
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
||||||
tag = AppConfig.TAG_AGENT
|
tag = AppConfig.TAG_PROXY
|
||||||
}
|
}
|
||||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
||||||
tag = AppConfig.TAG_DIRECT
|
tag = AppConfig.TAG_DIRECT
|
||||||
@@ -144,7 +145,7 @@ class RoutingSettingsFragment : Fragment() {
|
|||||||
val content = Utils.getUrlContext(url, 5000)
|
val content = Utils.getUrlContext(url, 5000)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
val routingList = if (TextUtils.isEmpty(content)) {
|
val routingList = if (TextUtils.isEmpty(content)) {
|
||||||
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
|
Utils.readTextFromAssets(activity?.v2RayApplication, "custom_routing_$tag")
|
||||||
} else {
|
} else {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,60 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.zxing.Result
|
|
||||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.os.Build
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.google.zxing.BarcodeFormat
|
|
||||||
import com.tbruyelle.rxpermissions.RxPermissions
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
|
import io.github.g00fy2.quickie.QRResult
|
||||||
|
import io.github.g00fy2.quickie.ScanCustomCode
|
||||||
|
import io.github.g00fy2.quickie.config.ScannerConfig
|
||||||
|
|
||||||
class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
class ScannerActivity : BaseActivity(){
|
||||||
|
|
||||||
private var mScannerView: ZXingScannerView? = null
|
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
||||||
|
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view
|
|
||||||
|
|
||||||
mScannerView?.setAutoFocus(true)
|
if (settingsStorage?.decodeBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
|
||||||
val formats = ArrayList<BarcodeFormat>()
|
launchScan()
|
||||||
formats.add(BarcodeFormat.QR_CODE)
|
}
|
||||||
mScannerView?.setFormats(formats)
|
|
||||||
|
|
||||||
setContentView(mScannerView) // Set the scanner view as the content view
|
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onResume() {
|
private fun launchScan(){
|
||||||
super.onResume()
|
scanQrCode.launch(
|
||||||
mScannerView!!.setResultHandler(this) // Register ourselves as a handler for scan results.
|
ScannerConfig.build {
|
||||||
mScannerView!!.startCamera() // Start camera on resume
|
setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected
|
||||||
|
setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button
|
||||||
|
setShowCloseButton(true) // show or hide (default) close button
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onPause() {
|
private fun handleResult(result: QRResult) {
|
||||||
super.onPause()
|
if (result is QRResult.QRSuccess ) {
|
||||||
mScannerView!!.stopCamera() // Stop camera on pause
|
finished(result.content.rawValue?:"")
|
||||||
|
} else {
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleResult(rawResult: Result) {
|
|
||||||
// Do something with the result here
|
|
||||||
// Log.v(FragmentActivity.TAG, rawResult.text) // Prints scan results
|
|
||||||
// Log.v(FragmentActivity.TAG, rawResult.barcodeFormat.toString()) // Prints the scan format (qrcode, pdf417 etc.)
|
|
||||||
|
|
||||||
finished(rawResult.text)
|
|
||||||
|
|
||||||
// If you would like to resume scanning, call this method below:
|
|
||||||
// mScannerView!!.resumeCameraPreview(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finished(text: String) {
|
private fun finished(text: String) {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.putExtra("SCAN_RESULT", text)
|
intent.putExtra("SCAN_RESULT", text)
|
||||||
setResult(Activity.RESULT_OK, intent)
|
setResult(AppCompatActivity.RESULT_OK, intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +64,18 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
R.id.scan_code -> {
|
||||||
|
launchScan()
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.select_photo -> {
|
R.id.select_photo -> {
|
||||||
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
|
} else {
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
RxPermissions(this)
|
RxPermissions(this)
|
||||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
.request(permission)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (it) {
|
if (it) {
|
||||||
try {
|
try {
|
||||||
@@ -106,7 +110,7 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
|||||||
try {
|
try {
|
||||||
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
||||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||||
finished(text!!)
|
finished(text?:"")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
toast(e.message.toString())
|
toast(e.message.toString())
|
||||||
|
|||||||
@@ -5,26 +5,42 @@ import android.text.TextUtils
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
||||||
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
||||||
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
|
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
|
||||||
|
import com.v2ray.ang.extension.removeWhiteSpace
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.MmkvManager.ID_MAIN
|
import com.v2ray.ang.util.MmkvManager.ID_MAIN
|
||||||
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
|
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.util.Utils.getIpv6Address
|
||||||
|
|
||||||
class ServerActivity : BaseActivity() {
|
class ServerActivity : BaseActivity() {
|
||||||
|
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
private val settingsStorage by lazy {
|
||||||
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SETTING,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
|
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
|
||||||
private val isRunning by lazy {
|
private val isRunning by lazy {
|
||||||
intent.getBooleanExtra("isRunning", false)
|
intent.getBooleanExtra("isRunning", false)
|
||||||
@@ -32,7 +48,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
&& editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER)
|
&& editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER)
|
||||||
}
|
}
|
||||||
private val createConfigType by lazy {
|
private val createConfigType by lazy {
|
||||||
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) ?: EConfigType.VMESS
|
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value))
|
||||||
|
?: EConfigType.VMESS
|
||||||
}
|
}
|
||||||
private val subscriptionId by lazy {
|
private val subscriptionId by lazy {
|
||||||
intent.getStringExtra("subscriptionId")
|
intent.getStringExtra("subscriptionId")
|
||||||
@@ -71,6 +88,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val alpns: Array<out String> by lazy {
|
private val alpns: Array<out String> by lazy {
|
||||||
resources.getStringArray(R.array.streamsecurity_alpn)
|
resources.getStringArray(R.array.streamsecurity_alpn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
|
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
|
||||||
// We don't use AndroidViewBinding because, it is better to share similar logics for different
|
// We don't use AndroidViewBinding because, it is better to share similar logics for different
|
||||||
// protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic.
|
// protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic.
|
||||||
@@ -84,33 +102,58 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
|
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
|
||||||
private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
|
private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
|
||||||
private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
|
private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
|
||||||
|
private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.l5) }
|
||||||
private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
|
private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
|
||||||
|
private val container_sni: LinearLayout? by lazy { findViewById(R.id.l2) }
|
||||||
private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
|
private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
|
||||||
|
private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.l3) }
|
||||||
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
|
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
|
||||||
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
|
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
|
||||||
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
|
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
|
||||||
|
private val tv_request_host: TextView? by lazy { findViewById(R.id.tv_request_host) }
|
||||||
private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
|
private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
|
||||||
|
private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) }
|
||||||
private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
|
private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
|
||||||
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
||||||
|
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.l4) }
|
||||||
|
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
|
||||||
|
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.l6) }
|
||||||
|
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
|
||||||
|
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.l7) }
|
||||||
|
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
|
||||||
|
private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.l8) }
|
||||||
|
private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
|
||||||
|
private val et_reserved2: EditText? by lazy { findViewById(R.id.et_reserved2) }
|
||||||
|
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
|
||||||
|
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
|
||||||
|
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
title = getString(R.string.title_server)
|
title = getString(R.string.title_server)
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid)
|
val config = MmkvManager.decodeServerConfig(editGuid)
|
||||||
when(config?.configType ?: createConfigType) {
|
when (config?.configType ?: createConfigType) {
|
||||||
EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess)
|
EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess)
|
||||||
EConfigType.CUSTOM -> return
|
EConfigType.CUSTOM -> return
|
||||||
EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
|
EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
|
||||||
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
|
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
|
||||||
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
|
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
|
||||||
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
|
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
|
||||||
|
EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard)
|
||||||
}
|
}
|
||||||
sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
override fun onItemSelected(
|
||||||
|
parent: AdapterView<*>?,
|
||||||
|
view: View?,
|
||||||
|
position: Int,
|
||||||
|
id: Long
|
||||||
|
) {
|
||||||
val types = transportTypes(networks[position])
|
val types = transportTypes(networks[position])
|
||||||
sp_header_type?.isEnabled = types.size > 1
|
sp_header_type?.isEnabled = types.size > 1
|
||||||
val adapter = ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
|
val adapter =
|
||||||
|
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
sp_header_type?.adapter = adapter
|
sp_header_type?.adapter = adapter
|
||||||
sp_header_type_title?.text = if (networks[position] == "grpc")
|
sp_header_type_title?.text = if (networks[position] == "grpc")
|
||||||
@@ -121,70 +164,196 @@ class ServerActivity : BaseActivity() {
|
|||||||
et_request_host?.text = Utils.getEditable(transportDetails[1])
|
et_request_host?.text = Utils.getEditable(transportDetails[1])
|
||||||
et_path?.text = Utils.getEditable(transportDetails[2])
|
et_path?.text = Utils.getEditable(transportDetails[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tv_request_host?.text = Utils.getEditable(
|
||||||
|
getString(
|
||||||
|
when (networks[position]) {
|
||||||
|
"tcp" -> R.string.server_lab_request_host_http
|
||||||
|
"ws" -> R.string.server_lab_request_host_ws
|
||||||
|
"httpupgrade" -> R.string.server_lab_request_host_httpupgrade
|
||||||
|
"splithttp" -> R.string.server_lab_request_host_splithttp
|
||||||
|
"h2" -> R.string.server_lab_request_host_h2
|
||||||
|
"quic" -> R.string.server_lab_request_host_quic
|
||||||
|
"grpc" -> R.string.server_lab_request_host_grpc
|
||||||
|
else -> R.string.server_lab_request_host
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
tv_path?.text = Utils.getEditable(
|
||||||
|
getString(
|
||||||
|
when (networks[position]) {
|
||||||
|
"kcp" -> R.string.server_lab_path_kcp
|
||||||
|
"ws" -> R.string.server_lab_path_ws
|
||||||
|
"httpupgrade" -> R.string.server_lab_path_httpupgrade
|
||||||
|
"splithttp" -> R.string.server_lab_path_splithttp
|
||||||
|
"h2" -> R.string.server_lab_path_h2
|
||||||
|
"quic" -> R.string.server_lab_path_quic
|
||||||
|
"grpc" -> R.string.server_lab_path_grpc
|
||||||
|
else -> R.string.server_lab_path
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sp_stream_security?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(
|
||||||
|
parent: AdapterView<*>?,
|
||||||
|
view: View?,
|
||||||
|
position: Int,
|
||||||
|
id: Long
|
||||||
|
) {
|
||||||
|
if (streamSecuritys[position].isBlank()) {
|
||||||
|
container_sni?.visibility = View.GONE
|
||||||
|
container_fingerprint?.visibility = View.GONE
|
||||||
|
container_alpn?.visibility = View.GONE
|
||||||
|
container_allow_insecure?.visibility = View.GONE
|
||||||
|
container_public_key?.visibility = View.GONE
|
||||||
|
container_short_id?.visibility = View.GONE
|
||||||
|
container_spider_x?.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
container_sni?.visibility = View.VISIBLE
|
||||||
|
container_fingerprint?.visibility = View.VISIBLE
|
||||||
|
container_alpn?.visibility = View.VISIBLE
|
||||||
|
if (streamSecuritys[position] == TLS) {
|
||||||
|
container_allow_insecure?.visibility = View.VISIBLE
|
||||||
|
container_public_key?.visibility = View.GONE
|
||||||
|
container_short_id?.visibility = View.GONE
|
||||||
|
container_spider_x?.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
container_allow_insecure?.visibility = View.GONE
|
||||||
|
container_alpn?.visibility = View.GONE
|
||||||
|
container_public_key?.visibility = View.VISIBLE
|
||||||
|
container_short_id?.visibility = View.VISIBLE
|
||||||
|
container_spider_x?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(p0: AdapterView<*>?) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
bindingServer(config)
|
bindingServer(config)
|
||||||
} else {
|
} else {
|
||||||
clearServer()
|
clearServer()
|
||||||
}
|
}
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bingding seleced server config
|
* binding selected server config
|
||||||
*/
|
*/
|
||||||
private fun bindingServer(config: ServerConfig): Boolean {
|
private fun bindingServer(config: ServerConfig): Boolean {
|
||||||
val outbound = config.getProxyOutbound() ?: return false
|
val outbound = config.getProxyOutbound() ?: return false
|
||||||
val streamSetting = config.outboundBean?.streamSettings ?: return false
|
|
||||||
|
|
||||||
et_remarks.text = Utils.getEditable(config.remarks)
|
et_remarks.text = Utils.getEditable(config.remarks)
|
||||||
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
|
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
|
||||||
et_port.text = Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
|
et_port.text =
|
||||||
|
Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
|
||||||
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
|
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
|
||||||
et_alterId?.text = Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
|
et_alterId?.text =
|
||||||
|
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
|
||||||
if (config.configType == EConfigType.SOCKS) {
|
if (config.configType == EConfigType.SOCKS) {
|
||||||
et_security?.text = Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
|
et_security?.text =
|
||||||
|
Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
|
||||||
} else if (config.configType == EConfigType.VLESS) {
|
} else if (config.configType == EConfigType.VLESS) {
|
||||||
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
|
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
|
||||||
val flow = Utils.arrayFind(flows, outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty())
|
val flow = Utils.arrayFind(
|
||||||
|
flows,
|
||||||
|
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty()
|
||||||
|
)
|
||||||
if (flow >= 0) {
|
if (flow >= 0) {
|
||||||
sp_flow?.setSelection(flow)
|
sp_flow?.setSelection(flow)
|
||||||
}
|
}
|
||||||
} else if (config.configType == EConfigType.TROJAN) {
|
} else if (config.configType == EConfigType.WIREGUARD) {
|
||||||
val flow = Utils.arrayFind(flows, outbound.settings?.servers?.get(0)?.flow.orEmpty())
|
et_public_key?.text =
|
||||||
if (flow >= 0) {
|
Utils.getEditable(outbound.settings?.peers?.get(0)?.publicKey.orEmpty())
|
||||||
sp_flow?.setSelection(flow)
|
if (outbound.settings?.reserved == null) {
|
||||||
|
et_reserved1?.text = Utils.getEditable("0")
|
||||||
|
et_reserved2?.text = Utils.getEditable("0")
|
||||||
|
et_reserved3?.text = Utils.getEditable("0")
|
||||||
|
} else {
|
||||||
|
et_reserved1?.text =
|
||||||
|
Utils.getEditable(outbound.settings?.reserved?.get(0).toString())
|
||||||
|
et_reserved2?.text =
|
||||||
|
Utils.getEditable(outbound.settings?.reserved?.get(1).toString())
|
||||||
|
et_reserved3?.text =
|
||||||
|
Utils.getEditable(outbound.settings?.reserved?.get(2).toString())
|
||||||
|
}
|
||||||
|
if (outbound.settings?.address == null) {
|
||||||
|
et_local_address?.text =
|
||||||
|
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
||||||
|
} else {
|
||||||
|
val list = outbound.settings?.address as List<*>
|
||||||
|
et_local_address?.text = Utils.getEditable(list.joinToString())
|
||||||
|
}
|
||||||
|
if (outbound.settings?.mtu == null) {
|
||||||
|
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
||||||
|
} else {
|
||||||
|
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
val securityEncryptions =
|
||||||
val security = Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
|
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||||
|
val security =
|
||||||
|
Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
|
||||||
if (security >= 0) {
|
if (security >= 0) {
|
||||||
sp_security?.setSelection(security)
|
sp_security?.setSelection(security)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val streamSetting = config.outboundBean?.streamSettings ?: return true
|
||||||
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
|
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
|
||||||
if (streamSecurity >= 0) {
|
if (streamSecurity >= 0) {
|
||||||
sp_stream_security?.setSelection(streamSecurity)
|
sp_stream_security?.setSelection(streamSecurity)
|
||||||
(streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting ->
|
(streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting ->
|
||||||
val allowinsecure = Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
|
container_sni?.visibility = View.VISIBLE
|
||||||
if (allowinsecure >= 0) {
|
container_fingerprint?.visibility = View.VISIBLE
|
||||||
sp_allow_insecure?.setSelection(allowinsecure)
|
container_alpn?.visibility = View.VISIBLE
|
||||||
}
|
|
||||||
et_sni?.text = Utils.getEditable(tlsSetting.serverName)
|
et_sni?.text = Utils.getEditable(tlsSetting.serverName)
|
||||||
|
|
||||||
tlsSetting.fingerprint?.let {
|
tlsSetting.fingerprint?.let {
|
||||||
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint)
|
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint)
|
||||||
sp_stream_fingerprint?.setSelection(utlsIndex)
|
sp_stream_fingerprint?.setSelection(utlsIndex)
|
||||||
}
|
}
|
||||||
tlsSetting.alpn?.let {
|
tlsSetting.alpn?.let {
|
||||||
val alpnIndex = Utils.arrayFind(alpns, Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())!!)
|
val alpnIndex = Utils.arrayFind(
|
||||||
|
alpns,
|
||||||
|
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())?:""
|
||||||
|
)
|
||||||
sp_stream_alpn?.setSelection(alpnIndex)
|
sp_stream_alpn?.setSelection(alpnIndex)
|
||||||
}
|
}
|
||||||
|
if (streamSetting.tlsSettings != null) {
|
||||||
|
container_allow_insecure?.visibility = View.VISIBLE
|
||||||
|
val allowinsecure =
|
||||||
|
Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
|
||||||
|
if (allowinsecure >= 0) {
|
||||||
|
sp_allow_insecure?.setSelection(allowinsecure)
|
||||||
|
}
|
||||||
|
container_public_key?.visibility = View.GONE
|
||||||
|
container_short_id?.visibility = View.GONE
|
||||||
|
container_spider_x?.visibility = View.GONE
|
||||||
|
} else { // reality settings
|
||||||
|
container_public_key?.visibility = View.VISIBLE
|
||||||
|
et_public_key?.text = Utils.getEditable(tlsSetting.publicKey.orEmpty())
|
||||||
|
container_short_id?.visibility = View.VISIBLE
|
||||||
|
et_short_id?.text = Utils.getEditable(tlsSetting.shortId.orEmpty())
|
||||||
|
container_spider_x?.visibility = View.VISIBLE
|
||||||
|
et_spider_x?.text = Utils.getEditable(tlsSetting.spiderX.orEmpty())
|
||||||
|
container_allow_insecure?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (streamSetting.tlsSettings == null && streamSetting.realitySettings == null) {
|
||||||
|
container_sni?.visibility = View.GONE
|
||||||
|
container_fingerprint?.visibility = View.GONE
|
||||||
|
container_alpn?.visibility = View.GONE
|
||||||
|
container_allow_insecure?.visibility = View.GONE
|
||||||
|
container_public_key?.visibility = View.GONE
|
||||||
|
container_short_id?.visibility = View.GONE
|
||||||
|
container_spider_x?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val network = Utils.arrayFind(networks, streamSetting.network)
|
val network = Utils.arrayFind(networks, streamSetting.network)
|
||||||
@@ -215,6 +384,13 @@ class ServerActivity : BaseActivity() {
|
|||||||
|
|
||||||
//et_security.text = null
|
//et_security.text = null
|
||||||
sp_flow?.setSelection(0)
|
sp_flow?.setSelection(0)
|
||||||
|
et_public_key?.text = null
|
||||||
|
et_reserved1?.text = Utils.getEditable("0")
|
||||||
|
et_reserved2?.text = Utils.getEditable("0")
|
||||||
|
et_reserved3?.text = Utils.getEditable("0")
|
||||||
|
et_local_address?.text =
|
||||||
|
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
||||||
|
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,9 +411,14 @@ class ServerActivity : BaseActivity() {
|
|||||||
toast(R.string.server_lab_port)
|
toast(R.string.server_lab_port)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
|
val config =
|
||||||
|
MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
|
||||||
if (config.configType != EConfigType.SOCKS && TextUtils.isEmpty(et_id.text.toString())) {
|
if (config.configType != EConfigType.SOCKS && TextUtils.isEmpty(et_id.text.toString())) {
|
||||||
|
if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.SHADOWSOCKS) {
|
||||||
|
toast(R.string.server_lab_id3)
|
||||||
|
} else {
|
||||||
toast(R.string.server_lab_id)
|
toast(R.string.server_lab_id)
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
sp_stream_security?.let {
|
sp_stream_security?.let {
|
||||||
@@ -261,11 +442,15 @@ class ServerActivity : BaseActivity() {
|
|||||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||||
saveServers(server, port, config)
|
saveServers(server, port, config)
|
||||||
}
|
}
|
||||||
|
val wireguard = config.outboundBean?.settings
|
||||||
|
wireguard?.peers?.get(0)?.let { _ ->
|
||||||
|
savePeer(wireguard, port)
|
||||||
|
}
|
||||||
config.outboundBean?.streamSettings?.let {
|
config.outboundBean?.streamSettings?.let {
|
||||||
saveStreamSettings(it)
|
saveStreamSettings(it)
|
||||||
}
|
}
|
||||||
if(config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||||
config.subscriptionId = subscriptionId!!
|
config.subscriptionId = subscriptionId?:""
|
||||||
}
|
}
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
@@ -274,7 +459,11 @@ class ServerActivity : BaseActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveVnext(vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean, port: Int, config: ServerConfig) {
|
private fun saveVnext(
|
||||||
|
vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean,
|
||||||
|
port: Int,
|
||||||
|
config: ServerConfig
|
||||||
|
) {
|
||||||
vnext.address = et_address.text.toString().trim()
|
vnext.address = et_address.text.toString().trim()
|
||||||
vnext.port = port
|
vnext.port = port
|
||||||
vnext.users[0].id = et_id.text.toString().trim()
|
vnext.users[0].id = et_id.text.toString().trim()
|
||||||
@@ -287,7 +476,11 @@ class ServerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveServers(server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean, port: Int, config: ServerConfig) {
|
private fun saveServers(
|
||||||
|
server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean,
|
||||||
|
port: Int,
|
||||||
|
config: ServerConfig
|
||||||
|
) {
|
||||||
server.address = et_address.text.toString().trim()
|
server.address = et_address.text.toString().trim()
|
||||||
server.port = port
|
server.port = port
|
||||||
if (config.configType == EConfigType.SHADOWSOCKS) {
|
if (config.configType == EConfigType.SHADOWSOCKS) {
|
||||||
@@ -297,20 +490,32 @@ class ServerActivity : BaseActivity() {
|
|||||||
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
|
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
|
||||||
server.users = null
|
server.users = null
|
||||||
} else {
|
} else {
|
||||||
val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
val socksUsersBean =
|
||||||
|
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
socksUsersBean.user = et_security?.text.toString().trim()
|
socksUsersBean.user = et_security?.text.toString().trim()
|
||||||
socksUsersBean.pass = et_id.text.toString().trim()
|
socksUsersBean.pass = et_id.text.toString().trim()
|
||||||
server.users = listOf(socksUsersBean)
|
server.users = listOf(socksUsersBean)
|
||||||
}
|
}
|
||||||
} else if (config.configType == EConfigType.TROJAN) {
|
} else if (config.configType == EConfigType.TROJAN) {
|
||||||
server.password = et_id.text.toString().trim()
|
server.password = et_id.text.toString().trim()
|
||||||
server.flow =
|
}
|
||||||
if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == V2rayConfig.XTLS) {
|
}
|
||||||
flows[sp_flow?.selectedItemPosition ?: 0]
|
|
||||||
|
private fun savePeer(wireguard: V2rayConfig.OutboundBean.OutSettingsBean, port: Int) {
|
||||||
|
wireguard.secretKey = et_id.text.toString().trim()
|
||||||
|
wireguard.peers?.get(0)?.publicKey = et_public_key?.text.toString().trim()
|
||||||
|
wireguard.peers?.get(0)?.endpoint =
|
||||||
|
getIpv6Address(et_address.text.toString().trim()) + ":" + port
|
||||||
|
val reserved1 = Utils.parseInt(et_reserved1?.text.toString())
|
||||||
|
val reserved2 = Utils.parseInt(et_reserved2?.text.toString())
|
||||||
|
val reserved3 = Utils.parseInt(et_reserved3?.text.toString())
|
||||||
|
if (reserved1 > 0 || reserved2 > 0 || reserved3 > 0) {
|
||||||
|
wireguard.reserved = listOf(reserved1, reserved2, reserved3)
|
||||||
} else {
|
} else {
|
||||||
""
|
wireguard.reserved = null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
wireguard.address = et_local_address?.text.toString().removeWhiteSpace().split(",")
|
||||||
|
wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) {
|
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) {
|
||||||
@@ -321,8 +526,11 @@ class ServerActivity : BaseActivity() {
|
|||||||
val sniField = et_sni?.text?.toString()?.trim() ?: return
|
val sniField = et_sni?.text?.toString()?.trim() ?: return
|
||||||
val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
|
val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
|
||||||
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
||||||
var utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
|
val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
|
||||||
var alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return
|
val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return
|
||||||
|
val publicKey = et_public_key?.text?.toString()?.trim() ?: return
|
||||||
|
val shortId = et_short_id?.text?.toString()?.trim() ?: return
|
||||||
|
val spiderX = et_spider_x?.text?.toString()?.trim() ?: return
|
||||||
|
|
||||||
var sni = streamSetting.populateTransportSettings(
|
var sni = streamSetting.populateTransportSettings(
|
||||||
transport = networks[network],
|
transport = networks[network],
|
||||||
@@ -333,7 +541,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
quicSecurity = requestHost,
|
quicSecurity = requestHost,
|
||||||
key = path,
|
key = path,
|
||||||
mode = transportTypes(networks[network])[type],
|
mode = transportTypes(networks[network])[type],
|
||||||
serviceName = path
|
serviceName = path,
|
||||||
|
authority = requestHost,
|
||||||
)
|
)
|
||||||
if (sniField.isNotBlank()) {
|
if (sniField.isNotBlank()) {
|
||||||
sni = sniField
|
sni = sniField
|
||||||
@@ -344,38 +553,60 @@ class ServerActivity : BaseActivity() {
|
|||||||
allowinsecures[allowInsecureField].toBoolean()
|
allowinsecures[allowInsecureField].toBoolean()
|
||||||
}
|
}
|
||||||
|
|
||||||
streamSetting.populateTlsSettings(streamSecuritys[streamSecurity], allowInsecure, sni, uTlsItems[utlsIndex], alpns[alpnIndex])
|
streamSetting.populateTlsSettings(
|
||||||
|
streamSecurity = streamSecuritys[streamSecurity],
|
||||||
|
allowInsecure = allowInsecure,
|
||||||
|
sni = sni,
|
||||||
|
fingerprint = uTlsItems[utlsIndex],
|
||||||
|
alpns = alpns[alpnIndex],
|
||||||
|
publicKey = publicKey,
|
||||||
|
shortId = shortId,
|
||||||
|
spiderX = spiderX
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun transportTypes(network: String?): Array<out String> {
|
private fun transportTypes(network: String?): Array<out String> {
|
||||||
return if (network == "tcp") {
|
return when (network) {
|
||||||
|
"tcp" -> {
|
||||||
tcpTypes
|
tcpTypes
|
||||||
} else if (network == "kcp" || network == "quic") {
|
}
|
||||||
|
|
||||||
|
"kcp", "quic" -> {
|
||||||
kcpAndQuicTypes
|
kcpAndQuicTypes
|
||||||
} else if (network == "grpc") {
|
}
|
||||||
|
|
||||||
|
"grpc" -> {
|
||||||
grpcModes
|
grpcModes
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
arrayOf("---")
|
arrayOf("---")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save server config
|
* save server config
|
||||||
*/
|
*/
|
||||||
private fun deleteServer(): Boolean {
|
private fun deleteServer(): Boolean {
|
||||||
if (editGuid.isNotEmpty()) {
|
if (editGuid.isNotEmpty()) {
|
||||||
if (editGuid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
if (editGuid != mainStorage?.decodeString(KEY_SELECTED_SERVER)) {
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
MmkvManager.removeServer(editGuid)
|
MmkvManager.removeServer(editGuid)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
MmkvManager.removeServer(editGuid)
|
MmkvManager.removeServer(editGuid)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
application.toast(R.string.toast_action_not_allowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -403,10 +634,12 @@ class ServerActivity : BaseActivity() {
|
|||||||
deleteServer()
|
deleteServer()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.save_config -> {
|
R.id.save_config -> {
|
||||||
saveServer()
|
saveServer()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.blacksquircle.ui.editorkit.utils.EditorTheme
|
||||||
import com.blacksquircle.ui.language.json.JsonLanguage
|
import com.blacksquircle.ui.language.json.JsonLanguage
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
@@ -38,6 +39,9 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
setContentView(view)
|
setContentView(view)
|
||||||
title = getString(R.string.title_server)
|
title = getString(R.string.title_server)
|
||||||
|
|
||||||
|
if (!Utils.getDarkModeStatus(this)) {
|
||||||
|
binding.editor.colorScheme = EditorTheme.INTELLIJ_LIGHT
|
||||||
|
}
|
||||||
binding.editor.language = JsonLanguage()
|
binding.editor.language = JsonLanguage()
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid)
|
val config = MmkvManager.decodeServerConfig(editGuid)
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
@@ -45,7 +49,6 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
} else {
|
} else {
|
||||||
clearServer()
|
clearServer()
|
||||||
}
|
}
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +91,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
|
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
|
||||||
config.remarks = binding.etRemarks.text.toString().trim()
|
config.remarks = v2rayConfig.remarks ?: binding.etRemarks.text.toString().trim()
|
||||||
config.fullConfig = v2rayConfig
|
config.fullConfig = v2rayConfig
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
@@ -108,6 +111,9 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
MmkvManager.removeServer(editGuid)
|
MmkvManager.removeServer(editGuid)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -5,11 +5,23 @@ import android.os.Bundle
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.preference.*
|
import androidx.preference.CheckBoxPreference
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.PeriodicWorkRequest
|
||||||
|
import androidx.work.multiprocess.RemoteWorkManager
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AngApplication
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.service.SubscriptionUpdater
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.SettingsViewModel
|
import com.v2ray.ang.viewmodel.SettingsViewModel
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
@@ -20,91 +32,118 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
title = getString(R.string.title_settings)
|
title = getString(R.string.title_settings)
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
settingsViewModel.startListenPreferenceChange()
|
settingsViewModel.startListenPreferenceChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
||||||
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
||||||
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||||
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||||
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
||||||
// val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
|
|
||||||
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
private val routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
|
||||||
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
|
|
||||||
|
private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
|
||||||
|
private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) }
|
||||||
|
private val muxXudpConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_XUDP_CONCURRENCY) }
|
||||||
|
private val muxXudpQuic by lazy { findPreference<ListPreference>(AppConfig.PREF_MUX_XUDP_QUIC) }
|
||||||
|
|
||||||
|
private val fragment by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FRAGMENT_ENABLED) }
|
||||||
|
private val fragmentPackets by lazy { findPreference<ListPreference>(AppConfig.PREF_FRAGMENT_PACKETS) }
|
||||||
|
private val fragmentLength by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_LENGTH) }
|
||||||
|
private val fragmentInterval by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_INTERVAL) }
|
||||||
|
|
||||||
|
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
|
||||||
|
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
||||||
|
|
||||||
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
||||||
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
|
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
|
||||||
private val routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
|
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
||||||
// val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
|
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
|
||||||
// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
|
private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
|
||||||
// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
|
|
||||||
|
|
||||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||||
|
|
||||||
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||||
addPreferencesFromResource(R.xml.pref_settings)
|
addPreferencesFromResource(R.xml.pref_settings)
|
||||||
|
|
||||||
routingCustom?.setOnPreferenceClickListener {
|
|
||||||
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
// licenses.onClick {
|
|
||||||
// val fragment = LicensesDialogFragment.Builder(act)
|
|
||||||
// .setNotices(R.raw.licenses)
|
|
||||||
// .setIncludeOwnLicense(false)
|
|
||||||
// .build()
|
|
||||||
// fragment.show((act as AppCompatActivity).supportFragmentManager, null)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// feedback.onClick {
|
|
||||||
// Utils.openUri(activity, "https://github.com/2dust/v2rayNG/issues")
|
|
||||||
// }
|
|
||||||
// tgGroup.onClick {
|
|
||||||
// // Utils.openUri(activity, "https://t.me/v2rayN")
|
|
||||||
// val intent = Intent(Intent.ACTION_VIEW, Uri.parse("tg:resolve?domain=v2rayN"))
|
|
||||||
// try {
|
|
||||||
// startActivity(intent)
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// toast(R.string.toast_tg_app_not_found)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
perAppProxy?.setOnPreferenceClickListener {
|
perAppProxy?.setOnPreferenceClickListener {
|
||||||
startActivity(Intent(activity, PerAppProxyActivity::class.java))
|
startActivity(Intent(activity, PerAppProxyActivity::class.java))
|
||||||
perAppProxy?.isChecked = true
|
perAppProxy?.isChecked = true
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
localDns?.setOnPreferenceChangeListener { _, any ->
|
||||||
remoteDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// remoteDns.summary = any as String
|
|
||||||
val nval = any as String
|
|
||||||
remoteDns?.summary = if (nval == "") AppConfig.DNS_AGENT else nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
domesticDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
// domesticDns.summary = any as String
|
|
||||||
val nval = any as String
|
|
||||||
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
localDns?.setOnPreferenceChangeListener{ _, any ->
|
|
||||||
updateLocalDns(any as Boolean)
|
updateLocalDns(any as Boolean)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
localDnsPort?.setOnPreferenceChangeListener { _, any ->
|
localDnsPort?.setOnPreferenceChangeListener { _, any ->
|
||||||
val nval = any as String
|
val nval = any as String
|
||||||
localDnsPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
|
localDnsPort?.summary =
|
||||||
|
if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
vpnDns?.setOnPreferenceChangeListener { _, any ->
|
vpnDns?.setOnPreferenceChangeListener { _, any ->
|
||||||
vpnDns?.summary = any as String
|
vpnDns?.summary = any as String
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routingCustom?.setOnPreferenceClickListener {
|
||||||
|
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
mux?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateMux(newValue as Boolean)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
muxConcurrency?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateMuxConcurrency(newValue as String)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
muxXudpConcurrency?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateMuxXudpConcurrency(newValue as String)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateFragment(newValue as Boolean)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fragmentPackets?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateFragmentPackets(newValue as String)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fragmentLength?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateFragmentLength(newValue as String)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fragmentInterval?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateFragmentInterval(newValue as String)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val value = newValue as Boolean
|
||||||
|
autoUpdateCheck?.isChecked = value
|
||||||
|
autoUpdateInterval?.isEnabled = value
|
||||||
|
autoUpdateInterval?.text?.toLong()?.let {
|
||||||
|
if (newValue) configureUpdateTask(it) else cancelUpdateTask()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
autoUpdateInterval?.setOnPreferenceChangeListener { _, any ->
|
||||||
|
var nval = any as String
|
||||||
|
|
||||||
|
// It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
|
||||||
|
nval =
|
||||||
|
if (TextUtils.isEmpty(nval) || nval.toLong() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
|
||||||
|
autoUpdateInterval?.summary = nval
|
||||||
|
configureUpdateTask(nval.toLong())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
socksPort?.setOnPreferenceChangeListener { _, any ->
|
socksPort?.setOnPreferenceChangeListener { _, any ->
|
||||||
val nval = any as String
|
val nval = any as String
|
||||||
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
|
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
|
||||||
@@ -115,57 +154,132 @@ class SettingsActivity : BaseActivity() {
|
|||||||
httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval
|
httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
remoteDns?.setOnPreferenceChangeListener { _, any ->
|
||||||
|
val nval = any as String
|
||||||
|
remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
|
||||||
|
true
|
||||||
|
}
|
||||||
|
domesticDns?.setOnPreferenceChangeListener { _, any ->
|
||||||
|
val nval = any as String
|
||||||
|
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
||||||
|
true
|
||||||
|
}
|
||||||
|
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
|
||||||
|
val nval = any as String
|
||||||
|
delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
|
||||||
|
true
|
||||||
|
}
|
||||||
mode?.setOnPreferenceChangeListener { _, newValue ->
|
mode?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
updateMode(newValue.toString())
|
updateMode(newValue.toString())
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
||||||
//loglevel.summary = "LogLevel"
|
//loglevel.summary = "LogLevel"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, "VPN"))
|
||||||
updateMode(defaultSharedPreferences.getString(AppConfig.PREF_MODE, "VPN"))
|
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||||
var remoteDnsString = defaultSharedPreferences.getString(AppConfig.PREF_REMOTE_DNS, "")
|
fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
||||||
domesticDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_DOMESTIC_DNS, "")
|
localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
||||||
|
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
||||||
|
|
||||||
localDnsPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false))
|
||||||
socksPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)
|
||||||
httpPort?.summary = defaultSharedPreferences.getString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
|
muxConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8")
|
||||||
|
muxXudpConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
||||||
|
|
||||||
if (TextUtils.isEmpty(remoteDnsString)) {
|
updateFragment(settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false))
|
||||||
remoteDnsString = AppConfig.DNS_AGENT
|
fragment?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false)
|
||||||
}
|
fragmentPackets?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
|
||||||
if (TextUtils.isEmpty(domesticDns?.summary)) {
|
fragmentLength?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
||||||
domesticDns?.summary = AppConfig.DNS_DIRECT
|
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
||||||
}
|
|
||||||
remoteDns?.summary = remoteDnsString
|
|
||||||
vpnDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString)
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(localDnsPort?.summary)) {
|
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
localDnsPort?.summary = AppConfig.PORT_LOCAL_DNS
|
autoUpdateInterval?.summary = settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
||||||
|
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
|
|
||||||
|
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||||
|
httpPort?.summary = settingsStorage.decodeString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
|
||||||
|
remoteDns?.summary = settingsStorage.decodeString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
||||||
|
domesticDns?.summary = settingsStorage.decodeString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
||||||
|
delayTestUrl?.summary = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
|
||||||
|
|
||||||
|
initSharedPreference()
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(socksPort?.summary)) {
|
|
||||||
socksPort?.summary = AppConfig.PORT_SOCKS
|
private fun initSharedPreference() {
|
||||||
|
listOf(
|
||||||
|
localDnsPort,
|
||||||
|
vpnDns,
|
||||||
|
muxConcurrency,
|
||||||
|
muxXudpConcurrency,
|
||||||
|
fragmentLength,
|
||||||
|
fragmentInterval,
|
||||||
|
autoUpdateInterval,
|
||||||
|
socksPort,
|
||||||
|
httpPort,
|
||||||
|
remoteDns,
|
||||||
|
domesticDns,
|
||||||
|
delayTestUrl
|
||||||
|
).forEach { key ->
|
||||||
|
key?.text = key?.summary.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
listOf(
|
||||||
|
AppConfig.PREF_SNIFFING_ENABLED,
|
||||||
|
).forEach { key ->
|
||||||
|
findPreference<CheckBoxPreference>(key)?.isChecked =
|
||||||
|
settingsStorage.decodeBool(key, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
listOf(
|
||||||
|
AppConfig.PREF_ROUTE_ONLY_ENABLED,
|
||||||
|
AppConfig.PREF_BYPASS_APPS,
|
||||||
|
AppConfig.PREF_SPEED_ENABLED,
|
||||||
|
AppConfig.PREF_CONFIRM_REMOVE,
|
||||||
|
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
||||||
|
AppConfig.PREF_PREFER_IPV6,
|
||||||
|
AppConfig.PREF_PROXY_SHARING,
|
||||||
|
AppConfig.PREF_ALLOW_INSECURE
|
||||||
|
).forEach { key ->
|
||||||
|
findPreference<CheckBoxPreference>(key)?.isChecked =
|
||||||
|
settingsStorage.decodeBool(key, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
listOf(
|
||||||
|
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
||||||
|
AppConfig.PREF_ROUTING_MODE,
|
||||||
|
AppConfig.PREF_MUX_XUDP_QUIC,
|
||||||
|
AppConfig.PREF_FRAGMENT_PACKETS,
|
||||||
|
AppConfig.PREF_LANGUAGE,
|
||||||
|
AppConfig.PREF_UI_MODE_NIGHT,
|
||||||
|
AppConfig.PREF_LOGLEVEL,
|
||||||
|
AppConfig.PREF_MODE
|
||||||
|
).forEach { key ->
|
||||||
|
if (settingsStorage.decodeString(key) != null) {
|
||||||
|
findPreference<ListPreference>(key)?.value = settingsStorage.decodeString(key)
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(httpPort?.summary)) {
|
|
||||||
httpPort?.summary = AppConfig.PORT_HTTP
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMode(mode: String?) {
|
private fun updateMode(mode: String?) {
|
||||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
|
||||||
val vpn = mode == "VPN"
|
val vpn = mode == "VPN"
|
||||||
perAppProxy?.isEnabled = vpn
|
perAppProxy?.isEnabled = vpn
|
||||||
perAppProxy?.isChecked = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
perAppProxy?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
||||||
.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
|
||||||
localDns?.isEnabled = vpn
|
localDns?.isEnabled = vpn
|
||||||
fakeDns?.isEnabled = vpn
|
fakeDns?.isEnabled = vpn
|
||||||
localDnsPort?.isEnabled = vpn
|
localDnsPort?.isEnabled = vpn
|
||||||
vpnDns?.isEnabled = vpn
|
vpnDns?.isEnabled = vpn
|
||||||
if (vpn) {
|
if (vpn) {
|
||||||
updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false))
|
updateLocalDns(
|
||||||
|
settingsStorage.getBoolean(
|
||||||
|
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +288,77 @@ class SettingsActivity : BaseActivity() {
|
|||||||
localDnsPort?.isEnabled = enabled
|
localDnsPort?.isEnabled = enabled
|
||||||
vpnDns?.isEnabled = !enabled
|
vpnDns?.isEnabled = !enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun configureUpdateTask(interval: Long) {
|
||||||
|
val rw = RemoteWorkManager.getInstance(AngApplication.application)
|
||||||
|
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
|
||||||
|
rw.enqueueUniquePeriodicWork(
|
||||||
|
AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME,
|
||||||
|
ExistingPeriodicWorkPolicy.UPDATE,
|
||||||
|
PeriodicWorkRequest.Builder(
|
||||||
|
SubscriptionUpdater.UpdateTask::class.java,
|
||||||
|
interval,
|
||||||
|
TimeUnit.MINUTES
|
||||||
|
)
|
||||||
|
.apply {
|
||||||
|
setInitialDelay(interval, TimeUnit.MINUTES)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelUpdateTask() {
|
||||||
|
val rw = RemoteWorkManager.getInstance(AngApplication.application)
|
||||||
|
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMux(enabled: Boolean) {
|
||||||
|
muxConcurrency?.isEnabled = enabled
|
||||||
|
muxXudpConcurrency?.isEnabled = enabled
|
||||||
|
muxXudpQuic?.isEnabled = enabled
|
||||||
|
if (enabled) {
|
||||||
|
updateMuxConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
|
||||||
|
updateMuxXudpConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMuxConcurrency(value: String?) {
|
||||||
|
if (value == null) {
|
||||||
|
} else {
|
||||||
|
val concurrency = value.toIntOrNull() ?: 8
|
||||||
|
muxConcurrency?.summary = concurrency.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMuxXudpConcurrency(value: String?) {
|
||||||
|
if (value == null) {
|
||||||
|
muxXudpQuic?.isEnabled = true
|
||||||
|
} else {
|
||||||
|
val concurrency = value.toIntOrNull() ?: 8
|
||||||
|
muxXudpConcurrency?.summary = concurrency.toString()
|
||||||
|
muxXudpQuic?.isEnabled = concurrency >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFragment(enabled: Boolean) {
|
||||||
|
fragmentPackets?.isEnabled = enabled
|
||||||
|
fragmentLength?.isEnabled = enabled
|
||||||
|
fragmentInterval?.isEnabled = enabled
|
||||||
|
if (enabled) {
|
||||||
|
updateFragmentPackets(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
|
||||||
|
updateFragmentLength(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
|
||||||
|
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun updateFragmentPackets(value: String?) {
|
||||||
|
fragmentPackets?.summary = value.toString()
|
||||||
|
}
|
||||||
|
private fun updateFragmentLength(value: String?) {
|
||||||
|
fragmentLength?.summary = value.toString()
|
||||||
|
}
|
||||||
|
private fun updateFragmentInterval(value: String?) {
|
||||||
|
fragmentInterval?.summary = value.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onModeHelpClicked(view: View) {
|
fun onModeHelpClicked(view: View) {
|
||||||
|
|||||||
@@ -5,14 +5,22 @@ import android.text.TextUtils
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.NetworkType
|
||||||
|
import androidx.work.PeriodicWorkRequest
|
||||||
|
import androidx.work.multiprocess.RemoteWorkManager
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AngApplication
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.service.SubscriptionUpdater
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SubEditActivity : BaseActivity() {
|
class SubEditActivity : BaseActivity() {
|
||||||
private lateinit var binding: ActivitySubEditBinding
|
private lateinit var binding: ActivitySubEditBinding
|
||||||
@@ -36,7 +44,6 @@ class SubEditActivity : BaseActivity() {
|
|||||||
} else {
|
} else {
|
||||||
clearServer()
|
clearServer()
|
||||||
}
|
}
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +53,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
||||||
binding.etUrl.text = Utils.getEditable(subItem.url)
|
binding.etUrl.text = Utils.getEditable(subItem.url)
|
||||||
binding.chkEnable.isChecked = subItem.enabled
|
binding.chkEnable.isChecked = subItem.enabled
|
||||||
|
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +84,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
subItem.remarks = binding.etRemarks.text.toString()
|
subItem.remarks = binding.etRemarks.text.toString()
|
||||||
subItem.url = binding.etUrl.text.toString()
|
subItem.url = binding.etUrl.text.toString()
|
||||||
subItem.enabled = binding.chkEnable.isChecked
|
subItem.enabled = binding.chkEnable.isChecked
|
||||||
|
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
||||||
|
|
||||||
if (TextUtils.isEmpty(subItem.remarks)) {
|
if (TextUtils.isEmpty(subItem.remarks)) {
|
||||||
toast(R.string.sub_setting_remarks)
|
toast(R.string.sub_setting_remarks)
|
||||||
@@ -102,6 +111,9 @@ class SubEditActivity : BaseActivity() {
|
|||||||
MmkvManager.removeSubscription(editSubId)
|
MmkvManager.removeSubscription(editSubId)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -130,4 +142,5 @@ class SubEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import android.os.Bundle
|
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||||
|
import com.v2ray.ang.databinding.LayoutProgressBinding
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SubSettingActivity : BaseActivity() {
|
class SubSettingActivity : BaseActivity() {
|
||||||
private lateinit var binding: ActivitySubSettingBinding
|
private lateinit var binding: ActivitySubSettingBinding
|
||||||
|
|
||||||
var subscriptions:List<Pair<String, SubscriptionItem>> = listOf()
|
var subscriptions: List<Pair<String, SubscriptionItem>> = listOf()
|
||||||
private val adapter by lazy { SubSettingRecyclerAdapter(this) }
|
private val adapter by lazy { SubSettingRecyclerAdapter(this) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -27,8 +35,6 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -39,9 +45,6 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.action_sub_setting, menu)
|
menuInflater.inflate(R.menu.action_sub_setting, menu)
|
||||||
menu.findItem(R.id.del_config)?.isVisible = false
|
|
||||||
menu.findItem(R.id.save_config)?.isVisible = false
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +53,30 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
startActivity(Intent(this, SubEditActivity::class.java))
|
startActivity(Intent(this, SubEditActivity::class.java))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.sub_update -> {
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val count = AngConfigManager.updateConfigViaSubAll()
|
||||||
|
delay(500L)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (count > 0) {
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,32 @@ package com.v2ray.ang.ui
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.text.TextUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
|
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) :
|
||||||
|
RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
|
||||||
|
|
||||||
private var mActivity: SubSettingActivity = activity
|
private var mActivity: SubSettingActivity = activity
|
||||||
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
|
private val share_method: Array<out String> by lazy {
|
||||||
|
mActivity.resources.getStringArray(R.array.share_sub_method)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount() = mActivity.subscriptions.size
|
override fun getItemCount() = mActivity.subscriptions.size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||||
@@ -24,14 +36,15 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
|||||||
holder.itemSubSettingBinding.tvName.text = subItem.remarks
|
holder.itemSubSettingBinding.tvName.text = subItem.remarks
|
||||||
holder.itemSubSettingBinding.tvUrl.text = subItem.url
|
holder.itemSubSettingBinding.tvUrl.text = subItem.url
|
||||||
if (subItem.enabled) {
|
if (subItem.enabled) {
|
||||||
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorSelected)
|
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorAccent)
|
||||||
} else {
|
} else {
|
||||||
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorUnselected)
|
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(0)
|
||||||
}
|
}
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||||
mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java)
|
mActivity.startActivity(
|
||||||
|
Intent(mActivity, SubEditActivity::class.java)
|
||||||
.putExtra("subId", subId)
|
.putExtra("subId", subId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -40,11 +53,51 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
|||||||
subStorage?.encode(subId, Gson().toJson(subItem))
|
subStorage?.encode(subId, Gson().toJson(subItem))
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(subItem.url)) {
|
||||||
|
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
|
||||||
|
} else {
|
||||||
|
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||||
|
AlertDialog.Builder(mActivity)
|
||||||
|
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
when (i) {
|
||||||
|
0 -> {
|
||||||
|
val ivBinding =
|
||||||
|
ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
||||||
|
ivBinding.ivQcode.setImageBitmap(
|
||||||
|
QRCodeDecoder.createQRCode(
|
||||||
|
subItem.url
|
||||||
|
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
Utils.setClipboard(mActivity, subItem.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> mActivity.toast("else")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||||
return MainViewHolder(ItemRecyclerSubSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
return MainViewHolder(
|
||||||
|
ItemRecyclerSubSettingBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
|
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) :
|
||||||
|
RecyclerView.ViewHolder(itemSubSettingBinding.root)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.app.Activity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
@@ -11,7 +11,6 @@ import android.content.Intent
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import com.google.zxing.WriterException
|
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.databinding.ActivityTaskerBinding
|
import com.v2ray.ang.databinding.ActivityTaskerBinding
|
||||||
@@ -45,7 +44,7 @@ class TaskerActivity : BaseActivity() {
|
|||||||
val adapter = ArrayAdapter(this,
|
val adapter = ArrayAdapter(this,
|
||||||
android.R.layout.simple_list_item_single_choice, lstData)
|
android.R.layout.simple_list_item_single_choice, lstData)
|
||||||
listview = findViewById<View>(R.id.listview) as ListView
|
listview = findViewById<View>(R.id.listview) as ListView
|
||||||
listview!!.adapter = adapter
|
listview?.adapter = adapter
|
||||||
|
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
@@ -65,7 +64,7 @@ class TaskerActivity : BaseActivity() {
|
|||||||
listview?.setItemChecked(pos, true)
|
listview?.setItemChecked(pos, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: WriterException) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -91,7 +90,7 @@ class TaskerActivity : BaseActivity() {
|
|||||||
|
|
||||||
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
|
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
|
||||||
intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
|
intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
|
||||||
setResult(Activity.RESULT_OK, intent)
|
setResult(AppCompatActivity.RESULT_OK, intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package com.v2ray.ang.ui
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.zxing.WriterException
|
import android.util.Log
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
|
import java.net.URLDecoder
|
||||||
|
|
||||||
class UrlSchemeActivity : BaseActivity() {
|
class UrlSchemeActivity : BaseActivity() {
|
||||||
private lateinit var binding: ActivityLogcatBinding
|
private lateinit var binding: ActivityLogcatBinding
|
||||||
@@ -18,34 +19,57 @@ class UrlSchemeActivity : BaseActivity() {
|
|||||||
val view = binding.root
|
val view = binding.root
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
|
|
||||||
var shareUrl: String = ""
|
|
||||||
try {
|
try {
|
||||||
intent?.apply {
|
intent.apply {
|
||||||
when (action) {
|
if (action == Intent.ACTION_SEND) {
|
||||||
Intent.ACTION_SEND -> {
|
|
||||||
if ("text/plain" == type) {
|
if ("text/plain" == type) {
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||||
shareUrl = it
|
parseUri(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (action == Intent.ACTION_VIEW) {
|
||||||
Intent.ACTION_VIEW -> {
|
when (data?.host) {
|
||||||
|
"install-config" -> {
|
||||||
val uri: Uri? = intent.data
|
val uri: Uri? = intent.data
|
||||||
shareUrl = uri?.getQueryParameter("url")!!
|
val shareUrl = uri?.getQueryParameter("url") ?: ""
|
||||||
|
parseUri(shareUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"install-sub" -> {
|
||||||
|
val uri: Uri? = intent.data
|
||||||
|
val shareUrl = uri?.getQueryParameter("url") ?: ""
|
||||||
|
parseUri(shareUrl)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
toast(shareUrl)
|
else -> {
|
||||||
val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
|
|
||||||
if (count > 0) {
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
} catch (e: WriterException) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseUri(uriString: String?) {
|
||||||
|
if (uriString.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.d("UrlScheme", uriString)
|
||||||
|
|
||||||
|
val decodedUrl = URLDecoder.decode(uriString, "UTF-8")
|
||||||
|
val uri = Uri.parse(decodedUrl)
|
||||||
|
if (uri != null) {
|
||||||
|
val count = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
||||||
|
if (count > 0) {
|
||||||
|
toast(R.string.import_subscription_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.import_subscription_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.Manifest
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -14,12 +15,14 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.tbruyelle.rxpermissions.RxPermissions
|
import com.tbruyelle.rxpermissions.RxPermissions
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||||
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
import com.v2ray.ang.extension.toTrafficString
|
import com.v2ray.ang.extension.toTrafficString
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
@@ -38,9 +41,11 @@ import java.util.*
|
|||||||
class UserAssetActivity : BaseActivity() {
|
class UserAssetActivity : BaseActivity() {
|
||||||
private lateinit var binding: ActivitySubSettingBinding
|
private lateinit var binding: ActivitySubSettingBinding
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
val geofiles = arrayOf("geosite.dat", "geoip.dat")
|
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -48,13 +53,17 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
val view = binding.root
|
val view = binding.root
|
||||||
setContentView(view)
|
setContentView(view)
|
||||||
title = getString(R.string.title_user_asset_setting)
|
title = getString(R.string.title_user_asset_setting)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
binding.recyclerView.adapter = UserAssetAdapter()
|
binding.recyclerView.adapter = UserAssetAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_asset, menu)
|
menuInflater.inflate(R.menu.menu_asset, menu)
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
@@ -66,6 +75,11 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.add_url -> {
|
||||||
|
val intent = Intent(this, UserAssetUrlActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.download_file -> {
|
R.id.download_file -> {
|
||||||
downloadGeoFiles()
|
downloadGeoFiles()
|
||||||
true
|
true
|
||||||
@@ -75,7 +89,14 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showFileChooser() {
|
private fun showFileChooser() {
|
||||||
RxPermissions(this).request(Manifest.permission.READ_EXTERNAL_STORAGE).subscribe {
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
|
} else {
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
|
RxPermissions(this)
|
||||||
|
.request(permission)
|
||||||
|
.subscribe {
|
||||||
if (it) {
|
if (it) {
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
intent.type = "*/*"
|
intent.type = "*/*"
|
||||||
@@ -91,18 +112,33 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
} catch (ex: android.content.ActivityNotFoundException) {
|
} catch (ex: android.content.ActivityNotFoundException) {
|
||||||
toast(R.string.toast_require_file_manager)
|
toast(R.string.toast_require_file_manager)
|
||||||
}
|
}
|
||||||
}
|
} else
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val chooseFile =
|
private val chooseFile =
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it ->
|
||||||
val uri = it.data?.data
|
val uri = it.data?.data
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
if (it.resultCode == RESULT_OK && uri != null) {
|
||||||
|
val assetId = Utils.getUuid()
|
||||||
try {
|
try {
|
||||||
|
val assetItem = AssetUrlItem(
|
||||||
|
getCursorName(uri) ?: uri.toString(),
|
||||||
|
"file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// check remarks unique
|
||||||
|
val assetList = MmkvManager.decodeAssetUrls()
|
||||||
|
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
|
||||||
|
toast(R.string.msg_remark_is_duplicate)
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
assetStorage?.encode(assetId, Gson().toJson(assetItem))
|
||||||
copyFile(uri)
|
copyFile(uri)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
toast(R.string.toast_asset_copy_failed)
|
toast(R.string.toast_asset_copy_failed)
|
||||||
|
MmkvManager.removeAssetUrl(assetId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,36 +171,45 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
||||||
|
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
geofiles.forEach {
|
var assets = MmkvManager.decodeAssetUrls()
|
||||||
|
assets = addBuiltInGeoItems(assets)
|
||||||
|
|
||||||
|
assets.forEach {
|
||||||
//toast(getString(R.string.msg_downloading_content) + it)
|
//toast(getString(R.string.msg_downloading_content) + it)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val result = downloadGeo(it, 60000, httpPort)
|
var result = downloadGeo(it.second, 60000, httpPort)
|
||||||
|
if (!result) {
|
||||||
|
result = downloadGeo(it.second, 60000, 0)
|
||||||
|
}
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (result) {
|
if (result) {
|
||||||
toast(getString(R.string.toast_success) + " " + it)
|
toast(getString(R.string.toast_success) + " " + it.second.remarks)
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||||
} else {
|
} else {
|
||||||
toast(getString(R.string.toast_failure) + " " + it)
|
toast(getString(R.string.toast_failure) + " " + it.second.remarks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadGeo(name: String, timeout: Int, httpPort: Int): Boolean {
|
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
||||||
val url = AppConfig.geoUrl + name
|
val targetTemp = File(extDir, item.remarks + "_temp")
|
||||||
val targetTemp = File(extDir, name + "_temp")
|
val target = File(extDir, item.remarks)
|
||||||
val target = File(extDir, name)
|
|
||||||
var conn: HttpURLConnection? = null
|
var conn: HttpURLConnection? = null
|
||||||
//Log.d(AppConfig.ANG_PACKAGE, url)
|
//Log.d(AppConfig.ANG_PACKAGE, url)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
conn = URL(url).openConnection(
|
conn = if (httpPort == 0) {
|
||||||
|
URL(item.url).openConnection() as HttpURLConnection
|
||||||
|
} else {
|
||||||
|
URL(item.url).openConnection(
|
||||||
Proxy(
|
Proxy(
|
||||||
Proxy.Type.HTTP,
|
Proxy.Type.HTTP,
|
||||||
InetSocketAddress("127.0.0.1", httpPort)
|
InetSocketAddress("127.0.0.1", httpPort)
|
||||||
)
|
)
|
||||||
) as HttpURLConnection
|
) as HttpURLConnection
|
||||||
|
}
|
||||||
conn.connectTimeout = timeout
|
conn.connectTimeout = timeout
|
||||||
conn.readTimeout = timeout
|
conn.readTimeout = timeout
|
||||||
val inputStream = conn.inputStream
|
val inputStream = conn.inputStream
|
||||||
@@ -184,33 +229,75 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
conn?.disconnect()
|
conn?.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
|
||||||
|
val list = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||||
|
builtInGeoFiles
|
||||||
|
.filter { geoFile -> assets.none { it.second.remarks == geoFile } }
|
||||||
|
.forEach {
|
||||||
|
list.add(Utils.getUuid() to AssetUrlItem(
|
||||||
|
it,
|
||||||
|
AppConfig.GeoUrl + it
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return list + assets
|
||||||
|
}
|
||||||
|
|
||||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||||
return UserAssetViewHolder(ItemRecyclerUserAssetBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
return UserAssetViewHolder(
|
||||||
|
ItemRecyclerUserAssetBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||||
val file = extDir.listFiles()?.getOrNull(position) ?: return
|
var assets = MmkvManager.decodeAssetUrls();
|
||||||
holder.itemUserAssetBinding.assetName.text = file.name
|
assets = addBuiltInGeoItems(assets);
|
||||||
|
val item = assets.getOrNull(position) ?: return
|
||||||
|
// file with name == item.second.remarks
|
||||||
|
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
||||||
|
|
||||||
|
holder.itemUserAssetBinding.assetName.text = item.second.remarks
|
||||||
|
|
||||||
|
if (file != null) {
|
||||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
||||||
holder.itemUserAssetBinding.assetProperties.text = "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
|
holder.itemUserAssetBinding.assetProperties.text =
|
||||||
if (file.name in geofiles) {
|
"${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
|
||||||
|
} else {
|
||||||
|
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
||||||
holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
||||||
} else {
|
} else {
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
|
||||||
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
|
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
|
||||||
|
val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java)
|
||||||
|
intent.putExtra("assetId", item.first)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
||||||
file.delete()
|
file?.delete()
|
||||||
|
MmkvManager.removeAssetUrl(item.first)
|
||||||
binding.recyclerView.adapter?.notifyItemRemoved(position)
|
binding.recyclerView.adapter?.notifyItemRemoved(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return extDir.listFiles()?.size ?: 0
|
var assets = MmkvManager.decodeAssetUrls();
|
||||||
|
assets = addBuiltInGeoItems(assets);
|
||||||
|
return assets.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
||||||
|
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
|
||||||
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class UserAssetUrlActivity : BaseActivity() {
|
||||||
|
private lateinit var binding: ActivityUserAssetUrlBinding
|
||||||
|
|
||||||
|
var del_config: MenuItem? = null
|
||||||
|
var save_config: MenuItem? = null
|
||||||
|
|
||||||
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
|
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityUserAssetUrlBinding.inflate(layoutInflater)
|
||||||
|
val view = binding.root
|
||||||
|
setContentView(view)
|
||||||
|
title = getString(R.string.title_user_asset_add_url)
|
||||||
|
|
||||||
|
val json = assetStorage?.decodeString(editAssetId)
|
||||||
|
if (!json.isNullOrBlank()) {
|
||||||
|
bindingAsset(Gson().fromJson(json, AssetUrlItem::class.java))
|
||||||
|
} else {
|
||||||
|
clearAsset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bingding seleced asset config
|
||||||
|
*/
|
||||||
|
private fun bindingAsset(assetItem: AssetUrlItem): Boolean {
|
||||||
|
binding.etRemarks.text = Utils.getEditable(assetItem.remarks)
|
||||||
|
binding.etUrl.text = Utils.getEditable(assetItem.url)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clear or init asset config
|
||||||
|
*/
|
||||||
|
private fun clearAsset(): Boolean {
|
||||||
|
binding.etRemarks.text = null
|
||||||
|
binding.etUrl.text = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* save asset config
|
||||||
|
*/
|
||||||
|
private fun saveServer(): Boolean {
|
||||||
|
val assetItem: AssetUrlItem
|
||||||
|
val json = assetStorage?.decodeString(editAssetId)
|
||||||
|
var assetId = editAssetId
|
||||||
|
if (!json.isNullOrBlank()) {
|
||||||
|
assetItem = Gson().fromJson(json, AssetUrlItem::class.java)
|
||||||
|
|
||||||
|
// remove file associated with the asset
|
||||||
|
val file = extDir.resolve(assetItem.remarks)
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assetId = Utils.getUuid()
|
||||||
|
assetItem = AssetUrlItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
assetItem.remarks = binding.etRemarks.text.toString()
|
||||||
|
assetItem.url = binding.etUrl.text.toString()
|
||||||
|
|
||||||
|
// check remarks unique
|
||||||
|
val assetList = MmkvManager.decodeAssetUrls()
|
||||||
|
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
|
||||||
|
toast(R.string.msg_remark_is_duplicate)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(assetItem.remarks)) {
|
||||||
|
toast(R.string.sub_setting_remarks)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (TextUtils.isEmpty(assetItem.url)) {
|
||||||
|
toast(R.string.title_url)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
assetStorage?.encode(assetId, Gson().toJson(assetItem))
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* save server config
|
||||||
|
*/
|
||||||
|
private fun deleteServer(): Boolean {
|
||||||
|
if (editAssetId.isNotEmpty()) {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
MmkvManager.removeAssetUrl(editAssetId)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.action_server, menu)
|
||||||
|
del_config = menu.findItem(R.id.del_config)
|
||||||
|
save_config = menu.findItem(R.id.save_config)
|
||||||
|
|
||||||
|
if (editAssetId.isEmpty()) {
|
||||||
|
del_config?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
R.id.del_config -> {
|
||||||
|
deleteServer()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.save_config -> {
|
||||||
|
saveServer()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@ import android.content.pm.PackageInfo
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
object AppManagerUtil {
|
object AppManagerUtil {
|
||||||
fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
|
fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
|
||||||
@@ -22,7 +21,7 @@ object AppManagerUtil {
|
|||||||
|
|
||||||
val appName = applicationInfo.loadLabel(packageManager).toString()
|
val appName = applicationInfo.loadLabel(packageManager).toString()
|
||||||
val appIcon = applicationInfo.loadIcon(packageManager)
|
val appIcon = applicationInfo.loadIcon(packageManager)
|
||||||
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
|
val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0
|
||||||
|
|
||||||
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
||||||
apps.add(appInfo)
|
apps.add(appInfo)
|
||||||
@@ -31,7 +30,8 @@ object AppManagerUtil {
|
|||||||
return apps
|
return apps
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> = Observable.unsafeCreate {
|
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
|
||||||
|
Observable.unsafeCreate {
|
||||||
it.onNext(loadNetworkAppList(ctx))
|
it.onNext(loadNetworkAppList(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package com.v2ray.ang.util
|
|||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
import com.v2ray.ang.dto.ServerAffiliationInfo
|
import com.v2ray.ang.dto.ServerAffiliationInfo
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
object MmkvManager {
|
object MmkvManager {
|
||||||
const val ID_MAIN = "MAIN"
|
const val ID_MAIN = "MAIN"
|
||||||
@@ -12,6 +14,7 @@ object MmkvManager {
|
|||||||
const val ID_SERVER_RAW = "SERVER_RAW"
|
const val ID_SERVER_RAW = "SERVER_RAW"
|
||||||
const val ID_SERVER_AFF = "SERVER_AFF"
|
const val ID_SERVER_AFF = "SERVER_AFF"
|
||||||
const val ID_SUB = "SUB"
|
const val ID_SUB = "SUB"
|
||||||
|
const val ID_ASSET = "ASSET"
|
||||||
const val ID_SETTING = "SETTING"
|
const val ID_SETTING = "SETTING"
|
||||||
const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
|
const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
|
||||||
const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
|
const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
|
||||||
@@ -20,6 +23,7 @@ object MmkvManager {
|
|||||||
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||||
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
|
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
|
||||||
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
fun decodeServerList(): MutableList<String> {
|
fun decodeServerList(): MutableList<String> {
|
||||||
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
|
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
|
||||||
@@ -46,7 +50,7 @@ object MmkvManager {
|
|||||||
serverStorage?.encode(key, Gson().toJson(config))
|
serverStorage?.encode(key, Gson().toJson(config))
|
||||||
val serverList = decodeServerList()
|
val serverList = decodeServerList()
|
||||||
if (!serverList.contains(key)) {
|
if (!serverList.contains(key)) {
|
||||||
serverList.add(key)
|
serverList.add(0, key)
|
||||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||||
if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) {
|
if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) {
|
||||||
mainStorage?.encode(KEY_SELECTED_SERVER, key)
|
mainStorage?.encode(KEY_SELECTED_SERVER, key)
|
||||||
@@ -102,8 +106,8 @@ object MmkvManager {
|
|||||||
serverAffStorage?.encode(guid, Gson().toJson(aff))
|
serverAffStorage?.encode(guid, Gson().toJson(aff))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearAllTestDelayResults() {
|
fun clearAllTestDelayResults(keys: List<String>?) {
|
||||||
serverAffStorage?.allKeys()?.forEach { key ->
|
keys?.forEach { key ->
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
aff.testDelayMillis = 0
|
aff.testDelayMillis = 0
|
||||||
serverAffStorage?.encode(key, Gson().toJson(aff))
|
serverAffStorage?.encode(key, Gson().toJson(aff))
|
||||||
@@ -118,8 +122,9 @@ object MmkvManager {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(url))
|
||||||
val subItem = SubscriptionItem()
|
val subItem = SubscriptionItem()
|
||||||
subItem.remarks = "import sub"
|
subItem.remarks = Utils.urlDecode(uri.fragment ?: "import sub")
|
||||||
subItem.url = url
|
subItem.url = url
|
||||||
subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
|
subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
|
||||||
return 1
|
return 1
|
||||||
@@ -142,6 +147,22 @@ object MmkvManager {
|
|||||||
removeServerViaSubid(subid)
|
removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
||||||
|
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||||
|
assetStorage?.allKeys()?.forEach { key ->
|
||||||
|
val json = assetStorage?.decodeString(key)
|
||||||
|
if (!json.isNullOrBlank()) {
|
||||||
|
assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
||||||
|
return assetUrlItems
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAssetUrl(assetid: String) {
|
||||||
|
assetStorage?.remove(assetid)
|
||||||
|
}
|
||||||
|
|
||||||
fun removeAllServer() {
|
fun removeAllServer() {
|
||||||
mainStorage?.clearAll()
|
mainStorage?.clearAll()
|
||||||
serverStorage?.clearAll()
|
serverStorage?.clearAll()
|
||||||
@@ -151,14 +172,14 @@ object MmkvManager {
|
|||||||
fun removeInvalidServer() {
|
fun removeInvalidServer() {
|
||||||
serverAffStorage?.allKeys()?.forEach { key ->
|
serverAffStorage?.allKeys()?.forEach { key ->
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
if (aff.testDelayMillis <= 0L) {
|
if (aff.testDelayMillis < 0L) {
|
||||||
removeServer(key)
|
removeServer(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortByTestResults( ) {
|
fun sortByTestResults() {
|
||||||
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
||||||
|
|
||||||
val serverDelays = mutableListOf<ServerDelay>()
|
val serverDelays = mutableListOf<ServerDelay>()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import android.content.res.Resources
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.LocaleList
|
import android.os.LocaleList
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
|
open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ package com.v2ray.ang.util
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import com.google.zxing.*
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.BinaryBitmap
|
||||||
|
import com.google.zxing.DecodeHintType
|
||||||
|
import com.google.zxing.EncodeHintType
|
||||||
|
import com.google.zxing.NotFoundException
|
||||||
|
import com.google.zxing.RGBLuminanceSource
|
||||||
import com.google.zxing.common.GlobalHistogramBinarizer
|
import com.google.zxing.common.GlobalHistogramBinarizer
|
||||||
import com.google.zxing.common.HybridBinarizer
|
import com.google.zxing.qrcode.QRCodeReader
|
||||||
import java.util.*
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
|
import java.util.EnumMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 描述:解析二维码图片
|
* 描述:解析二维码图片
|
||||||
@@ -13,6 +19,40 @@ import java.util.*
|
|||||||
object QRCodeDecoder {
|
object QRCodeDecoder {
|
||||||
val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java)
|
val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create qrcode using zxing
|
||||||
|
*/
|
||||||
|
fun createQRCode(text: String, size: Int = 800): Bitmap? {
|
||||||
|
try {
|
||||||
|
val hints = HashMap<EncodeHintType, String>()
|
||||||
|
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
||||||
|
val bitMatrix = QRCodeWriter().encode(
|
||||||
|
text,
|
||||||
|
BarcodeFormat.QR_CODE, size, size, hints
|
||||||
|
)
|
||||||
|
val pixels = IntArray(size * size)
|
||||||
|
for (y in 0 until size) {
|
||||||
|
for (x in 0 until size) {
|
||||||
|
if (bitMatrix.get(x, y)) {
|
||||||
|
pixels[y * size + x] = 0xff000000.toInt()
|
||||||
|
} else {
|
||||||
|
pixels[y * size + x] = 0xffffffff.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val bitmap = Bitmap.createBitmap(
|
||||||
|
size, size,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
|
||||||
|
return bitmap
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
||||||
*
|
*
|
||||||
@@ -30,24 +70,37 @@ object QRCodeDecoder {
|
|||||||
* @return 返回二维码图片里的内容 或 null
|
* @return 返回二维码图片里的内容 或 null
|
||||||
*/
|
*/
|
||||||
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
||||||
|
if (bitmap == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
var source: RGBLuminanceSource? = null
|
var source: RGBLuminanceSource? = null
|
||||||
try {
|
try {
|
||||||
val width = bitmap!!.width
|
val width = bitmap.width
|
||||||
val height = bitmap.height
|
val height = bitmap.height
|
||||||
val pixels = IntArray(width * height)
|
val pixels = IntArray(width * height)
|
||||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
source = RGBLuminanceSource(width, height, pixels)
|
source = RGBLuminanceSource(width, height, pixels)
|
||||||
return MultiFormatReader().decode(BinaryBitmap(HybridBinarizer(source)), HINTS).text
|
val qrReader = QRCodeReader()
|
||||||
|
try {
|
||||||
|
val result = try {
|
||||||
|
qrReader.decode(
|
||||||
|
BinaryBitmap(GlobalHistogramBinarizer(source)),
|
||||||
|
mapOf(DecodeHintType.TRY_HARDER to true)
|
||||||
|
)
|
||||||
|
} catch (e: NotFoundException) {
|
||||||
|
qrReader.decode(
|
||||||
|
BinaryBitmap(GlobalHistogramBinarizer(source.invert())),
|
||||||
|
mapOf(DecodeHintType.TRY_HARDER to true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result.text
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
if (source != null) {
|
} catch (e: Exception) {
|
||||||
try {
|
e.printStackTrace()
|
||||||
return MultiFormatReader().decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS).text
|
|
||||||
} catch (e2: Throwable) {
|
|
||||||
e2.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,23 +129,24 @@ object QRCodeDecoder {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
val allFormats: List<BarcodeFormat> = arrayListOf(
|
val allFormats: List<BarcodeFormat> = arrayListOf(
|
||||||
BarcodeFormat.AZTEC
|
BarcodeFormat.AZTEC,
|
||||||
,BarcodeFormat.CODABAR
|
BarcodeFormat.CODABAR,
|
||||||
,BarcodeFormat.CODE_39
|
BarcodeFormat.CODE_39,
|
||||||
,BarcodeFormat.CODE_93
|
BarcodeFormat.CODE_93,
|
||||||
,BarcodeFormat.CODE_128
|
BarcodeFormat.CODE_128,
|
||||||
,BarcodeFormat.DATA_MATRIX
|
BarcodeFormat.DATA_MATRIX,
|
||||||
,BarcodeFormat.EAN_8
|
BarcodeFormat.EAN_8,
|
||||||
,BarcodeFormat.EAN_13
|
BarcodeFormat.EAN_13,
|
||||||
,BarcodeFormat.ITF
|
BarcodeFormat.ITF,
|
||||||
,BarcodeFormat.MAXICODE
|
BarcodeFormat.MAXICODE,
|
||||||
,BarcodeFormat.PDF_417
|
BarcodeFormat.PDF_417,
|
||||||
,BarcodeFormat.QR_CODE
|
BarcodeFormat.QR_CODE,
|
||||||
,BarcodeFormat.RSS_14
|
BarcodeFormat.RSS_14,
|
||||||
,BarcodeFormat.RSS_EXPANDED
|
BarcodeFormat.RSS_EXPANDED,
|
||||||
,BarcodeFormat.UPC_A
|
BarcodeFormat.UPC_A,
|
||||||
,BarcodeFormat.UPC_E
|
BarcodeFormat.UPC_E,
|
||||||
,BarcodeFormat.UPC_EAN_EXTENSION)
|
BarcodeFormat.UPC_EAN_EXTENSION
|
||||||
|
)
|
||||||
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
|
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
|
||||||
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
|
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
|
||||||
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"
|
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ import com.v2ray.ang.extension.responseLength
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.*
|
import java.net.HttpURLConnection
|
||||||
import java.util.*
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.net.Socket
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.UnknownHostException
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
object SpeedtestUtil {
|
object SpeedtestUtil {
|
||||||
@@ -34,7 +38,7 @@ object SpeedtestUtil {
|
|||||||
|
|
||||||
fun realPing(config: String): Long {
|
fun realPing(config: String): Long {
|
||||||
return try {
|
return try {
|
||||||
Libv2ray.measureOutboundDelay(config)
|
Libv2ray.measureOutboundDelay(config, Utils.getDelayTestUrl())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
|
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
|
||||||
-1L
|
-1L
|
||||||
@@ -48,7 +52,8 @@ object SpeedtestUtil {
|
|||||||
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
||||||
if (!TextUtils.isEmpty(allText)) {
|
if (!TextUtils.isEmpty(allText)) {
|
||||||
val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
|
val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
|
||||||
val temps = tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
val temps =
|
||||||
|
tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
if (temps.count() > 0 && temps[0].length < 10) {
|
if (temps.count() > 0 && temps[0].length < 10) {
|
||||||
return temps[0].toFloat().toInt().toString() + "ms"
|
return temps[0].toFloat().toInt().toString() + "ms"
|
||||||
}
|
}
|
||||||
@@ -66,7 +71,7 @@ object SpeedtestUtil {
|
|||||||
tcpTestingSockets.add(socket)
|
tcpTestingSockets.add(socket)
|
||||||
}
|
}
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
socket.connect(InetSocketAddress(url, port),3000)
|
socket.connect(InetSocketAddress(url, port), 3000)
|
||||||
val time = System.currentTimeMillis() - start
|
val time = System.currentTimeMillis() - start
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
tcpTestingSockets.remove(socket)
|
tcpTestingSockets.remove(socket)
|
||||||
@@ -98,13 +103,14 @@ object SpeedtestUtil {
|
|||||||
var conn: HttpURLConnection? = null
|
var conn: HttpURLConnection? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val url = URL("https",
|
val url = URL(Utils.getDelayTestUrl())
|
||||||
"www.google.com",
|
|
||||||
"/generate_204")
|
|
||||||
|
|
||||||
conn = url.openConnection(
|
conn = url.openConnection(
|
||||||
Proxy(Proxy.Type.HTTP,
|
Proxy(
|
||||||
InetSocketAddress("127.0.0.1", port))) as HttpURLConnection
|
Proxy.Type.HTTP,
|
||||||
|
InetSocketAddress("127.0.0.1", port)
|
||||||
|
)
|
||||||
|
) as HttpURLConnection
|
||||||
conn.connectTimeout = 30000
|
conn.connectTimeout = 30000
|
||||||
conn.readTimeout = 30000
|
conn.readTimeout = 30000
|
||||||
conn.setRequestProperty("Connection", "close")
|
conn.setRequestProperty("Connection", "close")
|
||||||
@@ -118,11 +124,19 @@ object SpeedtestUtil {
|
|||||||
if (code == 204 || code == 200 && conn.responseLength == 0L) {
|
if (code == 204 || code == 200 && conn.responseLength == 0L) {
|
||||||
result = context.getString(R.string.connection_test_available, elapsed)
|
result = context.getString(R.string.connection_test_available, elapsed)
|
||||||
} else {
|
} else {
|
||||||
throw IOException(context.getString(R.string.connection_test_error_status_code, code))
|
throw IOException(
|
||||||
|
context.getString(
|
||||||
|
R.string.connection_test_error_status_code,
|
||||||
|
code
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// network exception
|
// network exception
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
|
Log.d(
|
||||||
|
AppConfig.ANG_PACKAGE,
|
||||||
|
"testConnection IOException: " + Log.getStackTraceString(e)
|
||||||
|
)
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
result = context.getString(R.string.connection_test_error, e.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// library exception, eg sumsung
|
// library exception, eg sumsung
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.Editable
|
|
||||||
import android.util.Base64
|
|
||||||
import com.google.zxing.WriterException
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import com.google.zxing.BarcodeFormat
|
|
||||||
import com.google.zxing.qrcode.QRCodeWriter
|
|
||||||
import com.google.zxing.EncodeHintType
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.HashMap
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.LocaleList
|
import android.os.LocaleList
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.text.Editable
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import java.net.*
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.net.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
object Utils {
|
object Utils {
|
||||||
|
|
||||||
@@ -42,8 +39,8 @@ object Utils {
|
|||||||
* @param text
|
* @param text
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
fun getEditable(text: String): Editable {
|
fun getEditable(text: String?): Editable {
|
||||||
return Editable.Factory.getInstance().newEditable(text)
|
return Editable.Factory.getInstance().newEditable(text?:"")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,16 +101,16 @@ object Utils {
|
|||||||
/**
|
/**
|
||||||
* base64 decode
|
* base64 decode
|
||||||
*/
|
*/
|
||||||
fun decode(text: String): String {
|
fun decode(text: String?): String {
|
||||||
tryDecodeBase64(text)?.let { return it }
|
tryDecodeBase64(text)?.let { return it }
|
||||||
if (text.endsWith('=')) {
|
if (text?.endsWith('=')==true) {
|
||||||
// try again for some loosely formatted base64
|
// try again for some loosely formatted base64
|
||||||
tryDecodeBase64(text.trimEnd('='))?.let { return it }
|
tryDecodeBase64(text.trimEnd('='))?.let { return it }
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryDecodeBase64(text: String): String? {
|
fun tryDecodeBase64(text: String?): String? {
|
||||||
try {
|
try {
|
||||||
return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8"))
|
return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8"))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -143,18 +140,16 @@ object Utils {
|
|||||||
* get remote dns servers from preference
|
* get remote dns servers from preference
|
||||||
*/
|
*/
|
||||||
fun getRemoteDnsServers(): List<String> {
|
fun getRemoteDnsServers(): List<String> {
|
||||||
val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_AGENT
|
val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
||||||
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
||||||
if (ret.isEmpty()) {
|
if (ret.isEmpty()) {
|
||||||
return listOf(AppConfig.DNS_AGENT)
|
return listOf(AppConfig.DNS_PROXY)
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVpnDnsServers(): List<String> {
|
fun getVpnDnsServers(): List<String> {
|
||||||
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)
|
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)?:AppConfig.DNS_VPN
|
||||||
?: settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS)
|
|
||||||
?: AppConfig.DNS_AGENT
|
|
||||||
return vpnDns.split(",").filter { isPureIpAddress(it) }
|
return vpnDns.split(",").filter { isPureIpAddress(it) }
|
||||||
// allow empty, in that case dns will use system default
|
// allow empty, in that case dns will use system default
|
||||||
}
|
}
|
||||||
@@ -171,36 +166,6 @@ object Utils {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* create qrcode using zxing
|
|
||||||
*/
|
|
||||||
fun createQRCode(text: String, size: Int = 800): Bitmap? {
|
|
||||||
try {
|
|
||||||
val hints = HashMap<EncodeHintType, String>()
|
|
||||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
|
||||||
val bitMatrix = QRCodeWriter().encode(text,
|
|
||||||
BarcodeFormat.QR_CODE, size, size, hints)
|
|
||||||
val pixels = IntArray(size * size)
|
|
||||||
for (y in 0 until size) {
|
|
||||||
for (x in 0 until size) {
|
|
||||||
if (bitMatrix.get(x, y)) {
|
|
||||||
pixels[y * size + x] = 0xff000000.toInt()
|
|
||||||
} else {
|
|
||||||
pixels[y * size + x] = 0xffffffff.toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val bitmap = Bitmap.createBitmap(size, size,
|
|
||||||
Bitmap.Config.ARGB_8888)
|
|
||||||
bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
|
|
||||||
return bitmap
|
|
||||||
} catch (e: WriterException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* is ip address
|
* is ip address
|
||||||
*/
|
*/
|
||||||
@@ -213,7 +178,7 @@ object Utils {
|
|||||||
//CIDR
|
//CIDR
|
||||||
if (addr.indexOf("/") > 0) {
|
if (addr.indexOf("/") > 0) {
|
||||||
val arr = addr.split("/")
|
val arr = addr.split("/")
|
||||||
if (arr.count() == 2 && Integer.parseInt(arr[1]) > 0) {
|
if (arr.count() == 2 && Integer.parseInt(arr[1]) > -1) {
|
||||||
addr = arr[0]
|
addr = arr[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +209,7 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isPureIpAddress(value: String): Boolean {
|
fun isPureIpAddress(value: String): Boolean {
|
||||||
return (isIpv4Address(value) || isIpv6Address(value))
|
return isIpv4Address(value) || isIpv6Address(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isIpv4Address(value: String): Boolean {
|
fun isIpv4Address(value: String): Boolean {
|
||||||
@@ -274,7 +239,7 @@ object Utils {
|
|||||||
if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) {
|
if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (e: WriterException) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -317,7 +282,7 @@ object Utils {
|
|||||||
|
|
||||||
fun urlDecode(url: String): String {
|
fun urlDecode(url: String): String {
|
||||||
return try {
|
return try {
|
||||||
URLDecoder.decode(URLDecoder.decode(url), "utf-8")
|
URLDecoder.decode(url, "UTF-8")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
url
|
url
|
||||||
@@ -337,7 +302,11 @@ object Utils {
|
|||||||
/**
|
/**
|
||||||
* readTextFromAssets
|
* readTextFromAssets
|
||||||
*/
|
*/
|
||||||
fun readTextFromAssets(context: Context, fileName: String): String {
|
fun readTextFromAssets(context: Context?, fileName: String): String {
|
||||||
|
if(context == null)
|
||||||
|
{
|
||||||
|
return ""
|
||||||
|
}
|
||||||
val content = context.assets.open(fileName).bufferedReader().use {
|
val content = context.assets.open(fileName).bufferedReader().use {
|
||||||
it.readText()
|
it.readText()
|
||||||
}
|
}
|
||||||
@@ -352,6 +321,19 @@ object Utils {
|
|||||||
return extDir.absolutePath
|
return extDir.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun backupPath(context: Context?): String {
|
||||||
|
if (context == null)
|
||||||
|
return ""
|
||||||
|
val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS)
|
||||||
|
?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
|
||||||
|
return extDir.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDeviceIdForXUDPBaseKey(): String {
|
||||||
|
val androidId = Settings.Secure.ANDROID_ID.toByteArray(charset("UTF-8"))
|
||||||
|
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
||||||
|
}
|
||||||
|
|
||||||
fun getUrlContext(url: String, timeout: Int): String {
|
fun getUrlContext(url: String, timeout: Int): String {
|
||||||
var result: String
|
var result: String
|
||||||
var conn: HttpURLConnection? = null
|
var conn: HttpURLConnection? = null
|
||||||
@@ -374,9 +356,20 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getUrlContentWithCustomUserAgent(urlStr: String?): String {
|
fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String {
|
||||||
val url = URL(urlStr)
|
val url = URL(urlStr)
|
||||||
val conn = url.openConnection()
|
val conn = if (httpPort == 0) {
|
||||||
|
url.openConnection()
|
||||||
|
} else {
|
||||||
|
url.openConnection(
|
||||||
|
Proxy(
|
||||||
|
Proxy.Type.HTTP,
|
||||||
|
InetSocketAddress("127.0.0.1", httpPort)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
conn.connectTimeout = timeout
|
||||||
|
conn.readTimeout = timeout
|
||||||
conn.setRequestProperty("Connection", "close")
|
conn.setRequestProperty("Connection", "close")
|
||||||
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
||||||
url.userInfo?.let {
|
url.userInfo?.let {
|
||||||
@@ -391,11 +384,22 @@ object Utils {
|
|||||||
|
|
||||||
fun getDarkModeStatus(context: Context): Boolean {
|
fun getDarkModeStatus(context: Context): Boolean {
|
||||||
val mode = context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK
|
val mode = context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK
|
||||||
return mode == UI_MODE_NIGHT_YES
|
return mode != UI_MODE_NIGHT_NO
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIpv6Address(address: String): String {
|
fun setNightMode(context: Context) {
|
||||||
return if (isIpv6Address(address)) {
|
when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
||||||
|
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
|
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||||
|
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIpv6Address(address: String?): String {
|
||||||
|
if(address == null){
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
|
||||||
String.format("[%s]", address)
|
String.format("[%s]", address)
|
||||||
} else {
|
} else {
|
||||||
address
|
address
|
||||||
@@ -435,5 +439,22 @@ object Utils {
|
|||||||
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file)
|
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file)
|
||||||
.toExternalForm()
|
.toExternalForm()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isTv(context: Context): Boolean =
|
||||||
|
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||||
|
|
||||||
|
fun getDelayTestUrl(): String {
|
||||||
|
val url = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL)
|
||||||
|
return if (url.isNullOrEmpty()) AppConfig.DelayTestUrl else url
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDelayTestUrl(second: Boolean = false): String {
|
||||||
|
return if (second) {
|
||||||
|
AppConfig.DelayTestUrl2
|
||||||
|
} else {
|
||||||
|
val url = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL)
|
||||||
|
if (url.isNullOrEmpty()) AppConfig.DelayTestUrl else url
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,34 @@ package com.v2ray.ang.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.*
|
import com.google.gson.Gson
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
|
||||||
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ERoutingMode
|
import com.v2ray.ang.dto.ERoutingMode
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
|
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
|
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
|
||||||
|
|
||||||
object V2rayConfigUtil {
|
object V2rayConfigUtil {
|
||||||
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
private val serverRawStorage by lazy {
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SERVER_RAW,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val settingsStorage by lazy {
|
||||||
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SETTING,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class Result(var status: Boolean, var content: String)
|
data class Result(var status: Boolean, var content: String)
|
||||||
|
|
||||||
@@ -32,12 +47,18 @@ object V2rayConfigUtil {
|
|||||||
} else {
|
} else {
|
||||||
raw
|
raw
|
||||||
}
|
}
|
||||||
Log.d(ANG_PACKAGE, customConfig)
|
//Log.d(ANG_PACKAGE, customConfig)
|
||||||
return Result(true, customConfig)
|
return Result(true, customConfig)
|
||||||
}
|
}
|
||||||
val outbound = config.getProxyOutbound() ?: return Result(false, "")
|
val outbound = config.getProxyOutbound() ?: return Result(false, "")
|
||||||
val result = getV2rayNonCustomConfig(context, outbound)
|
val address = outbound.getServerAddress() ?: return Result(false, "")
|
||||||
Log.d(ANG_PACKAGE, result.content)
|
if (!Utils.isIpAddress(address) && !Utils.isValidUrl(address)) {
|
||||||
|
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
|
||||||
|
return Result(false, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = getV2rayNonCustomConfig(context, outbound, config.remarks)
|
||||||
|
//Log.d(ANG_PACKAGE, result.content)
|
||||||
return result
|
return result
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -48,7 +69,11 @@ object V2rayConfigUtil {
|
|||||||
/**
|
/**
|
||||||
* 生成v2ray的客户端配置文件
|
* 生成v2ray的客户端配置文件
|
||||||
*/
|
*/
|
||||||
private fun getV2rayNonCustomConfig(context: Context, outbound: V2rayConfig.OutboundBean): Result {
|
private fun getV2rayNonCustomConfig(
|
||||||
|
context: Context,
|
||||||
|
outbound: V2rayConfig.OutboundBean,
|
||||||
|
remarks: String,
|
||||||
|
): Result {
|
||||||
val result = Result(false, "")
|
val result = Result(false, "")
|
||||||
//取得默认配置
|
//取得默认配置
|
||||||
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||||
@@ -64,10 +89,11 @@ object V2rayConfigUtil {
|
|||||||
|
|
||||||
inbounds(v2rayConfig)
|
inbounds(v2rayConfig)
|
||||||
|
|
||||||
httpRequestObject(outbound)
|
updateOutboundWithGlobalSettings(outbound)
|
||||||
|
|
||||||
v2rayConfig.outbounds[0] = outbound
|
v2rayConfig.outbounds[0] = outbound
|
||||||
|
|
||||||
|
updateOutboundFragment(v2rayConfig)
|
||||||
|
|
||||||
routing(v2rayConfig)
|
routing(v2rayConfig)
|
||||||
|
|
||||||
fakedns(v2rayConfig)
|
fakedns(v2rayConfig)
|
||||||
@@ -81,6 +107,9 @@ object V2rayConfigUtil {
|
|||||||
v2rayConfig.stats = null
|
v2rayConfig.stats = null
|
||||||
v2rayConfig.policy = null
|
v2rayConfig.policy = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v2rayConfig.remarks = remarks
|
||||||
|
|
||||||
result.status = true
|
result.status = true
|
||||||
result.content = v2rayConfig.toPrettyPrinting()
|
result.content = v2rayConfig.toPrettyPrinting()
|
||||||
return result
|
return result
|
||||||
@@ -91,8 +120,14 @@ object V2rayConfigUtil {
|
|||||||
*/
|
*/
|
||||||
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
val socksPort = Utils.parseInt(
|
||||||
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT),
|
||||||
|
AppConfig.PORT_SOCKS.toInt()
|
||||||
|
)
|
||||||
|
val httpPort = Utils.parseInt(
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
|
||||||
|
AppConfig.PORT_HTTP.toInt()
|
||||||
|
)
|
||||||
|
|
||||||
v2rayConfig.inbounds.forEach { curInbound ->
|
v2rayConfig.inbounds.forEach { curInbound ->
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
||||||
@@ -103,9 +138,12 @@ object V2rayConfigUtil {
|
|||||||
v2rayConfig.inbounds[0].port = socksPort
|
v2rayConfig.inbounds[0].port = socksPort
|
||||||
val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED)
|
val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED)
|
||||||
?: false
|
?: false
|
||||||
val sniffAllTlsAndHttp = settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
|
val sniffAllTlsAndHttp =
|
||||||
|
settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
|
||||||
?: true
|
?: true
|
||||||
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
|
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
|
||||||
|
v2rayConfig.inbounds[0].sniffing?.routeOnly =
|
||||||
|
settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
|
||||||
if (!sniffAllTlsAndHttp) {
|
if (!sniffAllTlsAndHttp) {
|
||||||
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
|
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
|
||||||
}
|
}
|
||||||
@@ -129,9 +167,12 @@ object V2rayConfigUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fakedns(v2rayConfig: V2rayConfig) {
|
private fun fakedns(v2rayConfig: V2rayConfig) {
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
||||||
|
&& settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
||||||
|
) {
|
||||||
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
|
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
|
||||||
v2rayConfig.outbounds.filter { it.protocol == "freedom" }.forEach {
|
v2rayConfig.outbounds.filter { it.protocol == PROTOCOL_FREEDOM && it.tag == TAG_DIRECT }
|
||||||
|
.forEach {
|
||||||
it.settings?.domainStrategy = "UseIP"
|
it.settings?.domainStrategy = "UseIP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,47 +183,68 @@ object V2rayConfigUtil {
|
|||||||
*/
|
*/
|
||||||
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
routingUserRule(
|
||||||
?: "", AppConfig.TAG_AGENT, v2rayConfig)
|
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||||
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
?: "", AppConfig.TAG_PROXY, v2rayConfig
|
||||||
?: "", AppConfig.TAG_DIRECT, v2rayConfig)
|
)
|
||||||
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
routingUserRule(
|
||||||
?: "", AppConfig.TAG_BLOCKED, v2rayConfig)
|
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||||
|
?: "", TAG_DIRECT, v2rayConfig
|
||||||
|
)
|
||||||
|
routingUserRule(
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
||||||
|
?: "", AppConfig.TAG_BLOCKED, v2rayConfig
|
||||||
|
)
|
||||||
|
|
||||||
v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
v2rayConfig.routing.domainStrategy =
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
||||||
?: "IPIfNonMatch"
|
?: "IPIfNonMatch"
|
||||||
v2rayConfig.routing.domainMatcher = "mph"
|
// v2rayConfig.routing.domainMatcher = "mph"
|
||||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
|
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
||||||
|
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
||||||
|
|
||||||
// Hardcode googleapis.cn
|
// Hardcode googleapis.cn gstatic.com
|
||||||
val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
|
val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
|
||||||
type = "field",
|
outboundTag = AppConfig.TAG_PROXY,
|
||||||
outboundTag = AppConfig.TAG_AGENT,
|
domain = arrayListOf("domain:googleapis.cn", "domain:gstatic.com")
|
||||||
domain = arrayListOf("domain:googleapis.cn")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
when (routingMode) {
|
when (routingMode) {
|
||||||
ERoutingMode.BYPASS_LAN.value -> {
|
ERoutingMode.BYPASS_LAN.value -> {
|
||||||
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
|
routingGeo("ip", "private", TAG_DIRECT, v2rayConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
ERoutingMode.BYPASS_MAINLAND.value -> {
|
ERoutingMode.BYPASS_MAINLAND.value -> {
|
||||||
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
|
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
|
||||||
|
routingGeo("domain", "geolocation-cn", TAG_DIRECT, v2rayConfig)
|
||||||
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
ERoutingMode.BYPASS_LAN_MAINLAND.value -> {
|
ERoutingMode.BYPASS_LAN_MAINLAND.value -> {
|
||||||
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
|
routingGeo("ip", "private", TAG_DIRECT, v2rayConfig)
|
||||||
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
|
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
|
||||||
|
routingGeo("domain", "geolocation-cn", TAG_DIRECT, v2rayConfig)
|
||||||
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
ERoutingMode.GLOBAL_DIRECT.value -> {
|
ERoutingMode.GLOBAL_DIRECT.value -> {
|
||||||
val globalDirect = V2rayConfig.RoutingBean.RulesBean(
|
val globalDirect = V2rayConfig.RoutingBean.RulesBean(
|
||||||
type = "field",
|
outboundTag = TAG_DIRECT,
|
||||||
outboundTag = AppConfig.TAG_DIRECT,
|
|
||||||
port = "0-65535"
|
port = "0-65535"
|
||||||
)
|
)
|
||||||
v2rayConfig.routing.rules.add(globalDirect)
|
v2rayConfig.routing.rules.add(globalDirect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (routingMode != ERoutingMode.GLOBAL_DIRECT.value) {
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
outboundTag = AppConfig.TAG_PROXY,
|
||||||
|
port = "0-65535"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return false
|
return false
|
||||||
@@ -190,13 +252,17 @@ object V2rayConfigUtil {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun routingGeo(ipOrDomain: String, code: String, tag: String, v2rayConfig: V2rayConfig) {
|
private fun routingGeo(
|
||||||
|
ipOrDomain: String,
|
||||||
|
code: String,
|
||||||
|
tag: String,
|
||||||
|
v2rayConfig: V2rayConfig
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
if (!TextUtils.isEmpty(code)) {
|
if (!TextUtils.isEmpty(code)) {
|
||||||
//IP
|
//IP
|
||||||
if (ipOrDomain == "ip" || ipOrDomain == "") {
|
if (ipOrDomain == "ip" || ipOrDomain == "") {
|
||||||
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
||||||
rulesIP.type = "field"
|
|
||||||
rulesIP.outboundTag = tag
|
rulesIP.outboundTag = tag
|
||||||
rulesIP.ip = ArrayList()
|
rulesIP.ip = ArrayList()
|
||||||
rulesIP.ip?.add("geoip:$code")
|
rulesIP.ip?.add("geoip:$code")
|
||||||
@@ -206,7 +272,6 @@ object V2rayConfigUtil {
|
|||||||
if (ipOrDomain == "domain" || ipOrDomain == "") {
|
if (ipOrDomain == "domain" || ipOrDomain == "") {
|
||||||
//Domain
|
//Domain
|
||||||
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
||||||
rulesDomain.type = "field"
|
|
||||||
rulesDomain.outboundTag = tag
|
rulesDomain.outboundTag = tag
|
||||||
rulesDomain.domain = ArrayList()
|
rulesDomain.domain = ArrayList()
|
||||||
rulesDomain.domain?.add("geosite:$code")
|
rulesDomain.domain?.add("geosite:$code")
|
||||||
@@ -223,33 +288,27 @@ object V2rayConfigUtil {
|
|||||||
if (!TextUtils.isEmpty(userRule)) {
|
if (!TextUtils.isEmpty(userRule)) {
|
||||||
//Domain
|
//Domain
|
||||||
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
||||||
rulesDomain.type = "field"
|
|
||||||
rulesDomain.outboundTag = tag
|
rulesDomain.outboundTag = tag
|
||||||
rulesDomain.domain = ArrayList()
|
rulesDomain.domain = ArrayList()
|
||||||
|
|
||||||
//IP
|
//IP
|
||||||
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
||||||
rulesIP.type = "field"
|
|
||||||
rulesIP.outboundTag = tag
|
rulesIP.outboundTag = tag
|
||||||
rulesIP.ip = ArrayList()
|
rulesIP.ip = ArrayList()
|
||||||
|
|
||||||
userRule.split(",").map { it.trim() }.forEach {
|
userRule.split(",").map { it.trim() }.forEach {
|
||||||
if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
|
if (it.startsWith("ext:") && it.contains("geoip")) {
|
||||||
rulesIP.ip?.add(it)
|
rulesIP.ip?.add(it)
|
||||||
} else if (it.isNotEmpty())
|
} else if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
|
||||||
// if (Utils.isValidUrl(it)
|
rulesIP.ip?.add(it)
|
||||||
// || it.startsWith("geosite:")
|
} else if (it.isNotEmpty()) {
|
||||||
// || it.startsWith("regexp:")
|
|
||||||
// || it.startsWith("domain:")
|
|
||||||
// || it.startsWith("full:"))
|
|
||||||
{
|
|
||||||
rulesDomain.domain?.add(it)
|
rulesDomain.domain?.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rulesDomain.domain?.size!! > 0) {
|
if ((rulesDomain.domain?.size ?: 0) > 0) {
|
||||||
v2rayConfig.routing.rules.add(rulesDomain)
|
v2rayConfig.routing.rules.add(rulesDomain)
|
||||||
}
|
}
|
||||||
if (rulesIP.ip?.size!! > 0) {
|
if ((rulesIP.ip?.size ?: 0) > 0) {
|
||||||
v2rayConfig.routing.rules.add(rulesIP)
|
v2rayConfig.routing.rules.add(rulesIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,24 +334,37 @@ object V2rayConfigUtil {
|
|||||||
try {
|
try {
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
||||||
val geositeCn = arrayListOf("geosite:cn")
|
val geositeCn = arrayListOf("geosite:cn")
|
||||||
val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
val proxyDomain = userRule2Domian(
|
||||||
?: "")
|
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||||
val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
?: ""
|
||||||
?: "")
|
)
|
||||||
|
val directDomain = userRule2Domian(
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||||
|
?: ""
|
||||||
|
)
|
||||||
// fakedns with all domains to make it always top priority
|
// fakedns with all domains to make it always top priority
|
||||||
v2rayConfig.dns.servers?.add(0,
|
v2rayConfig.dns.servers?.add(
|
||||||
V2rayConfig.DnsBean.ServersBean(address = "fakedns", domains = geositeCn.plus(proxyDomain).plus(directDomain)))
|
0,
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
address = "fakedns",
|
||||||
|
domains = geositeCn.plus(proxyDomain).plus(directDomain)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS inbound对象
|
// DNS inbound对象
|
||||||
val remoteDns = Utils.getRemoteDnsServers()
|
val remoteDns = Utils.getRemoteDnsServers()
|
||||||
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
|
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
|
||||||
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
|
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
|
||||||
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else "1.1.1.1",
|
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
|
||||||
port = 53,
|
port = 53,
|
||||||
network = "tcp,udp")
|
network = "tcp,udp"
|
||||||
|
)
|
||||||
|
|
||||||
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
val localDnsPort = Utils.parseInt(
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT),
|
||||||
|
AppConfig.PORT_LOCAL_DNS.toInt()
|
||||||
|
)
|
||||||
v2rayConfig.inbounds.add(
|
v2rayConfig.inbounds.add(
|
||||||
V2rayConfig.InboundBean(
|
V2rayConfig.InboundBean(
|
||||||
tag = "dns-in",
|
tag = "dns-in",
|
||||||
@@ -300,7 +372,9 @@ object V2rayConfigUtil {
|
|||||||
listen = "127.0.0.1",
|
listen = "127.0.0.1",
|
||||||
protocol = "dokodemo-door",
|
protocol = "dokodemo-door",
|
||||||
settings = dnsInboundSettings,
|
settings = dnsInboundSettings,
|
||||||
sniffing = null))
|
sniffing = null
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS outbound对象
|
// DNS outbound对象
|
||||||
@@ -311,15 +385,18 @@ object V2rayConfigUtil {
|
|||||||
tag = "dns-out",
|
tag = "dns-out",
|
||||||
settings = null,
|
settings = null,
|
||||||
streamSettings = null,
|
streamSettings = null,
|
||||||
mux = null))
|
mux = null
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS routing tag
|
// DNS routing tag
|
||||||
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
|
v2rayConfig.routing.rules.add(
|
||||||
type = "field",
|
0, V2rayConfig.RoutingBean.RulesBean(
|
||||||
inboundTag = arrayListOf("dns-in"),
|
inboundTag = arrayListOf("dns-in"),
|
||||||
outboundTag = "dns-out",
|
outboundTag = "dns-out",
|
||||||
domain = null)
|
domain = null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -332,44 +409,77 @@ object V2rayConfigUtil {
|
|||||||
try {
|
try {
|
||||||
val hosts = mutableMapOf<String, String>()
|
val hosts = mutableMapOf<String, String>()
|
||||||
val servers = ArrayList<Any>()
|
val servers = ArrayList<Any>()
|
||||||
val remoteDns = Utils.getRemoteDnsServers()
|
|
||||||
val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
|
||||||
?: "")
|
|
||||||
|
|
||||||
|
//remote Dns
|
||||||
|
val remoteDns = Utils.getRemoteDnsServers()
|
||||||
|
val proxyDomain = userRule2Domian(
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||||
|
?: ""
|
||||||
|
)
|
||||||
remoteDns.forEach {
|
remoteDns.forEach {
|
||||||
servers.add(it)
|
servers.add(it)
|
||||||
}
|
}
|
||||||
if (proxyDomain.size > 0) {
|
if (proxyDomain.size > 0) {
|
||||||
servers.add(V2rayConfig.DnsBean.ServersBean(remoteDns.first(), 53, proxyDomain, null))
|
servers.add(
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
remoteDns.first(),
|
||||||
|
53,
|
||||||
|
proxyDomain,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// domestic DNS
|
// domestic DNS
|
||||||
val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
|
||||||
?: "")
|
|
||||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: ERoutingMode.GLOBAL_PROXY.value
|
|
||||||
if (directDomain.size > 0 || routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
|
||||||
val domesticDns = Utils.getDomesticDnsServers()
|
val domesticDns = Utils.getDomesticDnsServers()
|
||||||
val geositeCn = arrayListOf("geosite:cn")
|
val directDomain = userRule2Domian(
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||||
|
?: ""
|
||||||
|
)
|
||||||
|
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
||||||
|
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
||||||
|
val isCnRoutingMode =
|
||||||
|
(routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value)
|
||||||
val geoipCn = arrayListOf("geoip:cn")
|
val geoipCn = arrayListOf("geoip:cn")
|
||||||
|
|
||||||
if (directDomain.size > 0) {
|
if (directDomain.size > 0) {
|
||||||
servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, directDomain, geoipCn))
|
servers.add(
|
||||||
}
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
if (routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
domesticDns.first(),
|
||||||
servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, geositeCn, geoipCn))
|
53,
|
||||||
}
|
directDomain,
|
||||||
if (Utils.isPureIpAddress(domesticDns.first())) {
|
if (isCnRoutingMode) geoipCn else null
|
||||||
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
|
)
|
||||||
type = "field",
|
|
||||||
outboundTag = AppConfig.TAG_DIRECT,
|
|
||||||
port = "53",
|
|
||||||
ip = arrayListOf(domesticDns.first()),
|
|
||||||
domain = null)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (isCnRoutingMode) {
|
||||||
|
val geositeCn = arrayListOf("geosite:cn", "geosite:geolocation-cn")
|
||||||
|
servers.add(
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
domesticDns.first(),
|
||||||
|
53,
|
||||||
|
geositeCn,
|
||||||
|
geoipCn
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val blkDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
if (Utils.isPureIpAddress(domesticDns.first())) {
|
||||||
?: "")
|
v2rayConfig.routing.rules.add(
|
||||||
|
0, V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
outboundTag = TAG_DIRECT,
|
||||||
|
port = "53",
|
||||||
|
ip = arrayListOf(domesticDns.first()),
|
||||||
|
domain = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//block dns
|
||||||
|
val blkDomain = userRule2Domian(
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
||||||
|
?: ""
|
||||||
|
)
|
||||||
if (blkDomain.size > 0) {
|
if (blkDomain.size > 0) {
|
||||||
hosts.putAll(blkDomain.map { it to "127.0.0.1" })
|
hosts.putAll(blkDomain.map { it to "127.0.0.1" })
|
||||||
}
|
}
|
||||||
@@ -380,16 +490,18 @@ object V2rayConfigUtil {
|
|||||||
// DNS dns对象
|
// DNS dns对象
|
||||||
v2rayConfig.dns = V2rayConfig.DnsBean(
|
v2rayConfig.dns = V2rayConfig.DnsBean(
|
||||||
servers = servers,
|
servers = servers,
|
||||||
hosts = hosts)
|
hosts = hosts
|
||||||
|
)
|
||||||
|
|
||||||
// DNS routing
|
// DNS routing
|
||||||
if (Utils.isPureIpAddress(remoteDns.first())) {
|
if (Utils.isPureIpAddress(remoteDns.first())) {
|
||||||
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
|
v2rayConfig.routing.rules.add(
|
||||||
type = "field",
|
0, V2rayConfig.RoutingBean.RulesBean(
|
||||||
outboundTag = AppConfig.TAG_AGENT,
|
outboundTag = AppConfig.TAG_PROXY,
|
||||||
port = "53",
|
port = "53",
|
||||||
ip = arrayListOf(remoteDns.first()),
|
ip = arrayListOf(remoteDns.first()),
|
||||||
domain = null)
|
domain = null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -399,10 +511,49 @@ object V2rayConfigUtil {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun httpRequestObject(outbound: V2rayConfig.OutboundBean): Boolean {
|
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
|
||||||
try {
|
try {
|
||||||
|
var muxEnabled = settingsStorage?.decodeBool(AppConfig.PREF_MUX_ENABLED, false)
|
||||||
|
val protocol = outbound.protocol
|
||||||
|
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.WIREGUARD.name, true)
|
||||||
|
) {
|
||||||
|
muxEnabled = false
|
||||||
|
} else if (protocol.equals(EConfigType.VLESS.name, true)
|
||||||
|
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true
|
||||||
|
) {
|
||||||
|
muxEnabled = false
|
||||||
|
}
|
||||||
|
if (muxEnabled == true) {
|
||||||
|
outbound.mux?.enabled = true
|
||||||
|
outbound.mux?.concurrency =
|
||||||
|
settingsStorage?.decodeInt(AppConfig.PREF_MUX_CONCURRENCY) ?: 8
|
||||||
|
outbound.mux?.xudpConcurrency =
|
||||||
|
settingsStorage?.decodeInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY) ?: 8
|
||||||
|
outbound.mux?.xudpProxyUDP443 =
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
|
||||||
|
} else {
|
||||||
|
outbound.mux?.enabled = false
|
||||||
|
outbound.mux?.concurrency = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
|
var localTunAddr = if (outbound.settings?.address == null) {
|
||||||
|
listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6)
|
||||||
|
} else {
|
||||||
|
outbound.settings?.address as List<*>
|
||||||
|
}
|
||||||
|
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) != true) {
|
||||||
|
localTunAddr = listOf(localTunAddr.first())
|
||||||
|
}
|
||||||
|
outbound.settings?.address = localTunAddr
|
||||||
|
}
|
||||||
|
|
||||||
if (outbound.streamSettings?.network == DEFAULT_NETWORK
|
if (outbound.streamSettings?.network == DEFAULT_NETWORK
|
||||||
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP) {
|
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP
|
||||||
|
) {
|
||||||
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
||||||
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
||||||
|
|
||||||
@@ -419,9 +570,10 @@ object V2rayConfigUtil {
|
|||||||
} else {
|
} else {
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!!
|
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return false
|
return false
|
||||||
@@ -429,4 +581,62 @@ object V2rayConfigUtil {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
|
||||||
|
try {
|
||||||
|
if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.TLS
|
||||||
|
&& v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.REALITY
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragmentOutbound =
|
||||||
|
V2rayConfig.OutboundBean(
|
||||||
|
protocol = PROTOCOL_FREEDOM,
|
||||||
|
tag = TAG_FRAGMENT,
|
||||||
|
mux = null
|
||||||
|
)
|
||||||
|
|
||||||
|
var packets =
|
||||||
|
settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
||||||
|
if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY
|
||||||
|
&& packets == "tlshello"
|
||||||
|
) {
|
||||||
|
packets = "1-3"
|
||||||
|
} else if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.TLS
|
||||||
|
&& packets != "tlshello"
|
||||||
|
) {
|
||||||
|
packets = "tlshello"
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||||
|
fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
|
||||||
|
packets = packets,
|
||||||
|
length = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||||
|
?: "50-100",
|
||||||
|
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||||
|
?: "10-20"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
|
||||||
|
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
||||||
|
TcpNoDelay = true,
|
||||||
|
mark = 255
|
||||||
|
)
|
||||||
|
)
|
||||||
|
v2rayConfig.outbounds.add(fragmentOutbound)
|
||||||
|
|
||||||
|
//proxy chain
|
||||||
|
v2rayConfig.outbounds[0].streamSettings?.sockopt =
|
||||||
|
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
||||||
|
dialerProxy = TAG_FRAGMENT
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/ZipUtil.kt
Normal file
102
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/ZipUtil.kt
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
object ZipUtil {
|
||||||
|
private const val BUFFER_SIZE = 4096
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean {
|
||||||
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (folderPath.isEmpty() || outputZipFilePath.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val filesToCompress = ArrayList<String>()
|
||||||
|
val directory = File(folderPath)
|
||||||
|
if (directory.isDirectory) {
|
||||||
|
directory.listFiles()?.forEach {
|
||||||
|
if (it.isFile) {
|
||||||
|
filesToCompress.add(it.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filesToCompress.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val zos = ZipOutputStream(FileOutputStream(outputZipFilePath))
|
||||||
|
|
||||||
|
filesToCompress.forEach { file ->
|
||||||
|
val ze = ZipEntry(File(file).name)
|
||||||
|
zos.putNextEntry(ze)
|
||||||
|
val inputStream = FileInputStream(file)
|
||||||
|
while (true) {
|
||||||
|
val len = inputStream.read(buffer)
|
||||||
|
if (len <= 0) break
|
||||||
|
zos.write(buffer, 0, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputStream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
zos.closeEntry()
|
||||||
|
zos.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun unzipToFolder(zipFile: File, destDirectory: String): Boolean {
|
||||||
|
File(destDirectory).run {
|
||||||
|
if (!exists()) {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ZipFile(zipFile).use { zip ->
|
||||||
|
zip.entries().asSequence().forEach { entry ->
|
||||||
|
zip.getInputStream(entry).use { input ->
|
||||||
|
val filePath = destDirectory + File.separator + entry.name
|
||||||
|
if (!entry.isDirectory) {
|
||||||
|
// if the entry is a file, extracts it
|
||||||
|
extractFile(input, filePath)
|
||||||
|
} else {
|
||||||
|
// if the entry is a directory, make the directory
|
||||||
|
val dir = File(filePath)
|
||||||
|
dir.mkdir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun extractFile(inputStream: InputStream, destFilePath: String) {
|
||||||
|
val bos = BufferedOutputStream(FileOutputStream(destFilePath))
|
||||||
|
val bytesIn = ByteArray(BUFFER_SIZE)
|
||||||
|
var read: Int
|
||||||
|
while (inputStream.read(bytesIn).also { read = it } != -1) {
|
||||||
|
bos.write(bytesIn, 0, read)
|
||||||
|
}
|
||||||
|
bos.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object ShadowsocksFmt {
|
||||||
|
fun parseShadowsocks(str: String): ServerConfig? {
|
||||||
|
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
|
||||||
|
if (!tryResolveResolveSip002(str, config)) {
|
||||||
|
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||||
|
val indexSplit = result.indexOf("#")
|
||||||
|
if (indexSplit > 0) {
|
||||||
|
try {
|
||||||
|
config.remarks =
|
||||||
|
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.substring(0, indexSplit)
|
||||||
|
}
|
||||||
|
|
||||||
|
//part decode
|
||||||
|
val indexS = result.indexOf("@")
|
||||||
|
result = if (indexS > 0) {
|
||||||
|
Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||||
|
indexS,
|
||||||
|
result.length
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Utils.decode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
|
||||||
|
val match = legacyPattern.matchEntire(result)
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||||
|
server.address = match.groupValues[3].removeSurrounding("[", "]")
|
||||||
|
server.port = match.groupValues[4].toInt()
|
||||||
|
server.password = match.groupValues[2]
|
||||||
|
server.method = match.groupValues[1].lowercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ServerConfig): String {
|
||||||
|
val outbound = config.getProxyOutbound() ?: return ""
|
||||||
|
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||||
|
val pw =
|
||||||
|
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
|
||||||
|
val url = String.format(
|
||||||
|
"%s@%s:%s",
|
||||||
|
pw,
|
||||||
|
Utils.getIpv6Address(outbound.getServerAddress()),
|
||||||
|
outbound.getServerPort()
|
||||||
|
)
|
||||||
|
return url + remark
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
|
||||||
|
try {
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||||
|
|
||||||
|
val method: String
|
||||||
|
val password: String
|
||||||
|
if (uri.userInfo.contains(":")) {
|
||||||
|
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
|
||||||
|
if (arrUserInfo.count() != 2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
method = arrUserInfo[0]
|
||||||
|
password = Utils.urlDecode(arrUserInfo[1])
|
||||||
|
} else {
|
||||||
|
val base64Decode = Utils.decode(uri.userInfo)
|
||||||
|
val arrUserInfo = base64Decode.split(":").map { it.trim() }
|
||||||
|
if (arrUserInfo.count() < 2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
method = arrUserInfo[0]
|
||||||
|
password = base64Decode.substringAfter(":")
|
||||||
|
}
|
||||||
|
|
||||||
|
val query = Utils.urlDecode(uri.query ?: "")
|
||||||
|
if (query != "") {
|
||||||
|
val queryPairs = HashMap<String, String>()
|
||||||
|
val pairs = query.split(";")
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
|
||||||
|
for (pair in pairs) {
|
||||||
|
val idx = pair.indexOf("=")
|
||||||
|
if (idx == -1) {
|
||||||
|
queryPairs[Utils.urlDecode(pair)] = ""
|
||||||
|
} else {
|
||||||
|
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
|
||||||
|
Utils.urlDecode(pair.substring(idx + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
|
||||||
|
var sni: String? = ""
|
||||||
|
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
|
||||||
|
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
|
"tcp",
|
||||||
|
"http",
|
||||||
|
queryPairs["obfs-host"],
|
||||||
|
queryPairs["path"],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} else if (queryPairs["plugin"] == "v2ray-plugin") {
|
||||||
|
var network = "ws"
|
||||||
|
if (queryPairs["mode"] == "quic") {
|
||||||
|
network = "quic"
|
||||||
|
}
|
||||||
|
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
|
network,
|
||||||
|
null,
|
||||||
|
queryPairs["host"],
|
||||||
|
queryPairs["path"],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if ("tls" in queryPairs) {
|
||||||
|
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
|
"tls", false, sni ?: "", null, null, null, null, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||||
|
server.address = uri.idnHost
|
||||||
|
server.port = uri.port
|
||||||
|
server.password = password
|
||||||
|
server.method = method
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, e.toString())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
object SocksFmt {
|
||||||
|
fun parseSocks(str: String): ServerConfig? {
|
||||||
|
val config = ServerConfig.create(EConfigType.SOCKS)
|
||||||
|
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
|
||||||
|
val indexSplit = result.indexOf("#")
|
||||||
|
|
||||||
|
if (indexSplit > 0) {
|
||||||
|
try {
|
||||||
|
config.remarks =
|
||||||
|
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.substring(0, indexSplit)
|
||||||
|
}
|
||||||
|
|
||||||
|
//part decode
|
||||||
|
val indexS = result.indexOf("@")
|
||||||
|
if (indexS > 0) {
|
||||||
|
result = Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||||
|
indexS,
|
||||||
|
result.length
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result = Utils.decode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
|
||||||
|
val match =
|
||||||
|
legacyPattern.matchEntire(result) ?: return null
|
||||||
|
|
||||||
|
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||||
|
server.address = match.groupValues[3].removeSurrounding("[", "]")
|
||||||
|
server.port = match.groupValues[4].toInt()
|
||||||
|
val socksUsersBean =
|
||||||
|
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
socksUsersBean.user = match.groupValues[1]
|
||||||
|
socksUsersBean.pass = match.groupValues[2]
|
||||||
|
server.users = listOf(socksUsersBean)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ServerConfig): String {
|
||||||
|
val outbound = config.getProxyOutbound() ?: return ""
|
||||||
|
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||||
|
val pw =
|
||||||
|
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
|
||||||
|
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
|
||||||
|
else
|
||||||
|
":"
|
||||||
|
val url = String.format(
|
||||||
|
"%s@%s:%s",
|
||||||
|
Utils.encode(pw),
|
||||||
|
Utils.getIpv6Address(outbound.getServerAddress()),
|
||||||
|
outbound.getServerPort()
|
||||||
|
)
|
||||||
|
return url + remark
|
||||||
|
}
|
||||||
|
}
|
||||||
171
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt
Normal file
171
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object TrojanFmt {
|
||||||
|
private val settingsStorage by lazy {
|
||||||
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SETTING,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseTrojan(str: String): ServerConfig? {
|
||||||
|
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||||
|
val config = ServerConfig.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||||
|
|
||||||
|
var flow = ""
|
||||||
|
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
|
||||||
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
|
||||||
|
val queryParam = uri.rawQuery.split("&")
|
||||||
|
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||||
|
|
||||||
|
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
|
queryParam["type"] ?: "tcp",
|
||||||
|
queryParam["headerType"],
|
||||||
|
queryParam["host"],
|
||||||
|
queryParam["path"],
|
||||||
|
queryParam["seed"],
|
||||||
|
queryParam["quicSecurity"],
|
||||||
|
queryParam["key"],
|
||||||
|
queryParam["mode"],
|
||||||
|
queryParam["serviceName"],
|
||||||
|
queryParam["authority"]
|
||||||
|
)
|
||||||
|
fingerprint = queryParam["fp"] ?: ""
|
||||||
|
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
|
||||||
|
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
|
queryParam["security"] ?: V2rayConfig.TLS,
|
||||||
|
allowInsecure,
|
||||||
|
queryParam["sni"] ?: sni ?: "",
|
||||||
|
fingerprint,
|
||||||
|
queryParam["alpn"],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
flow = queryParam["flow"] ?: ""
|
||||||
|
|
||||||
|
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||||
|
server.address = uri.idnHost
|
||||||
|
server.port = uri.port
|
||||||
|
server.password = uri.userInfo
|
||||||
|
server.flow = flow
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ServerConfig): String {
|
||||||
|
val outbound = config.getProxyOutbound() ?: return ""
|
||||||
|
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||||
|
|
||||||
|
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||||
|
val dicQuery = HashMap<String, String>()
|
||||||
|
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
|
||||||
|
if (!TextUtils.isEmpty(it)) {
|
||||||
|
dicQuery["flow"] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
||||||
|
(streamSetting.tlsSettings
|
||||||
|
?: streamSetting.realitySettings)?.let { tlsSetting ->
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
|
||||||
|
dicQuery["sni"] = tlsSetting.serverName
|
||||||
|
}
|
||||||
|
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
||||||
|
dicQuery["alpn"] =
|
||||||
|
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
||||||
|
dicQuery["fp"] = tlsSetting.fingerprint ?: ""
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||||
|
dicQuery["pbk"] = tlsSetting.publicKey ?: ""
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||||
|
dicQuery["sid"] = tlsSetting.shortId ?: ""
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||||
|
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dicQuery["type"] =
|
||||||
|
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
|
||||||
|
|
||||||
|
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||||
|
when (streamSetting.network) {
|
||||||
|
"tcp" -> {
|
||||||
|
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||||
|
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"kcp" -> {
|
||||||
|
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||||
|
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"ws", "httpupgrade", "splithttp" -> {
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||||
|
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||||
|
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"http", "h2" -> {
|
||||||
|
dicQuery["type"] = "http"
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||||
|
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||||
|
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"quic" -> {
|
||||||
|
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||||
|
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
"grpc" -> {
|
||||||
|
dicQuery["mode"] = transportDetails[0]
|
||||||
|
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val query = "?" + dicQuery.toList().joinToString(
|
||||||
|
separator = "&",
|
||||||
|
transform = { it.first + "=" + it.second })
|
||||||
|
|
||||||
|
val url = String.format(
|
||||||
|
"%s@%s:%s",
|
||||||
|
outbound.getPassword(),
|
||||||
|
Utils.getIpv6Address(outbound.getServerAddress()),
|
||||||
|
outbound.getServerPort()
|
||||||
|
)
|
||||||
|
return url + query + remark
|
||||||
|
}
|
||||||
|
}
|
||||||
171
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt
Normal file
171
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object VlessFmt {
|
||||||
|
private val settingsStorage by lazy {
|
||||||
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SETTING,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseVless(str: String): ServerConfig? {
|
||||||
|
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||||
|
val config = ServerConfig.create(EConfigType.VLESS)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
|
val queryParam = uri.rawQuery.split("&")
|
||||||
|
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||||
|
|
||||||
|
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
||||||
|
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||||
|
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
||||||
|
vnext.address = uri.idnHost
|
||||||
|
vnext.port = uri.port
|
||||||
|
vnext.users[0].id = uri.userInfo
|
||||||
|
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
|
||||||
|
vnext.users[0].flow = queryParam["flow"] ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = streamSetting.populateTransportSettings(
|
||||||
|
queryParam["type"] ?: "tcp",
|
||||||
|
queryParam["headerType"],
|
||||||
|
queryParam["host"],
|
||||||
|
queryParam["path"],
|
||||||
|
queryParam["seed"],
|
||||||
|
queryParam["quicSecurity"],
|
||||||
|
queryParam["key"],
|
||||||
|
queryParam["mode"],
|
||||||
|
queryParam["serviceName"],
|
||||||
|
queryParam["authority"]
|
||||||
|
)
|
||||||
|
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
|
||||||
|
streamSetting.populateTlsSettings(
|
||||||
|
queryParam["security"] ?: "",
|
||||||
|
allowInsecure,
|
||||||
|
queryParam["sni"] ?: sni,
|
||||||
|
queryParam["fp"] ?: "",
|
||||||
|
queryParam["alpn"],
|
||||||
|
queryParam["pbk"] ?: "",
|
||||||
|
queryParam["sid"] ?: "",
|
||||||
|
queryParam["spx"] ?: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ServerConfig): String {
|
||||||
|
val outbound = config.getProxyOutbound() ?: return ""
|
||||||
|
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||||
|
|
||||||
|
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||||
|
val dicQuery = HashMap<String, String>()
|
||||||
|
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
|
||||||
|
if (!TextUtils.isEmpty(it)) {
|
||||||
|
dicQuery["flow"] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dicQuery["encryption"] =
|
||||||
|
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
|
||||||
|
else outbound.getSecurityEncryption().orEmpty()
|
||||||
|
|
||||||
|
|
||||||
|
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
||||||
|
(streamSetting.tlsSettings
|
||||||
|
?: streamSetting.realitySettings)?.let { tlsSetting ->
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
|
||||||
|
dicQuery["sni"] = tlsSetting.serverName
|
||||||
|
}
|
||||||
|
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
||||||
|
dicQuery["alpn"] =
|
||||||
|
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
||||||
|
dicQuery["fp"] = tlsSetting.fingerprint ?: ""
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||||
|
dicQuery["pbk"] = tlsSetting.publicKey ?: ""
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||||
|
dicQuery["sid"] = tlsSetting.shortId ?: ""
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||||
|
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dicQuery["type"] =
|
||||||
|
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
|
||||||
|
|
||||||
|
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||||
|
when (streamSetting.network) {
|
||||||
|
"tcp" -> {
|
||||||
|
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||||
|
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"kcp" -> {
|
||||||
|
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||||
|
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"ws", "httpupgrade", "splithttp" -> {
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||||
|
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||||
|
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"http", "h2" -> {
|
||||||
|
dicQuery["type"] = "http"
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||||
|
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||||
|
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"quic" -> {
|
||||||
|
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||||
|
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
"grpc" -> {
|
||||||
|
dicQuery["mode"] = transportDetails[0]
|
||||||
|
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
|
||||||
|
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val query = "?" + dicQuery.toList().joinToString(
|
||||||
|
separator = "&",
|
||||||
|
transform = { it.first + "=" + it.second })
|
||||||
|
|
||||||
|
val url = String.format(
|
||||||
|
"%s@%s:%s",
|
||||||
|
outbound.getPassword(),
|
||||||
|
Utils.getIpv6Address(outbound.getServerAddress()),
|
||||||
|
outbound.getServerPort()
|
||||||
|
)
|
||||||
|
return url + query + remark
|
||||||
|
}
|
||||||
|
}
|
||||||
160
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt
Normal file
160
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.dto.VmessQRCode
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object VmessFmt {
|
||||||
|
private val settingsStorage by lazy {
|
||||||
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseVmess(str: String): ServerConfig? {
|
||||||
|
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
||||||
|
return parseVmessStd(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||||
|
val config = ServerConfig.create(EConfigType.VMESS)
|
||||||
|
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
||||||
|
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
||||||
|
result = Utils.decode(result)
|
||||||
|
if (TextUtils.isEmpty(result)) {
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
|
||||||
|
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
||||||
|
if (TextUtils.isEmpty(vmessQRCode.add)
|
||||||
|
|| TextUtils.isEmpty(vmessQRCode.port)
|
||||||
|
|| TextUtils.isEmpty(vmessQRCode.id)
|
||||||
|
|| TextUtils.isEmpty(vmessQRCode.net)
|
||||||
|
) {
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
config.remarks = vmessQRCode.ps
|
||||||
|
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
||||||
|
vnext.address = vmessQRCode.add
|
||||||
|
vnext.port = Utils.parseInt(vmessQRCode.port)
|
||||||
|
vnext.users[0].id = vmessQRCode.id
|
||||||
|
vnext.users[0].security =
|
||||||
|
if (TextUtils.isEmpty(vmessQRCode.scy)) V2rayConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
||||||
|
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
|
||||||
|
}
|
||||||
|
val sni = streamSetting.populateTransportSettings(
|
||||||
|
vmessQRCode.net,
|
||||||
|
vmessQRCode.type,
|
||||||
|
vmessQRCode.host,
|
||||||
|
vmessQRCode.path,
|
||||||
|
vmessQRCode.path,
|
||||||
|
vmessQRCode.host,
|
||||||
|
vmessQRCode.path,
|
||||||
|
vmessQRCode.type,
|
||||||
|
vmessQRCode.path,
|
||||||
|
vmessQRCode.host
|
||||||
|
)
|
||||||
|
|
||||||
|
val fingerprint = vmessQRCode.fp
|
||||||
|
streamSetting.populateTlsSettings(
|
||||||
|
vmessQRCode.tls,
|
||||||
|
allowInsecure,
|
||||||
|
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
|
||||||
|
fingerprint,
|
||||||
|
vmessQRCode.alpn,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ServerConfig): String {
|
||||||
|
val outbound = config.getProxyOutbound() ?: return ""
|
||||||
|
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||||
|
|
||||||
|
val vmessQRCode = VmessQRCode()
|
||||||
|
vmessQRCode.v = "2"
|
||||||
|
vmessQRCode.ps = config.remarks
|
||||||
|
vmessQRCode.add = outbound.getServerAddress().orEmpty()
|
||||||
|
vmessQRCode.port = outbound.getServerPort().toString()
|
||||||
|
vmessQRCode.id = outbound.getPassword().orEmpty()
|
||||||
|
vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
|
||||||
|
vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
|
||||||
|
vmessQRCode.net = streamSetting.network
|
||||||
|
vmessQRCode.tls = streamSetting.security
|
||||||
|
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
|
||||||
|
vmessQRCode.alpn =
|
||||||
|
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty()
|
||||||
|
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
|
||||||
|
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||||
|
vmessQRCode.type = transportDetails[0]
|
||||||
|
vmessQRCode.host = transportDetails[1]
|
||||||
|
vmessQRCode.path = transportDetails[2]
|
||||||
|
}
|
||||||
|
val json = Gson().toJson(vmessQRCode)
|
||||||
|
return Utils.encode(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseVmessStd(str: String): ServerConfig? {
|
||||||
|
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||||
|
val config = ServerConfig.create(EConfigType.VMESS)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
|
val queryParam = uri.rawQuery.split("&")
|
||||||
|
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||||
|
|
||||||
|
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
||||||
|
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||||
|
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
||||||
|
vnext.address = uri.idnHost
|
||||||
|
vnext.port = uri.port
|
||||||
|
vnext.users[0].id = uri.userInfo
|
||||||
|
vnext.users[0].security = V2rayConfig.DEFAULT_SECURITY
|
||||||
|
vnext.users[0].alterId = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = streamSetting.populateTransportSettings(
|
||||||
|
queryParam["type"] ?: "tcp",
|
||||||
|
queryParam["headerType"],
|
||||||
|
queryParam["host"],
|
||||||
|
queryParam["path"],
|
||||||
|
queryParam["seed"],
|
||||||
|
queryParam["quicSecurity"],
|
||||||
|
queryParam["key"],
|
||||||
|
queryParam["mode"],
|
||||||
|
queryParam["serviceName"],
|
||||||
|
queryParam["authority"]
|
||||||
|
)
|
||||||
|
|
||||||
|
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
|
||||||
|
streamSetting.populateTlsSettings(
|
||||||
|
queryParam["security"] ?: "",
|
||||||
|
allowInsecure,
|
||||||
|
queryParam["sni"] ?: sni,
|
||||||
|
queryParam["fp"] ?: "",
|
||||||
|
queryParam["alpn"],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.extension.removeWhiteSpace
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object WireguardFmt {
|
||||||
|
fun parseWireguard(str: String): ServerConfig? {
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.rawQuery != null) {
|
||||||
|
val config = ServerConfig.create(EConfigType.WIREGUARD)
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||||
|
|
||||||
|
val queryParam = uri.rawQuery.split("&")
|
||||||
|
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||||
|
|
||||||
|
config.outboundBean?.settings?.let { wireguard ->
|
||||||
|
wireguard.secretKey = uri.userInfo
|
||||||
|
wireguard.address =
|
||||||
|
(queryParam["address"]
|
||||||
|
?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
|
||||||
|
.split(",")
|
||||||
|
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: ""
|
||||||
|
wireguard.peers?.get(0)?.endpoint =
|
||||||
|
Utils.getIpv6Address(uri.idnHost) + ":${uri.port}"
|
||||||
|
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
|
wireguard.reserved =
|
||||||
|
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
|
||||||
|
.map { it.toInt() }
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ServerConfig): String {
|
||||||
|
val outbound = config.getProxyOutbound() ?: return ""
|
||||||
|
|
||||||
|
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||||
|
val dicQuery = HashMap<String, String>()
|
||||||
|
dicQuery["publickey"] =
|
||||||
|
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
|
||||||
|
if (outbound.settings?.reserved != null) {
|
||||||
|
dicQuery["reserved"] = Utils.urlEncode(
|
||||||
|
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dicQuery["address"] = Utils.urlEncode(
|
||||||
|
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
if (outbound.settings?.mtu != null) {
|
||||||
|
dicQuery["mtu"] = outbound.settings?.mtu.toString()
|
||||||
|
}
|
||||||
|
val query = "?" + dicQuery.toList().joinToString(
|
||||||
|
separator = "&",
|
||||||
|
transform = { it.first + "=" + it.second })
|
||||||
|
|
||||||
|
val url = String.format(
|
||||||
|
"%s@%s:%s",
|
||||||
|
Utils.urlEncode(outbound.getPassword().toString()),
|
||||||
|
Utils.getIpv6Address(outbound.getServerAddress()),
|
||||||
|
outbound.getServerPort()
|
||||||
|
)
|
||||||
|
return url + query + remark
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
package com.v2ray.ang.viewmodel
|
package com.v2ray.ang.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.*
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
@@ -16,20 +22,49 @@ import com.v2ray.ang.AppConfig
|
|||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.DialogConfigFilterBinding
|
import com.v2ray.ang.databinding.DialogConfigFilterBinding
|
||||||
import com.v2ray.ang.dto.*
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.dto.ServersCache
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.*
|
import com.v2ray.ang.util.MessageUtil
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
|
import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
|
||||||
import kotlinx.coroutines.*
|
import com.v2ray.ang.util.SpeedtestUtil
|
||||||
import java.util.*
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.util.V2rayConfigUtil
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancelChildren
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
private val mainStorage by lazy {
|
||||||
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_MAIN,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val serverRawStorage by lazy {
|
||||||
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SERVER_RAW,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val settingsStorage by lazy {
|
||||||
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SETTING,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var serverList = MmkvManager.decodeServerList()
|
var serverList = MmkvManager.decodeServerList()
|
||||||
var subscriptionId: String = ""
|
var subscriptionId: String = settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "")?:""
|
||||||
var keywordFilter: String = ""
|
var keywordFilter: String = settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
|
||||||
private set
|
private set
|
||||||
val serversCache = mutableListOf<ServersCache>()
|
val serversCache = mutableListOf<ServersCache>()
|
||||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||||
@@ -40,7 +75,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
fun startListenBroadcast() {
|
fun startListenBroadcast() {
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
getApplication<AngApplication>().registerReceiver(mMsgReceiver, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getApplication<AngApplication>().registerReceiver(
|
||||||
|
mMsgReceiver,
|
||||||
|
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY),
|
||||||
|
Context.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
getApplication<AngApplication>().registerReceiver(
|
||||||
|
mMsgReceiver,
|
||||||
|
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||||
|
)
|
||||||
|
}
|
||||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,20 +108,31 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
serverList.remove(guid)
|
serverList.remove(guid)
|
||||||
MmkvManager.removeServer(guid)
|
MmkvManager.removeServer(guid)
|
||||||
val index = getPosition(guid)
|
val index = getPosition(guid)
|
||||||
if(index >= 0){
|
if (index >= 0) {
|
||||||
serversCache.removeAt(index)
|
serversCache.removeAt(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun appendCustomConfigServer(server: String) {
|
fun appendCustomConfigServer(server: String): Boolean {
|
||||||
|
if (server.contains("inbounds")
|
||||||
|
&& server.contains("outbounds")
|
||||||
|
&& server.contains("routing")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
val config = ServerConfig.create(EConfigType.CUSTOM)
|
||||||
config.remarks = System.currentTimeMillis().toString()
|
|
||||||
config.subscriptionId = subscriptionId
|
config.subscriptionId = subscriptionId
|
||||||
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
|
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
|
||||||
|
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
serverRawStorage?.encode(key, server)
|
serverRawStorage?.encode(key, server)
|
||||||
serverList.add(key)
|
serverList.add(0, key)
|
||||||
serversCache.add(ServersCache(key,config))
|
serversCache.add(0, ServersCache(key, config))
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun swapServer(fromPosition: Int, toPosition: Int) {
|
fun swapServer(fromPosition: Int, toPosition: Int) {
|
||||||
@@ -102,7 +159,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun testAllTcping() {
|
fun testAllTcping() {
|
||||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
SpeedtestUtil.closeAllTcpSockets()
|
SpeedtestUtil.closeAllTcpSockets()
|
||||||
MmkvManager.clearAllTestDelayResults()
|
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||||
updateListAction.value = -1 // update all
|
updateListAction.value = -1 // update all
|
||||||
|
|
||||||
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
||||||
@@ -125,15 +182,21 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
fun testAllRealPing() {
|
fun testAllRealPing() {
|
||||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
|
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
|
||||||
MmkvManager.clearAllTestDelayResults()
|
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||||
updateListAction.value = -1 // update all
|
updateListAction.value = -1 // update all
|
||||||
|
|
||||||
|
val serversCopy = serversCache.toList() // Create a copy of the list
|
||||||
|
|
||||||
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
||||||
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
|
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
|
||||||
for (item in serversCache) {
|
for (item in serversCopy) {
|
||||||
val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
|
val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
|
||||||
if (config.status) {
|
if (config.status) {
|
||||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, Pair(item.guid, config.content))
|
MessageUtil.sendMsg2TestService(
|
||||||
|
getApplication(),
|
||||||
|
AppConfig.MSG_MEASURE_CONFIG,
|
||||||
|
Pair(item.guid, config.content)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +206,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
|
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun filterConfig(context :Context) {
|
fun filterConfig(context: Context) {
|
||||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
val listId = subscriptions.map { it.first }.toList().toMutableList()
|
val listId = subscriptions.map { it.first }.toList().toMutableList()
|
||||||
val listRemarks = subscriptions.map { it.second.remarks }.toList().toMutableList()
|
val listRemarks = subscriptions.map { it.second.remarks }.toList().toMutableList()
|
||||||
@@ -155,7 +218,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val ivBinding = DialogConfigFilterBinding.inflate(LayoutInflater.from(context))
|
val ivBinding = DialogConfigFilterBinding.inflate(LayoutInflater.from(context))
|
||||||
ivBinding.spSubscriptionId.adapter = ArrayAdapter<String>( context, android.R.layout.simple_spinner_dropdown_item, listRemarks)
|
ivBinding.spSubscriptionId.adapter = ArrayAdapter<String>(
|
||||||
|
context,
|
||||||
|
android.R.layout.simple_spinner_dropdown_item,
|
||||||
|
listRemarks
|
||||||
|
)
|
||||||
ivBinding.spSubscriptionId.setSelection(checkedItem)
|
ivBinding.spSubscriptionId.setSelection(checkedItem)
|
||||||
ivBinding.etKeyword.text = Utils.getEditable(keywordFilter)
|
ivBinding.etKeyword.text = Utils.getEditable(keywordFilter)
|
||||||
val builder = AlertDialog.Builder(context).setView(ivBinding.root)
|
val builder = AlertDialog.Builder(context).setView(ivBinding.root)
|
||||||
@@ -169,6 +236,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
subscriptions[position].first
|
subscriptions[position].first
|
||||||
}
|
}
|
||||||
keywordFilter = ivBinding.etKeyword.text.toString()
|
keywordFilter = ivBinding.etKeyword.text.toString()
|
||||||
|
settingsStorage?.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
|
||||||
|
settingsStorage?.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
|
||||||
reloadServerList()
|
reloadServerList()
|
||||||
|
|
||||||
dialogInterface?.dismiss()
|
dialogInterface?.dismiss()
|
||||||
@@ -193,7 +262,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
// }.show()
|
// }.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPosition(guid: String) : Int {
|
fun getPosition(guid: String): Int {
|
||||||
serversCache.forEachIndexed { index, it ->
|
serversCache.forEachIndexed { index, it ->
|
||||||
if (it.guid == guid)
|
if (it.guid == guid)
|
||||||
return index
|
return index
|
||||||
@@ -201,29 +270,85 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeDuplicateServer() {
|
||||||
|
val deleteServer = mutableListOf<String>()
|
||||||
|
serversCache.forEachIndexed { index, it ->
|
||||||
|
val outbound = it.config.getProxyOutbound()
|
||||||
|
serversCache.forEachIndexed { index2, it2 ->
|
||||||
|
if (index2 > index) {
|
||||||
|
val outbound2 = it2.config.getProxyOutbound()
|
||||||
|
if (outbound == outbound2 && !deleteServer.contains(it2.guid)) {
|
||||||
|
deleteServer.add(it2.guid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (it in deleteServer) {
|
||||||
|
MmkvManager.removeServer(it)
|
||||||
|
}
|
||||||
|
getApplication<AngApplication>().toast(
|
||||||
|
getApplication<AngApplication>().getString(
|
||||||
|
R.string.title_del_duplicate_config_count,
|
||||||
|
deleteServer.count()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyAssets(assets: AssetManager) {
|
||||||
|
val extFolder = Utils.userAssetPath(getApplication<AngApplication>())
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
try {
|
||||||
|
val geo = arrayOf("geosite.dat", "geoip.dat")
|
||||||
|
assets.list("")
|
||||||
|
?.filter { geo.contains(it) }
|
||||||
|
?.filter { !File(extFolder, it).exists() }
|
||||||
|
?.forEach {
|
||||||
|
val target = File(extFolder, it)
|
||||||
|
assets.open(it).use { input ->
|
||||||
|
FileOutputStream(target).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(
|
||||||
|
ANG_PACKAGE,
|
||||||
|
"Copied from apk assets folder to ${target.absolutePath}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val mMsgReceiver = object : BroadcastReceiver() {
|
private val mMsgReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
AppConfig.MSG_STATE_RUNNING -> {
|
AppConfig.MSG_STATE_RUNNING -> {
|
||||||
isRunning.value = true
|
isRunning.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START_SUCCESS -> {
|
AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||||
getApplication<AngApplication>().toast(R.string.toast_services_success)
|
getApplication<AngApplication>().toast(R.string.toast_services_success)
|
||||||
isRunning.value = true
|
isRunning.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START_FAILURE -> {
|
AppConfig.MSG_STATE_START_FAILURE -> {
|
||||||
getApplication<AngApplication>().toast(R.string.toast_services_failure)
|
getApplication<AngApplication>().toast(R.string.toast_services_failure)
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
|
AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
|
||||||
updateTestResultAction.value = intent.getStringExtra("content")
|
updateTestResultAction.value = intent.getStringExtra("content")
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
||||||
val resultPair = intent.getSerializableExtra("content") as Pair<String, Long>
|
val resultPair = intent.getSerializableExtra("content") as Pair<String, Long>
|
||||||
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
||||||
|
|||||||
@@ -8,40 +8,59 @@ import androidx.preference.PreferenceManager
|
|||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class SettingsViewModel(application: Application) : AndroidViewModel(application), SharedPreferences.OnSharedPreferenceChangeListener {
|
class SettingsViewModel(application: Application) : AndroidViewModel(application),
|
||||||
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
private val settingsStorage by lazy {
|
||||||
|
MMKV.mmkvWithID(
|
||||||
|
MmkvManager.ID_SETTING,
|
||||||
|
MMKV.MULTI_PROCESS_MODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun startListenPreferenceChange() {
|
fun startListenPreferenceChange() {
|
||||||
PreferenceManager.getDefaultSharedPreferences(getApplication()).registerOnSharedPreferenceChangeListener(this)
|
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
||||||
|
.registerOnSharedPreferenceChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
PreferenceManager.getDefaultSharedPreferences(getApplication()).unregisterOnSharedPreferenceChangeListener(this)
|
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
||||||
|
.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared")
|
Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared")
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key")
|
Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key")
|
||||||
when(key) {
|
when (key) {
|
||||||
AppConfig.PREF_MODE,
|
AppConfig.PREF_MODE,
|
||||||
AppConfig.PREF_VPN_DNS,
|
AppConfig.PREF_VPN_DNS,
|
||||||
AppConfig.PREF_REMOTE_DNS,
|
AppConfig.PREF_REMOTE_DNS,
|
||||||
AppConfig.PREF_DOMESTIC_DNS,
|
AppConfig.PREF_DOMESTIC_DNS,
|
||||||
|
AppConfig.PREF_DELAY_TEST_URL,
|
||||||
AppConfig.PREF_LOCAL_DNS_PORT,
|
AppConfig.PREF_LOCAL_DNS_PORT,
|
||||||
AppConfig.PREF_SOCKS_PORT,
|
AppConfig.PREF_SOCKS_PORT,
|
||||||
AppConfig.PREF_HTTP_PORT,
|
AppConfig.PREF_HTTP_PORT,
|
||||||
AppConfig.PREF_LOGLEVEL,
|
AppConfig.PREF_LOGLEVEL,
|
||||||
AppConfig.PREF_LANGUAGE,
|
AppConfig.PREF_LANGUAGE,
|
||||||
|
AppConfig.PREF_UI_MODE_NIGHT,
|
||||||
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
||||||
AppConfig.PREF_ROUTING_MODE,
|
AppConfig.PREF_ROUTING_MODE,
|
||||||
AppConfig.PREF_V2RAY_ROUTING_AGENT,
|
AppConfig.PREF_V2RAY_ROUTING_AGENT,
|
||||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED,
|
AppConfig.PREF_V2RAY_ROUTING_BLOCKED,
|
||||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT, -> {
|
AppConfig.PREF_V2RAY_ROUTING_DIRECT,
|
||||||
|
AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,
|
||||||
|
AppConfig.PREF_FRAGMENT_PACKETS,
|
||||||
|
AppConfig.PREF_FRAGMENT_LENGTH,
|
||||||
|
AppConfig.PREF_FRAGMENT_INTERVAL,
|
||||||
|
AppConfig.PREF_MUX_XUDP_QUIC,
|
||||||
|
-> {
|
||||||
settingsStorage?.encode(key, sharedPreferences.getString(key, ""))
|
settingsStorage?.encode(key, sharedPreferences.getString(key, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppConfig.PREF_ROUTE_ONLY_ENABLED,
|
||||||
AppConfig.PREF_SPEED_ENABLED,
|
AppConfig.PREF_SPEED_ENABLED,
|
||||||
AppConfig.PREF_PROXY_SHARING,
|
AppConfig.PREF_PROXY_SHARING,
|
||||||
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
||||||
@@ -50,15 +69,30 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
AppConfig.PREF_PREFER_IPV6,
|
AppConfig.PREF_PREFER_IPV6,
|
||||||
AppConfig.PREF_PER_APP_PROXY,
|
AppConfig.PREF_PER_APP_PROXY,
|
||||||
AppConfig.PREF_BYPASS_APPS,
|
AppConfig.PREF_BYPASS_APPS,
|
||||||
AppConfig.PREF_CONFIRM_REMOVE, -> {
|
AppConfig.PREF_CONFIRM_REMOVE,
|
||||||
|
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
||||||
|
AppConfig.SUBSCRIPTION_AUTO_UPDATE,
|
||||||
|
AppConfig.PREF_FRAGMENT_ENABLED,
|
||||||
|
AppConfig.PREF_MUX_ENABLED,
|
||||||
|
-> {
|
||||||
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
|
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.PREF_SNIFFING_ENABLED -> {
|
AppConfig.PREF_SNIFFING_ENABLED -> {
|
||||||
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true))
|
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true))
|
||||||
}
|
}
|
||||||
AppConfig.PREF_PER_APP_PROXY_SET -> {
|
|
||||||
settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf()))
|
AppConfig.PREF_MUX_CONCURRENCY,
|
||||||
|
AppConfig.PREF_MUX_XUDP_CONCURRENCY -> {
|
||||||
|
settingsStorage?.encode(key, sharedPreferences.getString(key, "8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppConfig.PREF_PER_APP_PROXY_SET -> {
|
||||||
|
// settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf()))
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
|
||||||
|
Utils.setNightMode(getApplication())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:duration="@android:integer/config_shortAnimTime"
|
|
||||||
android:fromAlpha="0.0"
|
|
||||||
android:interpolator="@android:interpolator/decelerate_quad"
|
|
||||||
android:toAlpha="1.0" />
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:duration="@android:integer/config_shortAnimTime"
|
|
||||||
android:fromAlpha="1.0"
|
|
||||||
android:interpolator="@android:interpolator/accelerate_quad"
|
|
||||||
android:toAlpha="0.0" />
|
|
||||||
BIN
V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name_black.png
Normal file
BIN
V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name_black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 386 B |
12
V2rayNG/app/src/main/res/drawable-night/ic_about_24dp.xml
Normal file
12
V2rayNG/app/src/main/res/drawable-night/ic_about_24dp.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M810.7,206.3A426.7,426.7 0,1 0,512 938.7h5.5A426.7,426.7 0,0 0,810.7 206.3zM771.8,765A360.1,360.1 0,0 1,517.1 874.7L512,874.7a362.7,362.7 0,0 1,-4.9 -725.3h3.4a362.7,362.7 0,0 1,261.3 615.7z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M547.2,390a57.4,57.4 0,0 0,62.3 -55.3,39.3 39.3,0 0,0 -45,-42.7 58.9,58.9 0,0 0,-64 54.6c-1.7,26.9 13.9,43.3 46.7,43.3zM548.3,663.5c-5.5,0 -7.9,-7.3 -2.3,-28.2l31.1,-118c11.7,-42.7 7.9,-71.3 -15.8,-71.3 -28.4,0 -94.7,28.4 -152.5,76.4l11.7,19.4a181.3,181.3 0,0 1,56.1 -24.7c5.5,0 4.7,7.3 0,25.2l-27.3,112.2c-16.6,64 0,77.7 24.5,77.7s85.3,-21.3 141,-77.7l-13.4,-17.9a122.7,122.7 0,0 1,-53.1 26.9z" />
|
||||||
|
</vector>
|
||||||
@@ -5,5 +5,5 @@
|
|||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM13,14h-2v-2h2v2zM13,10h-2L11,6h2v4z"/>
|
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M864,192L704,192L704,96c0,-17.7 -14.3,-32 -32,-32L352,64c-9,0 -17.2,3.7 -22.9,9.7L137.7,265.1c-6,5.8 -9.7,14 -9.7,22.9v512c0,17.7 14.3,32 32,32h160v96c0,17.7 14.3,32 32,32h512c17.7,0 32,-14.3 32,-32L896,224c0,-17.7 -14.3,-32 -32,-32zM320,173.2L320,256h-82.8l82.8,-82.8zM192,768L192,320h160c17.7,0 32,-14.3 32,-32L384,128h256v64h-96c-9,0 -17.2,3.7 -22.9,9.7L329.7,393.1c-6,5.8 -9.7,14 -9.7,22.9v352L192,768zM512,301.2L512,384h-82.8l82.8,-82.8zM832,896L384,896L384,448h160c17.7,0 32,-14.3 32,-32L576,256h256v640z" />
|
||||||
|
</vector>
|
||||||
@@ -5,5 +5,5 @@
|
|||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M20,8L4,8L4,6h16v2zM18,2L6,2v2h12L18,2zM22,12v8c0,1.1 -0.9,2 -2,2L4,22c-1.1,0 -2,-0.9 -2,-2v-8c0,-1.1 0.9,-2 2,-2h16c1.1,0 2,0.9 2,2zM16,16l-6,-3.27v6.53L16,16z"/>
|
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||||
</vector>
|
</vector>
|
||||||
9
V2rayNG/app/src/main/res/drawable-night/ic_fab_check.xml
Normal file
9
V2rayNG/app/src/main/res/drawable-night/ic_fab_check.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
|
||||||
|
</vector>
|
||||||
12
V2rayNG/app/src/main/res/drawable-night/ic_feedback_24dp.xml
Normal file
12
V2rayNG/app/src/main/res/drawable-night/ic_feedback_24dp.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M512,192.7v42.7a21.3,21.3 0,0 1,-21.3 21.3H213.5V770.6l552.9,-2.2v-233.4a21.3,21.3 0,0 1,21.3 -21.3h42.7a21.3,21.3 0,0 1,21.3 21.3v256.1c0,32.6 -24.9,59.3 -56.6,62.4l-6.1,0.3 -598.2,2.1c-32.6,0 -59.3,-24.8 -62.4,-56.6l-0.3,-6V234c0,-32.6 24.8,-59.3 56.6,-62.4l6,-0.3H490.7a21.3,21.3 0,0 1,21.3 21.3z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M848.7,238.2l-250.8,250.8a21.3,21.3 0,0 1,-13.1 6.1c-30.8,2.9 -47.9,2.6 -51.2,-0.8 -3.4,-3.4 -4,-20.8 -2,-52.3a21.3,21.3 0,0 1,6.2 -13.7l250.5,-250.6a21.3,21.3 0,0 1,30.2 0l30.2,30.2a21.3,21.3 0,0 1,0 30.2z" />
|
||||||
|
</vector>
|
||||||
9
V2rayNG/app/src/main/res/drawable-night/ic_file_24dp.xml
Normal file
9
V2rayNG/app/src/main/res/drawable-night/ic_file_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z"
|
||||||
|
android:fillColor="#FFFFFFFF"/>
|
||||||
|
</vector>
|
||||||
12
V2rayNG/app/src/main/res/drawable-night/ic_image_24dp.xml
Normal file
12
V2rayNG/app/src/main/res/drawable-night/ic_image_24dp.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M784,112L240,112c-88,0 -160,72 -160,160v480c0,88 72,160 160,160h544c88,0 160,-72 160,-160L944,272c0,-88 -72,-160 -160,-160zM880,752c0,52.8 -43.2,96 -96,96L240,848c-52.8,0 -96,-43.2 -96,-96L144,272c0,-52.8 43.2,-96 96,-96h544c52.8,0 96,43.2 96,96v480z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M352,480c52.8,0 96,-43.2 96,-96s-43.2,-96 -96,-96 -96,43.2 -96,96 43.2,96 96,96zM352,352c17.6,0 32,14.4 32,32s-14.4,32 -32,32 -32,-14.4 -32,-32 14.4,-32 32,-32zM814.4,731.2l-3.2,-3.2 -177.6,-177.6c-25.6,-25.6 -65.6,-25.6 -91.2,0l-80,80 -36.8,-36.8c-25.6,-25.6 -65.6,-25.6 -91.2,0L200,728c-4.8,6.4 -8,14.4 -8,24 0,17.6 14.4,32 32,32 9.6,0 16,-3.2 22.4,-9.6L380.8,640l134.4,134.4c6.4,6.4 14.4,9.6 24,9.6 17.6,0 32,-14.4 32,-32 0,-9.6 -4.8,-17.6 -9.6,-24l-52.8,-52.8 80,-80L769.6,776c6.4,4.8 12.8,8 20.8,8 17.6,0 32,-14.4 32,-32 0,-8 -3.2,-16 -8,-20.8z" />
|
||||||
|
</vector>
|
||||||
15
V2rayNG/app/src/main/res/drawable-night/ic_logcat_24dp.xml
Normal file
15
V2rayNG/app/src/main/res/drawable-night/ic_logcat_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z"
|
||||||
|
android:fillColor="#FFFFFFFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z"
|
||||||
|
android:fillColor="#FFFFFFFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z"
|
||||||
|
android:fillColor="#FFFFFFFF"/>
|
||||||
|
</vector>
|
||||||
9
V2rayNG/app/src/main/res/drawable-night/ic_play_24dp.xml
Normal file
9
V2rayNG/app/src/main/res/drawable-night/ic_play_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M213.333333 789.461333V234.538667C213.333333 168.533333 285.013333 127.530667 341.930667 160.981333l471.68 277.461334c56.106667 32.981333 56.106667 114.133333 0 147.114666L341.930667 863.018667C285.056 896.469333 213.333333 855.466667 213.333333 789.461333z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M471.5,77.5a128,128 0,0 1,72.1 -2.6l8.9,2.6 256,85.3a128,128 0,0 1,87.3 113.6l0.3,7.8L896,512c0,101.6 -44.5,186 -103.2,253.1l-8.1,9 -12.1,12.8a618.2,618.2 0,0 1,-25 24.2l-12.8,11.4 -13,11a839.3,839.3 0,0 1,-127.7 86.5l-19.5,10.5 -17.5,9a101.4,101.4 0,0 1,-90.5 0l-18.9,-9.6c-40.1,-21.1 -93.9,-53.2 -145.9,-96.3l-12.9,-11 -12.8,-11.4a618.2,618.2 0,0 1,-24.9 -24.2l-12.1,-12.8 -8.1,-9c-55.9,-63.9 -98.9,-143.4 -102.9,-238.6L128,512L128,284.2A128,128 0,0 1,208.2 165.5l7.3,-2.7 256,-85.3zM512,661.3c-80.2,0 -151.1,40.3 -193.4,101.7 62,57.2 132.2,97.2 176.6,119a37.4,37.4 0,0 0,33.7 0c44.4,-21.9 114.6,-61.9 176.6,-119A234.4,234.4 0,0 0,512 661.3zM491.8,138.2l-256,85.3A64,64 0,0 0,192 284.2L192,512c0,78.5 32.9,146.4 81.5,204.2A298.2,298.2 0,0 1,512 597.3a298.2,298.2 0,0 1,238.5 118.8C799.1,658.4 832,590.5 832,512L832,284.2a64,64 0,0 0,-43.8 -60.7l-256,-85.3a64,64 0,0 0,-40.4 0zM512,298.7a128,128 0,1 1,0 256,128 128,0 0,1 0,-256zM512,362.7a64,64 0,1 0,0 128,64 64,0 0,0 0,-128z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z"
|
||||||
|
android:fillColor="#FFFFFFFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z"
|
||||||
|
android:fillColor="#FFFFFFFF"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M938.7,512a384,384 0,0 1,-384 384,379.3 379.3,0 0,1 -220.2,-69.5 21.8,21.8 0,0 1,-9 -15.8,21.3 21.3,0 0,1 6,-16.6l30.7,-31.1a21.3,21.3 0,0 1,26.9 -2.6A294.8,294.8 0,0 0,554.7 810.7a298.7,298.7 0,1 0,-298.7 -298.7h100.7a20.9,20.9 0,0 1,15.4 6.4l8.5,8.5a21.3,21.3 0,0 1,0 30.3L230,708.3a21.8,21.8 0,0 1,-30.3 0l-150.6,-151a21.3,21.3 0,0 1,0 -30.3l8.5,-8.5a20.9,20.9 0,0 1,15.4 -6.4H170.7a384,384 0,0 1,768 0z" />
|
||||||
|
</vector>
|
||||||
10
V2rayNG/app/src/main/res/drawable-night/ic_scan_24dp.xml
Normal file
10
V2rayNG/app/src/main/res/drawable-night/ic_scan_24dp.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M181.333333 384a32 32 0 0 1-64 0v-111.146667a155.52 155.52 0 0 1 155.52-155.52H384a32 32 0 0 1 0 64h-111.146667a91.52 91.52 0 0 0-91.52 91.52V384zM640 181.333333a32 32 0 0 1 0-64h111.146667a155.52 155.52 0 0 1 155.52 155.52V384a32 32 0 0 1-64 0v-111.146667a91.52 91.52 0 0 0-91.52-91.52H640zM842.666667 640a32 32 0 0 1 64 0v111.146667a155.52 155.52 0 0 1-155.52 155.52H640a32 32 0 0 1 0-64h111.146667a91.52 91.52 0 0 0 91.52-91.52V640zM384 842.666667a32 32 0 0 1 0 64h-111.146667a155.52 155.52 0 0 1-155.52-155.52V640a32 32 0 0 1 64 0v111.146667a91.52 91.52 0 0 0 91.52 91.52H384z m-192-298.666667a32 32 0 0 1 0-64h640a32 32 0 0 1 0 64H192z" />
|
||||||
|
</vector>
|
||||||
12
V2rayNG/app/src/main/res/drawable-night/ic_settings_24dp.xml
Normal file
12
V2rayNG/app/src/main/res/drawable-night/ic_settings_24dp.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M924.8,625.7l-65.5,-56c3.1,-19 4.7,-38.4 4.7,-57.8s-1.6,-38.8 -4.7,-57.8l65.5,-56c10.1,-8.6 13.8,-22.6 9.3,-35.2l-0.9,-2.6c-18.1,-50.5 -44.9,-96.9 -79.7,-137.9l-1.8,-2.1c-8.6,-10.1 -22.5,-13.9 -35.1,-9.5l-81.3,28.9c-30,-24.6 -63.5,-44 -99.7,-57.6l-15.7,-85c-2.4,-13.1 -12.7,-23.3 -25.8,-25.7l-2.7,-0.5c-52.1,-9.4 -106.9,-9.4 -159,0l-2.7,0.5c-13.1,2.4 -23.4,12.6 -25.8,25.7l-15.8,85.4c-35.9,13.6 -69.2,32.9 -99,57.4l-81.9,-29.1c-12.5,-4.4 -26.5,-0.7 -35.1,9.5l-1.8,2.1c-34.8,41.1 -61.6,87.5 -79.7,137.9l-0.9,2.6c-4.5,12.5 -0.8,26.5 9.3,35.2l66.3,56.6c-3.1,18.8 -4.6,38 -4.6,57.1 0,19.2 1.5,38.4 4.6,57.1L99,625.5c-10.1,8.6 -13.8,22.6 -9.3,35.2l0.9,2.6c18.1,50.4 44.9,96.9 79.7,137.9l1.8,2.1c8.6,10.1 22.5,13.9 35.1,9.5l81.9,-29.1c29.8,24.5 63.1,43.9 99,57.4l15.8,85.4c2.4,13.1 12.7,23.3 25.8,25.7l2.7,0.5c26.1,4.7 52.8,7.1 79.5,7.1 26.7,0 53.5,-2.4 79.5,-7.1l2.7,-0.5c13.1,-2.4 23.4,-12.6 25.8,-25.7l15.7,-85c36.2,-13.6 69.7,-32.9 99.7,-57.6l81.3,28.9c12.5,4.4 26.5,0.7 35.1,-9.5l1.8,-2.1c34.8,-41.1 61.6,-87.5 79.7,-137.9l0.9,-2.6c4.5,-12.3 0.8,-26.3 -9.3,-35zM788.3,465.9c2.5,15.1 3.8,30.6 3.8,46.1s-1.3,31 -3.8,46.1l-6.6,40.1 74.7,63.9c-11.3,26.1 -25.6,50.7 -42.6,73.6L721,702.8l-31.4,25.8c-23.9,19.6 -50.5,35 -79.3,45.8l-38.1,14.3 -17.9,97c-28.1,3.2 -56.8,3.2 -85,0l-17.9,-97.2 -37.8,-14.5c-28.5,-10.8 -55,-26.2 -78.7,-45.7l-31.4,-25.9 -93.4,33.2c-17,-22.9 -31.2,-47.6 -42.6,-73.6l75.5,-64.5 -6.5,-40c-2.4,-14.9 -3.7,-30.3 -3.7,-45.5 0,-15.3 1.2,-30.6 3.7,-45.5l6.5,-40 -75.5,-64.5c11.3,-26.1 25.6,-50.7 42.6,-73.6l93.4,33.2 31.4,-25.9c23.7,-19.5 50.2,-34.9 78.7,-45.7l37.9,-14.3 17.9,-97.2c28.1,-3.2 56.8,-3.2 85,0l17.9,97 38.1,14.3c28.7,10.8 55.4,26.2 79.3,45.8l31.4,25.8 92.8,-32.9c17,22.9 31.2,47.6 42.6,73.6L781.8,426l6.5,39.9z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M512,326c-97.2,0 -176,78.8 -176,176s78.8,176 176,176 176,-78.8 176,-176 -78.8,-176 -176,-176zM591.2,581.2C570,602.3 541.9,614 512,614c-29.9,0 -58,-11.7 -79.2,-32.8C411.7,560 400,531.9 400,502c0,-29.9 11.7,-58 32.8,-79.2C454,401.6 482.1,390 512,390c29.9,0 58,11.6 79.2,32.8C612.3,444 624,472.1 624,502c0,29.9 -11.7,58 -32.8,79.2z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M469.2,802.1l-81.7,-24.6L554.8,221.9l81.7,24.6L469.2,802.1zM362.7,654.5l-124.7,-141.7 124.8,-143.5 -64.4,-56 -173.8,199.8 174,197.7 64.1,-56.3zM899.4,513.1l-173.8,-199.8 -64.4,56 124.8,143.5 -124.7,141.7 64.1,56.4 174,-197.7z" />
|
||||||
|
</vector>
|
||||||
9
V2rayNG/app/src/main/res/drawable-night/ic_stop_24dp.xml
Normal file
9
V2rayNG/app/src/main/res/drawable-night/ic_stop_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M256 192h512c35.392 0 64 28.608 64 64v512c0 35.392-28.608 64-64 64H256c-35.392 0-64-28.608-64-64V256c0-35.392 28.608-64 64-64z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M170.6,256h682.7v85.3L170.6,341.3L170.6,256zM256,85.3h512v85.4L256,170.7L256,85.3zM853.3,426.7L170.6,426.7c-46.9,0 -85.3,38.4 -85.3,85.3v341.3c0,47 38.4,85.4 85.3,85.4h682.7c46.9,0 85.3,-38.4 85.3,-85.4L938.6,512c0,-46.9 -38.4,-85.3 -85.3,-85.3zM853.3,853.3L170.6,853.3L170.6,512h682.7v341.3z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M912.2,154A85.3,85.3 0,0 0,853.3 128a85.3,85.3 0,0 0,-33.3 6.8l-708.3,306.8a42.7,42.7 0,0 0,-26.5 42.7V512a42.7,42.7 0,0 0,29.4 42.7L298.7,616.1l55.5,187.3a75.9,75.9 0,0 0,56.3 53.3,62.7 62.7,0 0,0 15.4,0 73.8,73.8 0,0 0,50.8 -20.5l67.8,-64 131.4,103.7a85.3,85.3 0,0 0,90 9.4l14.1,-7.3a88.3,88.3 0,0 0,46.5 -62.7L938.7,235.1a90,90 0,0 0,-26.5 -81.1zM763.7,805.1a25.2,25.2 0,0 1,-12.8 17.5l-14.1,7.3a19.6,19.6 0,0 1,-9 2.1,19.6 19.6,0 0,1 -12.4,-4.7l-160.4,-128a20.9,20.9 0,0 0,-27.7 0l-94.7,89.2a11.1,11.1 0,0 1,-6 2.1V640a21.8,21.8 0,0 1,6.8 -15.8c136.1,-128 217.6,-199.7 266.2,-240.6a15.8,15.8 0,0 0,5.1 -11.1,13.7 13.7,0 0,0 -4.3,-11.1 14.9,14.9 0,0 0,-17.9 -4.3l-322.6,203.5a21.3,21.3 0,0 1,-18.3 0L149.3,494.9l694.2,-301.2a16.6,16.6 0,0 1,7.7 0,22.2 22.2,0 0,1 16.2,7.7 26.9,26.9 0,0 1,6.8 23.5z" />
|
||||||
|
</vector>
|
||||||
BIN
V2rayNG/app/src/main/res/drawable-night/nav_header_bg.png
Normal file
BIN
V2rayNG/app/src/main/res/drawable-night/nav_header_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name_black.png
Normal file
BIN
V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name_black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 679 B |
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
BIN
V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name_black.png
Normal file
BIN
V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name_black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 984 B |
@@ -1,9 +0,0 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<gradient
|
|
||||||
android:angle="135"
|
|
||||||
android:centerColor="#009688"
|
|
||||||
android:endColor="#00695C"
|
|
||||||
android:startColor="#4DB6AC"
|
|
||||||
android:type="linear" />
|
|
||||||
</shape>
|
|
||||||
BIN
V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name_black.png
Normal file
BIN
V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name_black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
12
V2rayNG/app/src/main/res/drawable/ic_about_24dp.xml
Normal file
12
V2rayNG/app/src/main/res/drawable/ic_about_24dp.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M810.7,206.3A426.7,426.7 0,1 0,512 938.7h5.5A426.7,426.7 0,0 0,810.7 206.3zM771.8,765A360.1,360.1 0,0 1,517.1 874.7L512,874.7a362.7,362.7 0,0 1,-4.9 -725.3h3.4a362.7,362.7 0,0 1,261.3 615.7z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M547.2,390a57.4,57.4 0,0 0,62.3 -55.3,39.3 39.3,0 0,0 -45,-42.7 58.9,58.9 0,0 0,-64 54.6c-1.7,26.9 13.9,43.3 46.7,43.3zM548.3,663.5c-5.5,0 -7.9,-7.3 -2.3,-28.2l31.1,-118c11.7,-42.7 7.9,-71.3 -15.8,-71.3 -28.4,0 -94.7,28.4 -152.5,76.4l11.7,19.4a181.3,181.3 0,0 1,56.1 -24.7c5.5,0 4.7,7.3 0,25.2l-27.3,112.2c-16.6,64 0,77.7 24.5,77.7s85.3,-21.3 141,-77.7l-13.4,-17.9a122.7,122.7 0,0 1,-53.1 26.9z" />
|
||||||
|
</vector>
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FF000000"
|
||||||
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user