Compare commits
497 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2574553180 | |||
| 66e77d50bd | |||
| 253bd793d7 | |||
| be30de6728 | |||
| a1455bbb1c | |||
| 1ac19ae3e9 | |||
| 6e6ca209df | |||
| 52699967cd | |||
| 146d20ce86 | |||
| b6959b5990 | |||
| 06649df8b1 | |||
| 0174ed9082 | |||
| adabb281b1 | |||
| 63e710d1ab | |||
| 509a568446 | |||
| 6fd94b53f0 | |||
| 164412fa34 | |||
| 514ca0810e | |||
| 7582f86482 | |||
| 4b4c46e5ae | |||
| 162e156b33 | |||
| bc7d1971ef | |||
| bbdee92f37 | |||
| cf9e830cc7 | |||
| 804e425a87 | |||
| cc21383928 | |||
| 413f4efd69 | |||
| de69605eff | |||
| 9338ba3525 | |||
| 322b6ec615 | |||
| 304232d029 | |||
| f80c3bfe07 | |||
| bb0a62fc8b | |||
| 5bfdca6cd9 | |||
| 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 | |||
| 402d63bd9a |
@@ -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 21 -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/
|
||||
+1
-5
@@ -1,9 +1,5 @@
|
||||
V2rayNG/app/src/main/res/layout/activity_inapp_buy.xml
|
||||
V2rayNG/app/src/main/assets/geoip.dat
|
||||
V2rayNG/app/src/main/assets/geosite.dat
|
||||
V2rayNG/app/src/main/java/com/v2ray/ang/InappBuyActivity.java
|
||||
*.dat
|
||||
*.jks
|
||||
V2rayNG/app/release/output.json
|
||||
.idea/
|
||||
.gradle/
|
||||
.gradle/
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
*.jar
|
||||
*.aar
|
||||
@@ -1,65 +0,0 @@
|
||||
package CoreI
|
||||
|
||||
import (
|
||||
v2core "github.com/xtls/xray-core/core"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
IsRunning bool
|
||||
IsTRunning bool
|
||||
PackageName string
|
||||
PackageCodePath string
|
||||
|
||||
Vpoint v2core.Server
|
||||
}
|
||||
|
||||
func CheckVersion() int {
|
||||
return 23
|
||||
}
|
||||
|
||||
func (v *Status) GetDataDir() string {
|
||||
return v.PackageName
|
||||
}
|
||||
|
||||
func (v *Status) GetApp(name string) string {
|
||||
return v.PackageCodePath + name
|
||||
}
|
||||
|
||||
func (v *Status) GetTun2socksArgs(localDNS bool, enableIPv6 bool) (ret []string) {
|
||||
ret = []string{"--netif-ipaddr",
|
||||
"26.26.26.2",
|
||||
"--netif-netmask",
|
||||
"255.255.255.252",
|
||||
"--socks-server-addr",
|
||||
"127.0.0.1:10808",
|
||||
"--tunmtu",
|
||||
"1500",
|
||||
"--loglevel",
|
||||
"notice",
|
||||
"--enable-udprelay",
|
||||
"--sock-path",
|
||||
v.GetDataDir() + "sock_path",
|
||||
}
|
||||
|
||||
if enableIPv6 {
|
||||
ret = append(ret, "--netif-ip6addr", "da26:2626::2")
|
||||
}
|
||||
|
||||
if localDNS {
|
||||
ret = append(ret, "--dnsgw", "127.0.0.1:10807")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Status) GetVPNSetupArg(localDNS bool, enableIPv6 bool) (ret string) {
|
||||
ret = "m,1500 a,26.26.26.1,30 r,0.0.0.0,0"
|
||||
|
||||
if enableIPv6 {
|
||||
ret += " a,da26:2626::1,126 r,::,0"
|
||||
}
|
||||
if localDNS {
|
||||
ret += " d,26.26.26.2"
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
pb:
|
||||
go get -u github.com/golang/protobuf/protoc-gen-go
|
||||
@echo "pb Start"
|
||||
asset:
|
||||
bash gen_assets.sh download
|
||||
mkdir assets
|
||||
cp -v data/*.dat assets/
|
||||
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geosite.dat > geosite.dat
|
||||
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geoip.dat > geoip.dat
|
||||
|
||||
ANDROID_HOME=$(HOME)/android-sdk-linux
|
||||
export ANDROID_HOME
|
||||
PATH:=$(PATH):$(GOPATH)/bin
|
||||
export PATH
|
||||
downloadGoMobile:
|
||||
sudo apt-get install -qq libstdc++6:i386 lib32z1 expect
|
||||
cd ~ ;curl -L https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/ubuntu-cli-install-android-sdk.sh | sudo bash - > /dev/null
|
||||
ls ~
|
||||
ls ~/android-sdk-linux/
|
||||
|
||||
BuildMobile:
|
||||
gomobile init
|
||||
gomobile bind -v -ldflags='-s -w' github.com/2dust/AndroidLibV2rayLite
|
||||
|
||||
all: asset pb
|
||||
@echo DONE
|
||||
@@ -1,79 +0,0 @@
|
||||
package Escort
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"log"
|
||||
|
||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
||||
)
|
||||
|
||||
func (v *Escorting) EscortRun(proc string, pt []string, additionalEnv string) {
|
||||
log.Println(proc, pt)
|
||||
count := 0
|
||||
for count <= 42 {
|
||||
cmd := exec.Command(proc, pt...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if len(additionalEnv) > 0 {
|
||||
//additionalEnv := "FOO=bar"
|
||||
newEnv := append(os.Environ(), additionalEnv)
|
||||
cmd.Env = newEnv
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Println("EscortRun cmd.Start err", err)
|
||||
goto CMDERROR
|
||||
}
|
||||
|
||||
if v.escortProcess == nil {
|
||||
log.Println("EscortRun v.escortProcess nil")
|
||||
break
|
||||
}
|
||||
|
||||
*v.escortProcess = append(*v.escortProcess, cmd.Process)
|
||||
log.Println("EscortRun Waiting....")
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Println("EscortRun cmd.Wait err:", err)
|
||||
}
|
||||
|
||||
CMDERROR:
|
||||
if v.Status.IsRunning {
|
||||
log.Println("EscortRun Unexpected Exit, Restart now.")
|
||||
count++
|
||||
} else {
|
||||
log.Println("EscortRun Exit")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Escorting) EscortingUp() {
|
||||
if v.escortProcess != nil {
|
||||
return
|
||||
}
|
||||
v.escortProcess = new([](*os.Process))
|
||||
}
|
||||
|
||||
func (v *Escorting) EscortingDown() {
|
||||
if v.escortProcess == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("EscortingDown() Killing all escorted process ")
|
||||
for _, pr := range *v.escortProcess {
|
||||
pr.Kill()
|
||||
if _, err := pr.Wait(); err != nil {
|
||||
log.Println("EscortingDown pr.Wait err:", err)
|
||||
}
|
||||
}
|
||||
v.escortProcess = nil
|
||||
}
|
||||
|
||||
type Escorting struct {
|
||||
escortProcess *[](*os.Process)
|
||||
Status *CoreI.Status
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
# AndroidLibV2rayLite
|
||||
|
||||
进入`AndroidLibV2rayLite`文件夹
|
||||
|
||||
```
|
||||
go mod download
|
||||
```
|
||||
|
||||
|
||||
|
||||
编译aar
|
||||
|
||||
```
|
||||
gomobile bind -v -o android.aar -target=android ./
|
||||
```
|
||||
|
||||
### Preparation
|
||||
- latest Ubuntu environment
|
||||
- At lease 30G free space
|
||||
- Get Repo [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
|
||||
### Prepare Go
|
||||
- Go to https://golang.org/doc/install and install latest go
|
||||
- Make sure `go version` works as expected
|
||||
### Prepare gomobile
|
||||
- Go to https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile and install gomobile
|
||||
- export PATH=$PATH:~/go/bin
|
||||
- Make sure `gomobile init` works as expected
|
||||
### Prepare NDK
|
||||
- Go to https://developer.android.com/ndk/downloads and install latest NDK
|
||||
- export PATH=$PATH:<wherever you ndk is located>
|
||||
- Make sure `ndk-build -v` works as expected
|
||||
### Make
|
||||
- sudo apt install make
|
||||
- Read and understand [build script](https://github.com/2dust/AndroidLibV2rayLite/blob/master/Makefile)
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
package VPN
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
v2net "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/features/dns"
|
||||
"github.com/xtls/xray-core/features/outbound"
|
||||
v2internet "github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
type protectSet interface {
|
||||
Protect(int) int
|
||||
}
|
||||
|
||||
type resolved struct {
|
||||
domain string
|
||||
IPs []net.IP
|
||||
Port int
|
||||
ipIdx uint8
|
||||
ipLock sync.Mutex
|
||||
lastSwitched time.Time
|
||||
}
|
||||
|
||||
// NextIP switch to another resolved result.
|
||||
// there still be race-condition here if multiple err concurently occured
|
||||
// may cause idx keep switching,
|
||||
// but that's an outside error can hardly handled here
|
||||
func (r *resolved) NextIP() {
|
||||
r.ipLock.Lock()
|
||||
defer r.ipLock.Unlock()
|
||||
|
||||
if len(r.IPs) > 1 {
|
||||
|
||||
// throttle, don't switch too quickly
|
||||
now := time.Now()
|
||||
if now.Sub(r.lastSwitched) < time.Second*5 {
|
||||
log.Println("switch too quickly")
|
||||
return
|
||||
}
|
||||
r.lastSwitched = now
|
||||
r.ipIdx++
|
||||
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ipIdx >= uint8(len(r.IPs)) {
|
||||
r.ipIdx = 0
|
||||
}
|
||||
|
||||
cur := r.currentIP()
|
||||
log.Printf("switched to next IP: %s", cur)
|
||||
}
|
||||
|
||||
func (r *resolved) currentIP() net.IP {
|
||||
if len(r.IPs) > 0 {
|
||||
return r.IPs[r.ipIdx]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPreotectedDialer ...
|
||||
func NewPreotectedDialer(p protectSet) *ProtectedDialer {
|
||||
d := &ProtectedDialer{
|
||||
// prefer native lookup on Android
|
||||
resolver: &net.Resolver{PreferGo: false},
|
||||
protectSet: p,
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// ProtectedDialer ...
|
||||
type ProtectedDialer struct {
|
||||
currentServer string
|
||||
resolveChan chan struct{}
|
||||
|
||||
vServer *resolved
|
||||
resolver *net.Resolver
|
||||
|
||||
protectSet
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) IsVServerReady() bool {
|
||||
return (d.vServer != nil)
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) PrepareResolveChan() {
|
||||
d.resolveChan = make(chan struct{})
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) ResolveChan() <-chan struct{} {
|
||||
return d.resolveChan
|
||||
}
|
||||
|
||||
// simplicated version of golang: internetAddrList in src/net/ipsock.go
|
||||
func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) {
|
||||
|
||||
var (
|
||||
err error
|
||||
host, port string
|
||||
portnum int
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if host, port, err = net.SplitHostPort(addr); err != nil {
|
||||
log.Printf("PrepareDomain SplitHostPort Err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if portnum, err = d.resolver.LookupPort(ctx, "tcp", port); err != nil {
|
||||
log.Printf("PrepareDomain LookupPort Err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addrs, err := d.resolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
return nil, fmt.Errorf("domain %s Failed to resolve", addr)
|
||||
}
|
||||
|
||||
IPs := make([]net.IP, len(addrs))
|
||||
for i, ia := range addrs {
|
||||
IPs[i] = ia.IP
|
||||
}
|
||||
|
||||
rs := &resolved{
|
||||
domain: host,
|
||||
IPs: IPs,
|
||||
Port: portnum,
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// PrepareDomain caches direct v2ray server host
|
||||
func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct{}) {
|
||||
log.Printf("Preparing Domain: %s", domainName)
|
||||
d.currentServer = domainName
|
||||
|
||||
defer close(d.resolveChan)
|
||||
maxRetry := 10
|
||||
for {
|
||||
if maxRetry == 0 {
|
||||
log.Println("PrepareDomain maxRetry reached. exiting.")
|
||||
return
|
||||
}
|
||||
|
||||
resolved, err := d.lookupAddr(domainName)
|
||||
if err != nil {
|
||||
maxRetry--
|
||||
log.Printf("PrepareDomain err: %v\n", err)
|
||||
select {
|
||||
case <-closeCh:
|
||||
log.Printf("PrepareDomain exit due to v2ray closed")
|
||||
return
|
||||
case <-time.After(time.Second * 2):
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
d.vServer = resolved
|
||||
log.Printf("Prepare Result:\n Domain: %s\n Port: %d\n IPs: %v\n",
|
||||
resolved.domain, resolved.Port, resolved.IPs)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) getFd(network v2net.Network) (fd int, err error) {
|
||||
switch network {
|
||||
case v2net.Network_TCP:
|
||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
|
||||
case v2net.Network_UDP:
|
||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
||||
default:
|
||||
err = fmt.Errorf("unknow network")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Init implement internet.SystemDialer
|
||||
func (d *ProtectedDialer) Init(_ dns.Client, _ outbound.Manager) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Dial exported as the protected dial method
|
||||
func (d *ProtectedDialer) Dial(ctx context.Context,
|
||||
src v2net.Address, dest v2net.Destination, sockopt *v2internet.SocketConfig) (net.Conn, error) {
|
||||
|
||||
network := dest.Network.SystemString()
|
||||
Address := dest.NetAddr()
|
||||
|
||||
// v2ray server address,
|
||||
// try to connect fixed IP if multiple IP parsed from domain,
|
||||
// and switch to next IP if error occurred
|
||||
if strings.Compare(Address, d.currentServer) == 0 {
|
||||
if d.vServer == nil {
|
||||
log.Println("Dial pending prepare ...", Address)
|
||||
<-d.resolveChan
|
||||
|
||||
// user may close connection during PrepareDomain,
|
||||
// fast return release resources.
|
||||
if d.vServer == nil {
|
||||
return nil, fmt.Errorf("fail to prepare domain %s", d.currentServer)
|
||||
}
|
||||
}
|
||||
|
||||
fd, err := d.getFd(dest.Network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
curIP := d.vServer.currentIP()
|
||||
conn, err := d.fdConn(ctx, curIP, d.vServer.Port, fd)
|
||||
if err != nil {
|
||||
d.vServer.NextIP()
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Using Prepared: %s", curIP)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// v2ray connecting to "domestic" servers, no caching results
|
||||
log.Printf("Not Using Prepared: %s,%s", network, Address)
|
||||
resolved, err := d.lookupAddr(Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fd, err := d.getFd(dest.Network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use the first resolved address.
|
||||
// the result IP may vary, eg: IPv6 addrs comes first if client has ipv6 address
|
||||
return d.fdConn(ctx, resolved.IPs[0], resolved.Port, fd)
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) fdConn(ctx context.Context, ip net.IP, port int, fd int) (net.Conn, error) {
|
||||
|
||||
defer unix.Close(fd)
|
||||
|
||||
// call android VPN service to "protect" the fd connecting straight out
|
||||
d.Protect(fd)
|
||||
|
||||
sa := &unix.SockaddrInet6{
|
||||
Port: port,
|
||||
}
|
||||
copy(sa.Addr[:], ip)
|
||||
|
||||
if err := unix.Connect(fd, sa); err != nil {
|
||||
log.Printf("fdConn unix.Connect err, Close Fd: %d Err: %v", fd, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := os.NewFile(uintptr(fd), "Socket")
|
||||
if file == nil {
|
||||
// returned value will be nil if fd is not a valid file descriptor
|
||||
return nil, errors.New("fdConn fd invalid")
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
//Closing conn does not affect file, and closing file does not affect conn.
|
||||
conn, err := net.FileConn(file)
|
||||
if err != nil {
|
||||
log.Printf("fdConn FileConn Close Fd: %d Err: %v", fd, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package VPN
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v2net "github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
type fakeSupportSet struct{}
|
||||
|
||||
func (f fakeSupportSet) Protect(int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func TestProtectedDialer_PrepareDomain(t *testing.T) {
|
||||
type args struct {
|
||||
domainName string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{"", args{"baidu.com:80"}},
|
||||
// {"", args{"cloudflare.com:443"}},
|
||||
// {"", args{"apple.com:443"}},
|
||||
// {"", args{"110.110.110.110:443"}},
|
||||
// {"", args{"[2002:1234::1]:443"}},
|
||||
}
|
||||
d := NewPreotectedDialer(fakeSupportSet{})
|
||||
for _, tt := range tests {
|
||||
ch := make(chan struct{})
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
go d.PrepareDomain(tt.args.domainName, ch)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
go d.vServer.NextIP()
|
||||
t.Log(d.vServer.currentIP())
|
||||
})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
func TestProtectedDialer_Dial(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{"baidu.com:80", false},
|
||||
{"cloudflare.com:80", false},
|
||||
{"172.16.192.11:80", true},
|
||||
// {"172.16.192.10:80", true},
|
||||
// {"[2fff:4322::1]:443", true},
|
||||
// {"[fc00::1]:443", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ch := make(chan struct{})
|
||||
|
||||
d := NewPreotectedDialer(fakeSupportSet{})
|
||||
d.currentServer = tt.name
|
||||
|
||||
go d.PrepareDomain(tt.name, ch)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
dial := func() {
|
||||
defer wg.Done()
|
||||
dest, _ := v2net.ParseDestination("tcp:" + tt.name)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := d.Dial(ctx, nil, dest, nil)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
_host, _, _ := net.SplitHostPort(tt.name)
|
||||
fmt.Fprintf(conn, fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\n\r\n", _host))
|
||||
status, err := bufio.NewReader(conn).ReadString('\n')
|
||||
t.Logf("%#v, %#v\n", status, err)
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
for n := 0; n < 3; n++ {
|
||||
wg.Add(1)
|
||||
go dial()
|
||||
// time.Sleep(time.Millisecond * 10)
|
||||
// d.pendingMap[tt.name] = make(chan struct{})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_resolved_NextIP(t *testing.T) {
|
||||
type fields struct {
|
||||
domain string
|
||||
IPs []net.IP
|
||||
Port int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{"test1",
|
||||
fields{
|
||||
domain: "www.baidu.com",
|
||||
IPs: []net.IP{
|
||||
net.ParseIP("1.2.3.4"),
|
||||
net.ParseIP("4.3.2.1"),
|
||||
net.ParseIP("1234::1"),
|
||||
net.ParseIP("4321::1"),
|
||||
},
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &resolved{
|
||||
domain: tt.fields.domain,
|
||||
IPs: tt.fields.IPs,
|
||||
Port: tt.fields.Port,
|
||||
}
|
||||
t.Logf("%v", r.IPs)
|
||||
t.Logf("%v", r.currentIP())
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
time.Sleep(3 * time.Second)
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
time.Sleep(5 * time.Second)
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
# Set magic variables for current file & dir
|
||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
||||
__base="$(basename ${__file} .sh)"
|
||||
|
||||
if [[ ! -d $NDK_HOME ]]; then
|
||||
echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
clear_tmp () {
|
||||
rm -rf $TMPDIR
|
||||
}
|
||||
|
||||
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; clear_tmp; exit 1' ERR INT
|
||||
install -m644 $__dir/tun2socks.mk $TMPDIR/
|
||||
|
||||
pushd $TMPDIR
|
||||
git clone --depth=1 https://github.com/XTLS/badvpn.git
|
||||
git clone --depth=1 https://github.com/XTLS/libancillary.git
|
||||
$NDK_HOME/ndk-build \
|
||||
NDK_PROJECT_PATH=. \
|
||||
APP_BUILD_SCRIPT=./tun2socks.mk \
|
||||
APP_ABI=all \
|
||||
APP_PLATFORM=android-19 \
|
||||
NDK_LIBS_OUT=$TMPDIR/libs \
|
||||
NDK_OUT=$TMPDIR/tmp \
|
||||
APP_SHORT_COMMANDS=false LOCAL_SHORT_COMMANDS=false -B -j4
|
||||
|
||||
install -v -m755 libs/armeabi-v7a/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so
|
||||
install -v -m755 libs/arm64-v8a/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so
|
||||
install -v -m755 libs/x86/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so
|
||||
install -v -m755 libs/x86_64/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so
|
||||
popd
|
||||
|
||||
rm -rf $TMPDIR
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
# Set magic variables for current file & dir
|
||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
||||
__base="$(basename ${__file} .sh)"
|
||||
|
||||
|
||||
DATADIR=${__dir}/data
|
||||
|
||||
compile_dat () {
|
||||
local TMPDIR=$(mktemp -d)
|
||||
|
||||
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; rm -rf $TMPDIR; trap ERR; exit 1' ERR
|
||||
|
||||
local GEOSITE=${GOPATH}/src/github.com/v2ray/domain-list-community
|
||||
if [[ -d ${GEOSITE} ]]; then
|
||||
cd ${GEOSITE} && git pull
|
||||
else
|
||||
mkdir -p ${GEOSITE}
|
||||
cd ${GEOSITE} && git clone https://github.com/v2ray/domain-list-community.git .
|
||||
fi
|
||||
go run main.go
|
||||
|
||||
if [[ -e dlc.dat ]]; then
|
||||
rm -f $DATADIR/geosite.dat
|
||||
mv dlc.dat $DATADIR/geosite.dat
|
||||
echo "----------> geosite.dat updated."
|
||||
else
|
||||
echo "----------> geosite.dat failed to update."
|
||||
fi
|
||||
|
||||
|
||||
if [[ ! -x $GOPATH/bin/geoip ]]; then
|
||||
go get -v -u github.com/v2ray/geoip
|
||||
fi
|
||||
|
||||
cd $TMPDIR
|
||||
curl -L -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country-CSV.zip
|
||||
unzip -q GeoLite2-Country-CSV.zip
|
||||
mkdir geoip && find . -name '*.csv' -exec mv -t ./geoip {} +
|
||||
$GOPATH/bin/geoip \
|
||||
--country=./geoip/GeoLite2-Country-Locations-en.csv \
|
||||
--ipv4=./geoip/GeoLite2-Country-Blocks-IPv4.csv \
|
||||
--ipv6=./geoip/GeoLite2-Country-Blocks-IPv6.csv
|
||||
|
||||
if [[ -e geoip.dat ]]; then
|
||||
rm -f $DATADIR/geoip.dat
|
||||
mv ./geoip.dat $DATADIR/geoip.dat
|
||||
echo "----------> geoip.dat updated."
|
||||
else
|
||||
echo "----------> geoip.dat failed to update."
|
||||
fi
|
||||
trap ERR
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
download_dat () {
|
||||
wget -qO - https://api.github.com/repos/v2ray/geoip/releases/latest \
|
||||
| grep browser_download_url | cut -d '"' -f 4 \
|
||||
| wget -i - -O $DATADIR/geoip.dat
|
||||
|
||||
wget -qO - https://api.github.com/repos/v2ray/domain-list-community/releases/latest \
|
||||
| grep browser_download_url | cut -d '"' -f 4 \
|
||||
| wget -i - -O $DATADIR/geosite.dat
|
||||
}
|
||||
|
||||
ACTION="${1:-}"
|
||||
if [[ -z $ACTION ]]; then
|
||||
ACTION=download
|
||||
fi
|
||||
|
||||
case $ACTION in
|
||||
"download") download_dat;;
|
||||
"compile") compile_dat;;
|
||||
esac
|
||||
@@ -1,9 +0,0 @@
|
||||
module github.com/2dust/AndroidLibV2rayLite
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/xtls/xray-core v1.5.2
|
||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08
|
||||
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d
|
||||
)
|
||||
@@ -1,535 +0,0 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0 h1:69FNAINiZfsEuwH3fKq8QrAAnHz+2m4XL4kVYi5BX0Q=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3 h1:hJiie5Bf3QucGRa4ymsAUOxyhYwGEz1xrsVk0P8erlw=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0 h1:SPOUaucgtVls75mg+X7CXigS71EnsfVUK/2CgVrwqgw=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412 h1:GvWw74lx5noHocd+f6HBMXK6DuggBB1dhVkuGZbv7qM=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c h1:ivON6cwHK1OH26MZyWDCnbTRZZf0IhNsENoNAKFS1g4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:OR8VhtwhcAI3U48/rzBsVOuHi0zDPzYI1xASVcdSgR8=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dgryski/go-metro v0.0.0-20211015221634-2661b20a2446 h1:QnWGyQI3H080vbC9E4jlr6scOYEnALtvV/69oATYzOo=
|
||||
github.com/dgryski/go-metro v0.0.0-20211015221634-2661b20a2446/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 h1:fP+fF0up6oPY49OrjPrhIJ8yQfdIM85NXMLkMg1EXVs=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57 h1:eqyIo2HjKhKe/mJzTG8n4VqvLXIOEG+SLdDqX7xGtkY=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3 h1:siORttZ36U2R/WjiJuDz8znElWBiAlO9rVt+mqJt0Cc=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI=
|
||||
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1 h1:ujPKutqRlJtcfWk6toYVYagwra7HQHbXOaS171b4Tg8=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucas-clemente/quic-go v0.24.0 h1:ToR7SIIEdrgOhgVTHvPgdVRJfgVy+N0wQAagH7L4d5g=
|
||||
github.com/lucas-clemente/quic-go v0.24.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0=
|
||||
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
|
||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
||||
github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A=
|
||||
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
|
||||
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
|
||||
github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/pires/go-proxyproto v0.6.1 h1:EBupykFmo22SDjv4fQVQd2J9NOoLPmyZA/15ldOGkPw=
|
||||
github.com/pires/go-proxyproto v0.6.1/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0 h1:1921Yw9Gc3iSc4VQh3PIoOqgPCZS7G/4xQNVUp8Mda8=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 h1:agujYaXJSxSo18YNX3jzl+4G6Bstwt+kqv47GS12uL0=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/refraction-networking/utls v1.0.0 h1:6XQHSjDmeBCF9sPq8p2zMVGq7Ud3rTD2q88Fw8Tz1tA=
|
||||
github.com/refraction-networking/utls v1.0.0/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c h1:pqy40B3MQWYrza7YZXOXgl0Nf0QGFqrOC0BKae1UNAA=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d h1:Yoy/IzG4lULT6qZg62sVC+qyBL8DQkmD2zv6i7OImrc=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c h1:UOk+nlt1BJtTcH15CT7iNO7YVWTfTv/DNwEAQHLIaDQ=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20 h1:7pDq9pAMCQgRohFmd25X8hIH8VxmT3TaDm+r9LHxgBk=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9 h1:MPblCbqA5+z6XARjScMfz1TqtJC7TuTRj0U9VqIBs6k=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50 h1:crYRwvwjdVh1biHzzciFHe8DrZcYrVcZFlJtykhRctg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc h1:eHRtZoIi6n9Wo1uR+RU44C247msLWwyA89hVKwRLkMk=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9 h1:fxoFD0in0/CBzXoyNhMTjvBZYW6ilSnTw7N7y/8vkmM=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191 h1:T4wuULTrzCKMFlg3HmKHgXAF8oStFb/+lOIupLV2v+o=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241 h1:Y+TeIabU8sJD10Qwd/zMty2/LEaT9GNDaA6nyZf+jgo=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122 h1:TQVQrsyNaimGwF7bIhzoVC9QkKm4KsWd8cECGzFx8gI=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2 h1:bu666BQci+y4S0tVRVjsHUeRon6vUXmsGBwdowgMrg4=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/AkPDU3AkqMxnMYL+imaqkpflHu73us8=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
|
||||
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0 h1:6TteTDQ68CjgcCe8wH3D3ZhUQQOJXMTbj/D9rkk2a1k=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 h1:4mkzGhKqt3JO1BWYjtD3iRFyAx4ow67hmSqOcGjuxqQ=
|
||||
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672/go.mod h1:YGGVbz9cOxyKFUmhW7LGaLZaMA0cPlHJinvAmVxEMSU=
|
||||
github.com/xtls/xray-core v1.5.2 h1:gaA3vHwXdfF5y8q++l/l94fgHzx5Yj2anonDG/16t3A=
|
||||
github.com/xtls/xray-core v1.5.2/go.mod h1:mqmSR72VHg0JUCnF8DWnAOTTrmnmrAYR8NWS+Cyg+F4=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0 h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.starlark.net v0.0.0-20211203141949-70c0e40ae128 h1:bxH+EXOo87zEOwKDdZ8Tevgi6irRbqheRm/fr293c58=
|
||||
go.starlark.net v0.0.0-20211203141949-70c0e40ae128/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5 h1:+hE86LblG4AyDgwMCLTE6FOlM9+qjHSYS+rKqxUVdsM=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d h1:E2M5QgjZ/Jg+ObCQAudsXxuTsLj7Nl5RV/lZcQZmKSo=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
|
||||
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU=
|
||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d h1:1oIt9o40TWWI9FUaveVpUvBe13FNqBNVXy3ue2fcfkw=
|
||||
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0 h1:K6z2u68e86TPdSdefXdzvXgR1zEMa+459vBSfWYAZkI=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM=
|
||||
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
|
||||
h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0 h1:eTiIR0CoWjGzJcnQ3OkhIl/b9GJovq4lSAVRt0ZFEG8=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
@@ -1,260 +0,0 @@
|
||||
package libv2ray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
||||
"github.com/2dust/AndroidLibV2rayLite/Process/Escort"
|
||||
"github.com/2dust/AndroidLibV2rayLite/VPN"
|
||||
mobasset "golang.org/x/mobile/asset"
|
||||
|
||||
v2core "github.com/xtls/xray-core/core"
|
||||
v2filesystem "github.com/xtls/xray-core/common/platform/filesystem"
|
||||
v2stats "github.com/xtls/xray-core/features/stats"
|
||||
v2serial "github.com/xtls/xray-core/infra/conf/serial"
|
||||
_ "github.com/xtls/xray-core/main/distro/all"
|
||||
v2internet "github.com/xtls/xray-core/transport/internet"
|
||||
|
||||
v2applog "github.com/xtls/xray-core/app/log"
|
||||
v2commlog "github.com/xtls/xray-core/common/log"
|
||||
)
|
||||
|
||||
const (
|
||||
v2Assert = "xray.location.asset"
|
||||
assetperfix = "/dev/libv2rayfs0/asset"
|
||||
)
|
||||
|
||||
/*V2RayPoint V2Ray Point Server
|
||||
This is territory of Go, so no getter and setters!
|
||||
*/
|
||||
type V2RayPoint struct {
|
||||
SupportSet V2RayVPNServiceSupportsSet
|
||||
statsManager v2stats.Manager
|
||||
|
||||
dialer *VPN.ProtectedDialer
|
||||
status *CoreI.Status
|
||||
escorter *Escort.Escorting
|
||||
v2rayOP *sync.Mutex
|
||||
closeChan chan struct{}
|
||||
|
||||
PackageName string
|
||||
PackageCodePath string
|
||||
DomainName string
|
||||
ConfigureFileContent string
|
||||
EnableLocalDNS bool
|
||||
ForwardIpv6 bool
|
||||
ProxyOnly bool
|
||||
}
|
||||
|
||||
/*V2RayVPNServiceSupportsSet To support Android VPN mode*/
|
||||
type V2RayVPNServiceSupportsSet interface {
|
||||
Setup(Conf string) int
|
||||
Prepare() int
|
||||
Shutdown() int
|
||||
Protect(int) int
|
||||
OnEmitStatus(int, string) int
|
||||
}
|
||||
|
||||
/*RunLoop Run V2Ray main loop
|
||||
*/
|
||||
func (v *V2RayPoint) RunLoop() (err error) {
|
||||
v.v2rayOP.Lock()
|
||||
defer v.v2rayOP.Unlock()
|
||||
//Construct Context
|
||||
v.status.PackageName = v.PackageName
|
||||
v.status.PackageCodePath = v.PackageCodePath
|
||||
|
||||
if !v.status.IsRunning {
|
||||
v.closeChan = make(chan struct{})
|
||||
v.dialer.PrepareResolveChan()
|
||||
go v.dialer.PrepareDomain(v.DomainName, v.closeChan)
|
||||
go func() {
|
||||
select {
|
||||
// wait until resolved
|
||||
case <-v.dialer.ResolveChan():
|
||||
// shutdown VPNService if server name can not reolved
|
||||
if !v.dialer.IsVServerReady() {
|
||||
log.Println("vServer cannot resolved, shutdown")
|
||||
v.StopLoop()
|
||||
}
|
||||
|
||||
// stop waiting if manually closed
|
||||
case <-v.closeChan:
|
||||
}
|
||||
}()
|
||||
|
||||
err = v.pointloop()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*StopLoop Stop V2Ray main loop
|
||||
*/
|
||||
func (v *V2RayPoint) StopLoop() (err error) {
|
||||
v.v2rayOP.Lock()
|
||||
defer v.v2rayOP.Unlock()
|
||||
if v.status.IsRunning {
|
||||
v.shutdownInit()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Delegate Funcation
|
||||
func (v *V2RayPoint) GetIsRunning() bool {
|
||||
return v.status.IsRunning
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) GetIsTRunning() bool {
|
||||
return v.status.IsTRunning
|
||||
}
|
||||
|
||||
//Delegate Funcation
|
||||
func (v V2RayPoint) QueryStats(tag string, direct string) int64 {
|
||||
if v.statsManager == nil {
|
||||
return 0
|
||||
}
|
||||
counter := v.statsManager.GetCounter(fmt.Sprintf("outbound>>>%s>>>traffic>>>%s", tag, direct))
|
||||
if counter == nil {
|
||||
return 0
|
||||
}
|
||||
return counter.Set(0)
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) shutdownInit() {
|
||||
close(v.closeChan)
|
||||
v.statsManager = nil
|
||||
v.status.Vpoint.Close()
|
||||
v.status.Vpoint = nil
|
||||
v.status.IsRunning = false
|
||||
|
||||
v.escorter.EscortingDown()
|
||||
|
||||
v.SupportSet.Shutdown()
|
||||
v.SupportSet.OnEmitStatus(0, "Closed")
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) pointloop() error {
|
||||
log.Println("loading v2ray config")
|
||||
config, err := v2serial.LoadJSONConfig(strings.NewReader(v.ConfigureFileContent))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("new v2ray core")
|
||||
inst, err := v2core.New(config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
v.status.Vpoint = inst
|
||||
v.statsManager = inst.GetFeature(v2stats.ManagerType()).(v2stats.Manager)
|
||||
|
||||
log.Println("start v2ray core")
|
||||
v.status.IsRunning = true
|
||||
if err := v.status.Vpoint.Start(); err != nil {
|
||||
v.status.IsRunning = false
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
v.SupportSet.Prepare()
|
||||
v.SupportSet.Setup(v.status.GetVPNSetupArg(v.EnableLocalDNS, v.ForwardIpv6))
|
||||
v.SupportSet.OnEmitStatus(0, "Running")
|
||||
|
||||
v.status.IsTRunning = false
|
||||
if !v.ProxyOnly {
|
||||
if err := v.runTun2socks(); err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
v.status.IsTRunning = true
|
||||
|
||||
log.Printf("EnableLocalDNS: %v\nForwardIpv6: %v\nDomainName: %s",
|
||||
v.EnableLocalDNS,
|
||||
v.ForwardIpv6,
|
||||
v.DomainName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initV2Env() {
|
||||
if os.Getenv(v2Assert) != "" {
|
||||
return
|
||||
}
|
||||
//Initialize asset API, Since Raymond Will not let notify the asset location inside Process,
|
||||
//We need to set location outside V2Ray
|
||||
os.Setenv(v2Assert, assetperfix)
|
||||
//Now we handle read
|
||||
v2filesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
|
||||
if strings.HasPrefix(path, assetperfix) {
|
||||
p := path[len(assetperfix)+1:]
|
||||
//is it overridden?
|
||||
//by, ok := overridedAssets[p]
|
||||
//if ok {
|
||||
// return os.Open(by)
|
||||
//}
|
||||
return mobasset.Open(p)
|
||||
}
|
||||
return os.Open(path)
|
||||
}
|
||||
}
|
||||
|
||||
//Delegate Funcation
|
||||
func TestConfig(ConfigureFileContent string) error {
|
||||
initV2Env()
|
||||
_, err := v2serial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
||||
return err
|
||||
}
|
||||
|
||||
/*NewV2RayPoint new V2RayPoint*/
|
||||
func NewV2RayPoint(s V2RayVPNServiceSupportsSet) *V2RayPoint {
|
||||
initV2Env()
|
||||
|
||||
// inject our own log writer
|
||||
v2applog.RegisterHandlerCreator(v2applog.LogType_Console,
|
||||
func(lt v2applog.LogType,
|
||||
options v2applog.HandlerCreatorOptions) (v2commlog.Handler, error) {
|
||||
return v2commlog.NewLogger(createStdoutLogWriter()), nil
|
||||
})
|
||||
|
||||
dialer := VPN.NewPreotectedDialer(s)
|
||||
v2internet.UseAlternativeSystemDialer(dialer)
|
||||
status := &CoreI.Status{}
|
||||
return &V2RayPoint{
|
||||
SupportSet: s,
|
||||
v2rayOP: new(sync.Mutex),
|
||||
status: status,
|
||||
dialer: dialer,
|
||||
escorter: &Escort.Escorting{Status: status},
|
||||
}
|
||||
}
|
||||
|
||||
func (v V2RayPoint) runTun2socks() error {
|
||||
v.escorter.EscortingUp()
|
||||
go v.escorter.EscortRun(
|
||||
v.status.GetApp("libtun2socks.so"),
|
||||
v.status.GetTun2socksArgs(v.EnableLocalDNS, v.ForwardIpv6), "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*CheckVersion int
|
||||
This func will return libv2ray binding version.
|
||||
*/
|
||||
func CheckVersion() int {
|
||||
return CoreI.CheckVersion()
|
||||
}
|
||||
|
||||
/*CheckVersionX string
|
||||
This func will return libv2ray binding version and V2Ray version used.
|
||||
*/
|
||||
func CheckVersionX() string {
|
||||
return fmt.Sprintf("Libv2rayLite V%d, Core V%s", CheckVersion(), v2core.Version())
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package libv2ray
|
||||
|
||||
//go:generate make all
|
||||
@@ -1 +0,0 @@
|
||||
readme.txt
|
||||
@@ -1,128 +0,0 @@
|
||||
# Copyright (C) 2009 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
#
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
ROOT_PATH := $(LOCAL_PATH)
|
||||
|
||||
########################################################
|
||||
## libancillary
|
||||
########################################################
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
ANCILLARY_SOURCE := fd_recv.c fd_send.c
|
||||
|
||||
LOCAL_MODULE := libancillary
|
||||
#LOCAL_CFLAGS += -I$(LOCAL_PATH)/libancillary
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/libancillary
|
||||
LOCAL_SRC_FILES := $(addprefix libancillary/, $(ANCILLARY_SOURCE))
|
||||
|
||||
include $(BUILD_STATIC_LIBRARY)
|
||||
|
||||
########################################################
|
||||
## tun2socks
|
||||
########################################################
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_CFLAGS := -std=gnu99
|
||||
LOCAL_CFLAGS += -DBADVPN_THREADWORK_USE_PTHREAD -DBADVPN_LINUX -DBADVPN_BREACTOR_BADVPN -D_GNU_SOURCE
|
||||
LOCAL_CFLAGS += -DBADVPN_USE_SIGNALFD -DBADVPN_USE_EPOLL
|
||||
LOCAL_CFLAGS += -DBADVPN_LITTLE_ENDIAN -DBADVPN_THREAD_SAFE
|
||||
LOCAL_CFLAGS += -DNDEBUG -DANDROID
|
||||
LOCAL_CFLAGS += -I
|
||||
|
||||
LOCAL_STATIC_LIBRARIES := libancillary
|
||||
|
||||
LOCAL_C_INCLUDES := \
|
||||
$(LOCAL_PATH)/badvpn/libancillary \
|
||||
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv4 \
|
||||
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv6 \
|
||||
$(LOCAL_PATH)/badvpn/lwip/src/include \
|
||||
$(LOCAL_PATH)/badvpn/lwip/custom \
|
||||
$(LOCAL_PATH)/badvpn \
|
||||
$(LOCAL_PATH)/libancillary
|
||||
|
||||
TUN2SOCKS_SOURCES := \
|
||||
base/BLog_syslog.c \
|
||||
system/BReactor_badvpn.c \
|
||||
system/BSignal.c \
|
||||
system/BConnection_common.c \
|
||||
system/BConnection_unix.c \
|
||||
system/BTime.c \
|
||||
system/BUnixSignal.c \
|
||||
system/BNetwork.c \
|
||||
flow/StreamRecvInterface.c \
|
||||
flow/PacketRecvInterface.c \
|
||||
flow/PacketPassInterface.c \
|
||||
flow/StreamPassInterface.c \
|
||||
flow/SinglePacketBuffer.c \
|
||||
flow/BufferWriter.c \
|
||||
flow/PacketBuffer.c \
|
||||
flow/PacketStreamSender.c \
|
||||
flow/PacketPassConnector.c \
|
||||
flow/PacketProtoFlow.c \
|
||||
flow/PacketPassFairQueue.c \
|
||||
flow/PacketProtoEncoder.c \
|
||||
flow/PacketProtoDecoder.c \
|
||||
socksclient/BSocksClient.c \
|
||||
tuntap/BTap.c \
|
||||
lwip/src/core/udp.c \
|
||||
lwip/src/core/memp.c \
|
||||
lwip/src/core/init.c \
|
||||
lwip/src/core/pbuf.c \
|
||||
lwip/src/core/tcp.c \
|
||||
lwip/src/core/tcp_out.c \
|
||||
lwip/src/core/netif.c \
|
||||
lwip/src/core/def.c \
|
||||
lwip/src/core/ip.c \
|
||||
lwip/src/core/mem.c \
|
||||
lwip/src/core/tcp_in.c \
|
||||
lwip/src/core/stats.c \
|
||||
lwip/src/core/inet_chksum.c \
|
||||
lwip/src/core/timeouts.c \
|
||||
lwip/src/core/ipv4/icmp.c \
|
||||
lwip/src/core/ipv4/igmp.c \
|
||||
lwip/src/core/ipv4/ip4_addr.c \
|
||||
lwip/src/core/ipv4/ip4_frag.c \
|
||||
lwip/src/core/ipv4/ip4.c \
|
||||
lwip/src/core/ipv4/autoip.c \
|
||||
lwip/src/core/ipv6/ethip6.c \
|
||||
lwip/src/core/ipv6/inet6.c \
|
||||
lwip/src/core/ipv6/ip6_addr.c \
|
||||
lwip/src/core/ipv6/mld6.c \
|
||||
lwip/src/core/ipv6/dhcp6.c \
|
||||
lwip/src/core/ipv6/icmp6.c \
|
||||
lwip/src/core/ipv6/ip6.c \
|
||||
lwip/src/core/ipv6/ip6_frag.c \
|
||||
lwip/src/core/ipv6/nd6.c \
|
||||
lwip/custom/sys.c \
|
||||
tun2socks/tun2socks.c \
|
||||
base/DebugObject.c \
|
||||
base/BLog.c \
|
||||
base/BPending.c \
|
||||
system/BDatagram_unix.c \
|
||||
flowextra/PacketPassInactivityMonitor.c \
|
||||
tun2socks/SocksUdpGwClient.c \
|
||||
udpgw_client/UdpGwClient.c
|
||||
|
||||
LOCAL_MODULE := tun2socks
|
||||
|
||||
LOCAL_LDLIBS := -ldl -llog
|
||||
|
||||
LOCAL_SRC_FILES := $(addprefix badvpn/, $(TUN2SOCKS_SOURCES))
|
||||
|
||||
include $(BUILD_SYSTEM)/build-executable.mk
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Thanks to https://gist.github.com/wenzhixin/43cf3ce909c24948c6e7
|
||||
# Execute this script in your home directory. Lines 17 and 21 will prompt you for a y/n
|
||||
|
||||
# Install Oracle JDK 8
|
||||
apt-get update
|
||||
apt-get install -y openjdk-8-jdk
|
||||
apt-get install -y unzip make expect # NDK stuff
|
||||
|
||||
# Get SDK tools (link from https://developer.android.com/studio/index.html#downloads)
|
||||
wget -q https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
|
||||
mkdir android-sdk-linux
|
||||
unzip sdk*.zip -d android-sdk-linux
|
||||
|
||||
# Get NDK (https://developer.android.com/ndk/downloads/index.html)
|
||||
# wget -q https://dl.google.com/android/repository/android-ndk-r15c-linux-x86_64.zip
|
||||
# unzip android-ndk*.zip >> /dev/null
|
||||
|
||||
ACCEPT_LICENSES_URL=https://gist.githubusercontent.com/xiaokangwang/1489fd223d26581bfec92adb3cb0088e/raw/328eb6925099df5aae3e76790f8232f0fc378f8b/accept-licenses
|
||||
|
||||
ACCEPT_LICENSES_ITEM="android-sdk-license-bcbbd656|intel-android-sysimage-license-1ea702d1|android-sdk-license-2742d1c5"
|
||||
|
||||
# Let it update itself and install some stuff
|
||||
cd android-sdk-linux/tools
|
||||
|
||||
curl -L -o accept-licenses $ACCEPT_LICENSES_URL
|
||||
|
||||
chmod +x accept-licenses
|
||||
|
||||
./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui" $ACCEPT_LICENSES_ITEM >/dev/null
|
||||
|
||||
# Download every build-tools version that has ever existed
|
||||
# This will save you time! Thank me later for this
|
||||
|
||||
#./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui --filter 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27" $ACCEPT_LICENSES_ITEM
|
||||
|
||||
PACKAGE_PARSE_URL=https://gist.githubusercontent.com/xiaokangwang/06268fb23034ed94bc301880e862da09/raw/afd95cbbe2f8c1d9e7b0277b7c5ef39af756a6ee/parse.awk
|
||||
|
||||
reduceout=https://gist.githubusercontent.com/xiaokangwang/4684bdb5c3415b943f52aa4803386480/raw/b46dab1cc60f02c0d87f88f01e27157034218faa/out.awk
|
||||
|
||||
cd bin
|
||||
|
||||
curl -L -o parse.awk $PACKAGE_PARSE_URL
|
||||
|
||||
curl -L -o reduce.awk $reduceout
|
||||
|
||||
sudo apt-get install gawk
|
||||
|
||||
./sdkmanager --verbose --list |awk -f parse.awk > ~/package_to_install
|
||||
|
||||
readarray -t filenames < $HOME/package_to_install
|
||||
|
||||
cat $HOME/package_to_install
|
||||
|
||||
yes|./sdkmanager --verbose "${filenames[@]}" |awk -f reduce.awk
|
||||
|
||||
# If you need additional packages for your app, check available packages with:
|
||||
# ./android list sdk --all
|
||||
|
||||
# install certain packages with:
|
||||
# ./android update sdk --no-ui --all --filter 1,2,3,<...>,N
|
||||
# where N is the number of the package in the list (see previous command)
|
||||
|
||||
./sdkmanager "ndk-bundle"
|
||||
|
||||
# Add the directory containing executables in PATH so that they can be found
|
||||
echo 'export ANDROID_HOME=$HOME/android-sdk-linux' >> ~/.bashrc
|
||||
echo 'export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools' >> ~/.bashrc
|
||||
# echo 'export NDK_HOME=$HOME/android-ndk-r15c' >> ~/.bashrc
|
||||
# echo 'export ANDROID_NDK_HOME=$NDK_HOME' >> ~/.bashrc
|
||||
|
||||
|
||||
source ~/.bashrc
|
||||
|
||||
# Make sure you can execute 32 bit executables if this is 64 bit machine, otherwise skip this
|
||||
dpkg --add-architecture i386
|
||||
apt-get update
|
||||
apt-get install -y libc6:i386 libstdc++6:i386 zlib1g:i386
|
||||
@@ -1,32 +0,0 @@
|
||||
package libv2ray
|
||||
|
||||
// This struct creates our own log writer without datatime stamp
|
||||
// As Android adds time stamps on each line
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
v2commlog "github.com/xtls/xray-core/common/log"
|
||||
)
|
||||
|
||||
type consoleLogWriter struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func (w *consoleLogWriter) Write(s string) error {
|
||||
w.logger.Print(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *consoleLogWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This logger won't print data/time stamps
|
||||
func createStdoutLogWriter() v2commlog.WriterCreator {
|
||||
return func() v2commlog.Writer {
|
||||
return &consoleLogWriter{
|
||||
logger: log.New(os.Stdout, "", 0)}
|
||||
}
|
||||
}
|
||||
@@ -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 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||
|
||||
[](https://developer.android.com/about/versions/lollipop)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||
[](https://github.com/2dust/v2rayNG/releases)
|
||||
@@ -13,6 +13,9 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
|
||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
||||
</a>
|
||||
|
||||
### Telegram Channel
|
||||
[github_2dust](https://t.me/github_2dust)
|
||||
|
||||
### Usage
|
||||
|
||||
#### Geoip and Geosite
|
||||
@@ -26,7 +29,8 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
|
||||
### Development guide
|
||||
|
||||
Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
|
||||
The aar can be compiled from the Golang project under AndroidLibV2rayLite folder. For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile)
|
||||
and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
|
||||
The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
|
||||
For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
|
||||
|
||||
v2rayNG can run on Android Emulators. But VPN mode will not work if system don't support it, like Windows 11 WSA
|
||||
v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via
|
||||
`appops set [package name] ACTIVATE_VPN allow`
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
/captures
|
||||
*.apk
|
||||
signing.properties
|
||||
*.aar
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
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 212
|
||||
versionName "1.0.2"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
zipAlignEnabled false
|
||||
shrinkResources false
|
||||
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
|
||||
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.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.3'
|
||||
implementation "androidx.legacy:legacy-support-v4:1.0.0"
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "com.google.android.material:material:1.5.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.preference:preference:1.0.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
|
||||
// Androidx ktx
|
||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.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.6'
|
||||
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' }
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
|
||||
}
|
||||
}
|
||||
@@ -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 = 580
|
||||
versionName = "1.8.36"
|
||||
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(libs.junit)
|
||||
|
||||
implementation(libs.flexbox)
|
||||
// Androidx
|
||||
implementation(libs.constraintlayout)
|
||||
implementation(libs.legacy.support.v4)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.cardview)
|
||||
implementation(libs.preference.ktx)
|
||||
implementation(libs.recyclerview)
|
||||
implementation(libs.fragment.ktx)
|
||||
implementation(libs.multidex)
|
||||
implementation(libs.viewpager2)
|
||||
|
||||
// Androidx ktx
|
||||
implementation(libs.activity.ktx)
|
||||
implementation(libs.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.lifecycle.livedata.ktx)
|
||||
implementation(libs.lifecycle.runtime.ktx)
|
||||
|
||||
//kotlin
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
implementation(libs.mmkv.static)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.rxandroid)
|
||||
implementation(libs.rxpermissions)
|
||||
implementation(libs.toastcompat)
|
||||
implementation(libs.editorkit)
|
||||
implementation(libs.language.base)
|
||||
implementation(libs.language.json)
|
||||
implementation(libs.quickie.bundled)
|
||||
implementation(libs.core)
|
||||
// Updating these 2 dependencies may cause some errors. Be careful.
|
||||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.work.multiprocess)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite
|
||||
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Vendored
-58
@@ -1,58 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in G:\android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
|
||||
-dontwarn org.apache.commons.**
|
||||
-keep class org.apache.commons.** { *;}
|
||||
|
||||
# Disable debug info output
|
||||
-assumenosideeffects class android.util.Log {
|
||||
public static boolean isLoggable(java.lang.String,int);
|
||||
public static int v(...);
|
||||
public static int i(...);
|
||||
public static int w(...);
|
||||
public static int d(...);
|
||||
public static int e(...);
|
||||
}
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
||||
static void throwUninitializedPropertyAccessException(java.lang.String);
|
||||
}
|
||||
|
||||
-dontwarn rx.internal.util.unsafe.**
|
||||
-keep class rx.internal.util.unsafe.** { *;}
|
||||
|
||||
-dontwarn app.dinus.**
|
||||
-keep class app.dinus.** { *;}
|
||||
|
||||
-keepclassmembers class ** {
|
||||
@com.hwangjr.rxbus.annotation.Subscribe public *;
|
||||
@com.hwangjr.rxbus.annotation.Produce public *;
|
||||
}
|
||||
|
||||
-keep class libv2ray.** { *;}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="app_name" type="string">v2rayNG (DEV)</item>
|
||||
</resources>
|
||||
@@ -1,55 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.v2ray.ang">
|
||||
tools:ignore="MissingLeanbackLauncher">
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:smallScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
android:anyDensity="true"
|
||||
android:smallScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="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.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 -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<!-- <uses-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="com.android.vending.BILLING" />
|
||||
<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.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
||||
|
||||
<application
|
||||
android:name=".AngApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/AppThemeDayNight"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="m">
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:launchMode="singleTask">
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppThemeDayNight.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
@@ -85,6 +90,13 @@
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.SubSettingActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.UserAssetActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.UserAssetUrlActivity" />
|
||||
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.SubEditActivity" />
|
||||
@@ -98,12 +110,34 @@
|
||||
android:process=":RunSoLibV2RayDaemon"
|
||||
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.UrlSchemeActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="v2rayng"/>
|
||||
<data android:host="install-config"/>
|
||||
<data android:host="install-sub"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.AboutActivity" />
|
||||
|
||||
<service
|
||||
android:name=".service.V2RayVpnService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
@@ -111,16 +145,28 @@
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true" />
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
|
||||
<service android:name=".service.V2RayProxyOnlyService"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service android:name=".service.V2RayTestService"
|
||||
android:exported="false"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:name=".receiver.WidgetProvider"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<meta-data
|
||||
@@ -136,13 +182,17 @@
|
||||
<service
|
||||
android:exported="true"
|
||||
android:name=".service.QSTileService"
|
||||
android:icon="@drawable/ic_v"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:label="@string/app_tile_name"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="tile" />
|
||||
</service>
|
||||
<!-- =====================Tasker===================== -->
|
||||
<activity
|
||||
@@ -164,6 +214,28 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- =====================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>
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
|
||||
* This service provides the following features:
|
||||
* 1. Provides a new API to get details of in-app items published for the app including
|
||||
* price, type, title and description.
|
||||
* 2. The purchase flow is synchronous and purchase information is available immediately
|
||||
* after it completes.
|
||||
* 3. Purchase information of in-app purchases is maintained within the Google Play system
|
||||
* till the purchase is consumed.
|
||||
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
|
||||
* in-app items are consumable and thereafter can be purchased again.
|
||||
* 5. An API to get current purchases of the user immediately. This will not contain any
|
||||
* consumed purchases.
|
||||
*
|
||||
* All calls will give a response code with the following possible values
|
||||
* RESULT_OK = 0 - success
|
||||
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
|
||||
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
|
||||
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
|
||||
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
|
||||
* RESULT_ERROR = 6 - Fatal error during the API action
|
||||
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
|
||||
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
|
||||
*/
|
||||
interface IInAppBillingService {
|
||||
/**
|
||||
* Checks support for the requested billing API version, package and in-app type.
|
||||
* Minimum API version supported by this interface is 3.
|
||||
* @param apiVersion the billing version which the app is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @param type type of the in-app item being purchased "inapp" for one-time purchases
|
||||
* and "subs" for subscription.
|
||||
* @return RESULT_OK(0) on success, corresponding result code on failures
|
||||
*/
|
||||
int isBillingSupported(int apiVersion, String packageName, String type);
|
||||
|
||||
/**
|
||||
* Provides details of a list of SKUs
|
||||
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
|
||||
* with a list JSON strings containing the productId, price, title and description.
|
||||
* This API can be called with a maximum of 20 SKUs.
|
||||
* @param apiVersion billing API version that the Third-party is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
||||
* in JSON format similar to:
|
||||
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
|
||||
* "title : "Example Title", "description" : "This is an example description" }'
|
||||
*/
|
||||
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
|
||||
|
||||
/**
|
||||
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
|
||||
* the type, a unique purchase token and an optional developer payload.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param sku the SKU of the in-app item as published in the developer console
|
||||
* @param type the type of the in-app item ("inapp" for one-time purchases
|
||||
* and "subs" for subscription).
|
||||
* @param developerPayload optional argument to be sent back with the purchase information
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
||||
*
|
||||
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
||||
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
||||
* If the purchase is successful, the result data will contain the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
||||
* '{"orderId":"12999763169054705758.1371079406387615",
|
||||
* "packageName":"com.example.app",
|
||||
* "productId":"exampleSku",
|
||||
* "purchaseTime":1345678900000,
|
||||
* "purchaseToken" : "122333444455555",
|
||||
* "developerPayload":"example developer payload" }'
|
||||
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
||||
* was signed with the private key of the developer
|
||||
* TODO: change this to app-specific keys.
|
||||
*/
|
||||
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
|
||||
String developerPayload);
|
||||
|
||||
/**
|
||||
* Returns the current SKUs owned by the user of the type and package name specified along with
|
||||
* purchase information and a signature of the data to be validated.
|
||||
* This will return all SKUs that have been purchased in V3 and managed items purchased using
|
||||
* V1 and V2 that have not been consumed.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param type the type of the in-app items being requested
|
||||
* ("inapp" for one-time purchases and "subs" for subscription).
|
||||
* @param continuationToken to be set as null for the first call, if the number of owned
|
||||
* skus are too many, a continuationToken is returned in the response bundle.
|
||||
* This method can be called again with the continuation token to get the next set of
|
||||
* owned skus.
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
|
||||
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
|
||||
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
|
||||
* of the purchase information
|
||||
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
|
||||
* next set of in-app purchases. Only set if the
|
||||
* user has more owned skus than the current list.
|
||||
*/
|
||||
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
|
||||
|
||||
/**
|
||||
* Consume the last purchase of the given SKU. This will result in this item being removed
|
||||
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param purchaseToken token in the purchase information JSON that identifies the purchase
|
||||
* to be consumed
|
||||
* @return 0 if consumption succeeded. Appropriate error values for failures.
|
||||
*/
|
||||
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
geosite:category-ads-all,
|
||||
@@ -0,0 +1 @@
|
||||
geosite:cn
|
||||
@@ -0,0 +1 @@
|
||||
geosite:geolocation-!cn
|
||||
@@ -54,6 +54,7 @@
|
||||
"users": [
|
||||
{
|
||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||
"alterId": 0,
|
||||
"security": "auto",
|
||||
"level": 8
|
||||
}
|
||||
@@ -80,7 +81,9 @@
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {},
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,116 +0,0 @@
|
||||
package com.v2ray.ang.util;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.DecodeHintType;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.RGBLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.common.GlobalHistogramBinarizer;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 描述:解析二维码图片
|
||||
*/
|
||||
public class QRCodeDecoder {
|
||||
public static final Map<DecodeHintType, Object> HINTS = new EnumMap<>(DecodeHintType.class);
|
||||
|
||||
static {
|
||||
List<BarcodeFormat> allFormats = new ArrayList<>();
|
||||
allFormats.add(BarcodeFormat.AZTEC);
|
||||
allFormats.add(BarcodeFormat.CODABAR);
|
||||
allFormats.add(BarcodeFormat.CODE_39);
|
||||
allFormats.add(BarcodeFormat.CODE_93);
|
||||
allFormats.add(BarcodeFormat.CODE_128);
|
||||
allFormats.add(BarcodeFormat.DATA_MATRIX);
|
||||
allFormats.add(BarcodeFormat.EAN_8);
|
||||
allFormats.add(BarcodeFormat.EAN_13);
|
||||
allFormats.add(BarcodeFormat.ITF);
|
||||
allFormats.add(BarcodeFormat.MAXICODE);
|
||||
allFormats.add(BarcodeFormat.PDF_417);
|
||||
allFormats.add(BarcodeFormat.QR_CODE);
|
||||
allFormats.add(BarcodeFormat.RSS_14);
|
||||
allFormats.add(BarcodeFormat.RSS_EXPANDED);
|
||||
allFormats.add(BarcodeFormat.UPC_A);
|
||||
allFormats.add(BarcodeFormat.UPC_E);
|
||||
allFormats.add(BarcodeFormat.UPC_EAN_EXTENSION);
|
||||
HINTS.put(DecodeHintType.TRY_HARDER, BarcodeFormat.QR_CODE);
|
||||
HINTS.put(DecodeHintType.POSSIBLE_FORMATS, allFormats);
|
||||
HINTS.put(DecodeHintType.CHARACTER_SET, "utf-8");
|
||||
}
|
||||
|
||||
private QRCodeDecoder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
||||
*
|
||||
* @param picturePath 要解析的二维码图片本地路径
|
||||
* @return 返回二维码图片里的内容 或 null
|
||||
*/
|
||||
public static String syncDecodeQRCode(String picturePath) {
|
||||
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
|
||||
*
|
||||
* @param bitmap 要解析的二维码图片
|
||||
* @return 返回二维码图片里的内容 或 null
|
||||
*/
|
||||
public static String syncDecodeQRCode(Bitmap bitmap) {
|
||||
Result result = null;
|
||||
RGBLuminanceSource source = null;
|
||||
try {
|
||||
int width = bitmap.getWidth();
|
||||
int height = bitmap.getHeight();
|
||||
int[] pixels = new int[width * height];
|
||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
|
||||
source = new RGBLuminanceSource(width, height, pixels);
|
||||
result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), HINTS);
|
||||
return result.getText();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
if (source != null) {
|
||||
try {
|
||||
result = new MultiFormatReader().decode(new BinaryBitmap(new GlobalHistogramBinarizer(source)), HINTS);
|
||||
return result.getText();
|
||||
} catch (Throwable e2) {
|
||||
e2.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
|
||||
*
|
||||
* @param picturePath 本地图片文件路径
|
||||
* @return
|
||||
*/
|
||||
private static Bitmap getDecodeAbleBitmap(String picturePath) {
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(picturePath, options);
|
||||
int sampleSize = options.outHeight / 400;
|
||||
if (sampleSize <= 0) {
|
||||
sampleSize = 1;
|
||||
}
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inJustDecodeBounds = false;
|
||||
return BitmapFactory.decodeFile(picturePath, options);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,44 @@
|
||||
package com.v2ray.ang
|
||||
|
||||
import android.content.Context
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.Configuration
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
class AngApplication : MultiDexApplication() {
|
||||
class AngApplication : MultiDexApplication(), Configuration.Provider {
|
||||
companion object {
|
||||
const val PREF_LAST_VERSION = "pref_last_version"
|
||||
//const val PREF_LAST_VERSION = "pref_last_version"
|
||||
lateinit var application: AngApplication
|
||||
}
|
||||
|
||||
var curIndex = -1 //Current proxy that is opened. (Used to implement restart feature)
|
||||
var firstRun = false
|
||||
private set
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
application = this
|
||||
}
|
||||
|
||||
//var firstRun = false
|
||||
// private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// LeakCanary.install(this)
|
||||
|
||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
||||
if (firstRun)
|
||||
defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
||||
// if (firstRun)
|
||||
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||
|
||||
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
|
||||
MMKV.initialize(this)
|
||||
|
||||
Utils.setNightMode(application)
|
||||
}
|
||||
|
||||
override fun getWorkManagerConfiguration(): Configuration {
|
||||
return Configuration.Builder()
|
||||
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,124 @@
|
||||
package com.v2ray.ang
|
||||
|
||||
/**
|
||||
*
|
||||
* App Config Const
|
||||
*/
|
||||
object AppConfig {
|
||||
const val ANG_PACKAGE = "com.v2ray.ang"
|
||||
|
||||
// legacy
|
||||
object AppConfig {
|
||||
|
||||
/** The application's package name. */
|
||||
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
|
||||
|
||||
/** Directory names used in the app's file system. */
|
||||
const val DIR_ASSETS = "assets"
|
||||
const val DIR_BACKUPS = "backups"
|
||||
|
||||
/** Legacy configuration keys. */
|
||||
const val ANG_CONFIG = "ang_config"
|
||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
||||
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
||||
|
||||
// Preferences mapped to MMKV
|
||||
const val PREF_MODE = "pref_mode"
|
||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||
/** Preferences mapped to MMKV storage. */
|
||||
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_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_FORWARD_IPV6 = "pref_forward_ipv6"
|
||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||
const val PREF_ROUTING_MODE = "pref_routing_mode"
|
||||
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_BLOCKED = "pref_v2ray_routing_blocked"
|
||||
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_BYPASS_MAINLAND = "pref_bypass_mainland"
|
||||
// const val PREF_START_ON_BOOT = "pref_start_on_boot"
|
||||
// const val PREF_MUX_ENAimport libv2ray.Libv2rayBLED = "pref_mux_enabled"
|
||||
// const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||
// const val PREF_HTTP_PORT = "pref_http_port"
|
||||
// const val PREF_DONATE = "pref_donate"
|
||||
// const val PREF_LICENSES = "pref_licenses"
|
||||
// const val PREF_FEEDBACK = "pref_feedback"
|
||||
// const val PREF_TG_GROUP = "pref_tg_group"
|
||||
// const val PREF_AUTO_RESTART = "pref_auto_restart"
|
||||
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
||||
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
||||
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
|
||||
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
|
||||
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" // Default is 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 HTTP_PROTOCOL: String = "http://"
|
||||
const val HTTPS_PROTOCOL: String = "https://"
|
||||
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"
|
||||
|
||||
/** Cache keys. */
|
||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
||||
|
||||
/** Protocol identifiers. */
|
||||
const val PROTOCOL_HTTP: String = "http://"
|
||||
const val PROTOCOL_HTTPS: String = "https://"
|
||||
const val PROTOCOL_FREEDOM: String = "freedom"
|
||||
|
||||
/** Broadcast actions. */
|
||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
||||
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
|
||||
|
||||
/** Tasker extras. */
|
||||
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
||||
const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
|
||||
const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
|
||||
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
||||
const val TASKER_DEFAULT_GUID = "Default"
|
||||
|
||||
const val TAG_AGENT = "proxy"
|
||||
/** Tags for different proxy modes. */
|
||||
const val TAG_PROXY = "proxy"
|
||||
const val TAG_DIRECT = "direct"
|
||||
const val TAG_BLOCKED = "block"
|
||||
const val TAG_FRAGMENT = "fragment"
|
||||
|
||||
const val androidpackagenamelistUrl = "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
||||
const val v2rayCustomRoutingListUrl = "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
||||
const val v2rayNGIssues = "https://github.com/2dust/v2rayNG/issues"
|
||||
const val v2rayNGWikiMode = "https://github.com/2dust/v2rayNG/wiki/Mode"
|
||||
const val promotionUrl = "https://1.2345345.xyz/ads.html"
|
||||
/** Network-related constants. */
|
||||
const val UPLINK = "uplink"
|
||||
const val DOWNLINK = "downlink"
|
||||
|
||||
const val DNS_AGENT = "1.1.1.1"
|
||||
/** URLs for various resources. */
|
||||
const val androidpackagenamelistUrl =
|
||||
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
||||
const val v2rayCustomRoutingListUrl =
|
||||
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
||||
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
|
||||
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"
|
||||
|
||||
/** DNS server addresses. */
|
||||
const val DNS_PROXY = "1.1.1.1"
|
||||
const val DNS_DIRECT = "223.5.5.5"
|
||||
const val DNS_VPN = "1.1.1.1"
|
||||
|
||||
/** Ports and addresses for various services. */
|
||||
const val PORT_LOCAL_DNS = "10853"
|
||||
const val PORT_SOCKS = "10808"
|
||||
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"
|
||||
|
||||
/** Message constants for communication. */
|
||||
const val MSG_REGISTER_CLIENT = 1
|
||||
const val MSG_STATE_RUNNING = 11
|
||||
const val MSG_STATE_NOT_RUNNING = 12
|
||||
@@ -79,4 +129,15 @@ object AppConfig {
|
||||
const val MSG_STATE_STOP = 4
|
||||
const val MSG_STATE_STOP_SUCCESS = 41
|
||||
const val MSG_STATE_RESTART = 5
|
||||
const val MSG_MEASURE_DELAY = 6
|
||||
const val MSG_MEASURE_DELAY_SUCCESS = 61
|
||||
const val MSG_MEASURE_CONFIG = 7
|
||||
const val MSG_MEASURE_CONFIG_SUCCESS = 71
|
||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
||||
|
||||
/** Notification channel IDs and names. */
|
||||
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
||||
const val RAY_NG_CHANNEL_NAME = "V2rayNG Background Service"
|
||||
const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
|
||||
const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
|
||||
}
|
||||
|
||||
@@ -1,28 +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 configType: Int = 1,
|
||||
var configVersion: Int = 1,
|
||||
var testResult: String = "",
|
||||
var subid: String = "")
|
||||
|
||||
data class SubItemBean(var id: String = "",
|
||||
var remarks: String = "",
|
||||
var url: String = "")
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -6,7 +6,8 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
|
||||
SHADOWSOCKS(3, "ss://"),
|
||||
SOCKS(4, "socks://"),
|
||||
VLESS(5, "vless://"),
|
||||
TROJAN(6, "trojan://");
|
||||
TROJAN(6, "trojan://"),
|
||||
WIREGUARD(7, "wireguard://");
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int) = values().firstOrNull { it.value == value }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
enum class ERoutingMode(val value: String ) {
|
||||
GLOBAL_PROXY("0"),
|
||||
BYPASS_LAN("1"),
|
||||
BYPASS_MAINLAND("2"),
|
||||
BYPASS_LAN_MAINLAND("3"),
|
||||
GLOBAL_DIRECT("4");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
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_DIRECT
|
||||
import com.v2ray.ang.util.Utils
|
||||
@@ -19,23 +19,32 @@ data class ServerConfig(
|
||||
when(configType) {
|
||||
EConfigType.VMESS, EConfigType.VLESS ->
|
||||
return ServerConfig(
|
||||
configType = configType,
|
||||
outboundBean = V2rayConfig.OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||
vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
||||
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
|
||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
||||
configType = configType,
|
||||
outboundBean = V2rayConfig.OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||
vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
||||
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
|
||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
||||
EConfigType.CUSTOM ->
|
||||
return ServerConfig(configType = configType)
|
||||
EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
|
||||
return ServerConfig(
|
||||
configType = configType,
|
||||
outboundBean = V2rayConfig.OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
|
||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
||||
configType = configType,
|
||||
outboundBean = V2rayConfig.OutboundBean(
|
||||
protocol = configType.name.lowercase(),
|
||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
|
||||
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> {
|
||||
if (configType != EConfigType.CUSTOM) {
|
||||
return mutableListOf(TAG_AGENT, TAG_DIRECT, TAG_BLOCKED)
|
||||
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
||||
}
|
||||
fullConfig?.let { config ->
|
||||
return config.outbounds.map { it.tag }.toMutableList()
|
||||
@@ -60,10 +69,6 @@ data class ServerConfig(
|
||||
fun getV2rayPointDomainAndPort(): String {
|
||||
val address = getProxyOutbound()?.getServerAddress().orEmpty()
|
||||
val port = getProxyOutbound()?.getServerPort()
|
||||
return if (Utils.isIpv6Address(address)) {
|
||||
String.format("[%s]:%s", address, port)
|
||||
} else {
|
||||
String.format("%s:%s", address, port)
|
||||
}
|
||||
return Utils.getIpv6Address(address) + ":" + port
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class ServersCache(val guid: String,
|
||||
val config: ServerConfig)
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
data class SubscriptionItem(
|
||||
var remarks: String = "",
|
||||
var url: String = "",
|
||||
var enabled: Boolean = true,
|
||||
val addedTime: Long = System.currentTimeMillis()) {
|
||||
}
|
||||
var remarks: String = "",
|
||||
var url: String = "",
|
||||
var enabled: Boolean = true,
|
||||
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
|
||||
|
||||
data class V2rayConfig(
|
||||
var remarks: String? = null,
|
||||
var stats: Any? = null,
|
||||
val log: LogBean,
|
||||
var policy: PolicyBean?,
|
||||
@@ -21,16 +22,17 @@ data class V2rayConfig(
|
||||
val transport: Any? = null,
|
||||
val reverse: Any? = null,
|
||||
var fakedns: Any? = null,
|
||||
val browserForwarder: Any? = null) {
|
||||
val browserForwarder: Any? = null,
|
||||
var observatory: Any? = null,
|
||||
var burstObservatory: Any? = null) {
|
||||
companion object {
|
||||
const val DEFAULT_PORT = 443
|
||||
const val DEFAULT_SECURITY = "auto"
|
||||
const val DEFAULT_LEVEL = 8
|
||||
const val DEFAULT_NETWORK = "tcp"
|
||||
const val DEFAULT_FLOW = "xtls-rprx-splice"
|
||||
|
||||
const val TLS = "tls"
|
||||
const val XTLS = "xtls"
|
||||
const val REALITY = "reality"
|
||||
const val HTTP = "http"
|
||||
}
|
||||
|
||||
@@ -58,43 +60,56 @@ data class V2rayConfig(
|
||||
|
||||
data class SniffingBean(var enabled: Boolean,
|
||||
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 settings: OutSettingsBean? = null,
|
||||
var streamSettings: StreamSettingsBean? = null,
|
||||
val proxySettings: Any? = null,
|
||||
val sendThrough: String? = null,
|
||||
val mux: MuxBean? = MuxBean(false)) {
|
||||
var mux: MuxBean? = MuxBean(false)) {
|
||||
|
||||
data class OutSettingsBean(var vnext: List<VnextBean>? = null,
|
||||
var fragment: FragmentBean? = null,
|
||||
var servers: List<ServersBean>? = null,
|
||||
/*Blackhole*/
|
||||
var response: Response? = null,
|
||||
/*DNS*/
|
||||
val network: String? = null,
|
||||
val address: String? = null,
|
||||
var address: Any? = null,
|
||||
val port: Int? = null,
|
||||
/*Freedom*/
|
||||
var domainStrategy: String? = null,
|
||||
val redirect: String? = null,
|
||||
val userLevel: Int? = null,
|
||||
/*Loopback*/
|
||||
val inboundTag: String? = null) {
|
||||
val inboundTag: String? = null,
|
||||
/*Wireguard*/
|
||||
var secretKey: String? = null,
|
||||
val peers: List<WireGuardBean>? = null,
|
||||
var reserved: List<Int>? = null,
|
||||
var mtu :Int? = null
|
||||
) {
|
||||
|
||||
data class VnextBean(var address: String = "",
|
||||
var port: Int = DEFAULT_PORT,
|
||||
var users: List<UsersBean>) {
|
||||
|
||||
data class UsersBean(var id: String = "",
|
||||
var alterId: Int? = null,
|
||||
var security: String = DEFAULT_SECURITY,
|
||||
var level: Int = DEFAULT_LEVEL,
|
||||
var encryption: 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 = "",
|
||||
var method: String = "chacha20-poly1305",
|
||||
var ota: Boolean = false,
|
||||
@@ -102,7 +117,7 @@ data class V2rayConfig(
|
||||
var port: Int = DEFAULT_PORT,
|
||||
var level: Int = DEFAULT_LEVEL,
|
||||
val email: String? = null,
|
||||
val flow: String? = null,
|
||||
var flow: String? = null,
|
||||
val ivCheck: Boolean? = null,
|
||||
var users: List<SocksUsersBean>? = null) {
|
||||
|
||||
@@ -113,6 +128,9 @@ data class V2rayConfig(
|
||||
}
|
||||
|
||||
data class Response(var type: String)
|
||||
|
||||
data class WireGuardBean(var publicKey: String = "",
|
||||
var endpoint: String = "")
|
||||
}
|
||||
|
||||
data class StreamSettingsBean(var network: String = DEFAULT_NETWORK,
|
||||
@@ -120,13 +138,15 @@ data class V2rayConfig(
|
||||
var tcpSettings: TcpSettingsBean? = null,
|
||||
var kcpSettings: KcpSettingsBean? = null,
|
||||
var wsSettings: WsSettingsBean? = null,
|
||||
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
||||
var splithttpSettings: SplithttpSettingsBean? = null,
|
||||
var httpSettings: HttpSettingsBean? = null,
|
||||
var tlsSettings: TlsSettingsBean? = null,
|
||||
var quicSettings: QuicSettingBean? = null,
|
||||
var xtlsSettings: TlsSettingsBean? = null,
|
||||
var realitySettings: TlsSettingsBean? = null,
|
||||
var grpcSettings: GrpcSettingsBean? = null,
|
||||
val dsSettings: Any? = null,
|
||||
val sockopt: Any? = null
|
||||
var sockopt: SockoptBean? = null
|
||||
) {
|
||||
|
||||
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
|
||||
@@ -138,7 +158,7 @@ data class V2rayConfig(
|
||||
var headers: HeadersBean = HeadersBean(),
|
||||
val version: 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")
|
||||
val userAgent: List<String>? = null,
|
||||
@SerializedName("Accept-Encoding")
|
||||
@@ -169,9 +189,24 @@ data class V2rayConfig(
|
||||
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(),
|
||||
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,
|
||||
var serverName: String = "",
|
||||
val alpn: List<String>? = null,
|
||||
@@ -182,7 +217,12 @@ data class V2rayConfig(
|
||||
val fingerprint: String? = null,
|
||||
val certificates: List<Any>? = 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",
|
||||
var key: String = "",
|
||||
@@ -191,10 +231,15 @@ data class V2rayConfig(
|
||||
}
|
||||
|
||||
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?,
|
||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String {
|
||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
||||
authority: String?): String {
|
||||
var sni = ""
|
||||
network = transport
|
||||
when (network) {
|
||||
@@ -207,7 +252,7 @@ data class V2rayConfig(
|
||||
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||
tcpSetting.header.request = requestObj
|
||||
sni = requestObj.headers.Host.getOrNull(0) ?: sni
|
||||
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
|
||||
}
|
||||
} else {
|
||||
tcpSetting.header.type = "none"
|
||||
@@ -232,6 +277,20 @@ data class V2rayConfig(
|
||||
wssetting.path = path ?: "/"
|
||||
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" -> {
|
||||
network = "h2"
|
||||
val h2Setting = HttpSettingsBean()
|
||||
@@ -251,28 +310,42 @@ data class V2rayConfig(
|
||||
val grpcSetting = GrpcSettingsBean()
|
||||
grpcSetting.multiMode = mode == "multi"
|
||||
grpcSetting.serviceName = serviceName ?: ""
|
||||
sni = host ?: ""
|
||||
grpcSetting.authority = authority ?: ""
|
||||
grpcSetting.idle_timeout = 60
|
||||
grpcSetting.health_check_timeout = 20
|
||||
sni = authority ?: ""
|
||||
grpcSettings = grpcSetting
|
||||
}
|
||||
}
|
||||
return sni
|
||||
}
|
||||
|
||||
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String) {
|
||||
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
|
||||
publicKey: String?, shortId: String?, spiderX: String?) {
|
||||
security = streamSecurity
|
||||
val tlsSetting = TlsSettingsBean(
|
||||
allowInsecure = allowInsecure,
|
||||
serverName = sni
|
||||
serverName = sni,
|
||||
fingerprint = fingerprint,
|
||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
||||
publicKey = publicKey,
|
||||
shortId = shortId,
|
||||
spiderX = spiderX
|
||||
)
|
||||
if (security == TLS) {
|
||||
tlsSettings = tlsSetting
|
||||
} else if (security == XTLS) {
|
||||
xtlsSettings = tlsSetting
|
||||
realitySettings = null
|
||||
} else if (security == REALITY) {
|
||||
tlsSettings = null
|
||||
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? {
|
||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||
@@ -282,6 +355,8 @@ data class V2rayConfig(
|
||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
return settings?.servers?.get(0)?.address
|
||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -294,6 +369,8 @@ data class V2rayConfig(
|
||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||
|| protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
return settings?.servers?.get(0)?.port
|
||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -307,6 +384,8 @@ data class V2rayConfig(
|
||||
return settings?.servers?.get(0)?.password
|
||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)) {
|
||||
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||
return settings?.secretKey
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -322,7 +401,9 @@ data class V2rayConfig(
|
||||
|
||||
fun getTransportSettingDetails(): List<String>? {
|
||||
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.SHADOWSOCKS.name, true)) {
|
||||
val transport = streamSettings?.network ?: return null
|
||||
return when (transport) {
|
||||
"tcp" -> {
|
||||
@@ -343,6 +424,18 @@ data class V2rayConfig(
|
||||
wsSetting.headers.Host,
|
||||
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" -> {
|
||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
||||
listOf("",
|
||||
@@ -358,7 +451,7 @@ data class V2rayConfig(
|
||||
"grpc" -> {
|
||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
||||
listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
|
||||
"",
|
||||
grpcSetting.authority ?: "",
|
||||
grpcSetting.serviceName)
|
||||
}
|
||||
else -> null
|
||||
@@ -383,11 +476,11 @@ data class V2rayConfig(
|
||||
}
|
||||
|
||||
data class RoutingBean(var domainStrategy: String,
|
||||
val domainMatcher: String? = null,
|
||||
var domainMatcher: String? = null,
|
||||
var rules: ArrayList<RulesBean>,
|
||||
val balancers: List<Any>? = null) {
|
||||
|
||||
data class RulesBean(var type: String = "",
|
||||
data class RulesBean(
|
||||
var ip: ArrayList<String>? = null,
|
||||
var domain: ArrayList<String>? = null,
|
||||
var outboundTag: String = "",
|
||||
@@ -420,13 +513,11 @@ data class V2rayConfig(
|
||||
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
|
||||
|
||||
fun getProxyOutbound(): OutboundBean? {
|
||||
outbounds.forEach { outbound ->
|
||||
if (outbound.protocol.equals(EConfigType.VMESS.name, true) ||
|
||||
outbound.protocol.equals(EConfigType.VLESS.name, true) ||
|
||||
outbound.protocol.equals(EConfigType.SHADOWSOCKS.name, true) ||
|
||||
outbound.protocol.equals(EConfigType.SOCKS.name, true) ||
|
||||
outbound.protocol.equals(EConfigType.TROJAN.name, true)) {
|
||||
return outbound
|
||||
outbounds?.forEach { outbound ->
|
||||
EConfigType.entries.forEach {
|
||||
if (outbound.protocol.equals(it.name, true)) {
|
||||
return outbound
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -6,9 +6,12 @@ data class VmessQRCode(var v: String = "",
|
||||
var port: String = "",
|
||||
var id: String = "",
|
||||
var aid: String = "0",
|
||||
var scy: String = "",
|
||||
var net: String = "",
|
||||
var type: String = "",
|
||||
var host: String = "",
|
||||
var path: String = "",
|
||||
var tls: String = "",
|
||||
var sni: String = "")
|
||||
var sni: String = "",
|
||||
var alpn: String = "",
|
||||
var fp: String = "")
|
||||
@@ -6,71 +6,52 @@ import android.widget.Toast
|
||||
import com.v2ray.ang.AngApplication
|
||||
import me.drakeet.support.toast.ToastCompat
|
||||
import org.json.JSONObject
|
||||
import java.net.URI
|
||||
import java.net.URLConnection
|
||||
|
||||
/**
|
||||
* Some extensions
|
||||
*/
|
||||
val Context.v2RayApplication: AngApplication?
|
||||
get() = applicationContext as? AngApplication
|
||||
|
||||
val Context.v2RayApplication: AngApplication
|
||||
get() = applicationContext as AngApplication
|
||||
|
||||
fun Context.toast(message: Int): Toast = ToastCompat
|
||||
.makeText(this, message, Toast.LENGTH_SHORT)
|
||||
.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 "∞"
|
||||
fun Context.toast(message: Int) {
|
||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun Float.toShortString(): String {
|
||||
val s = "%.2f".format(this)
|
||||
if (s.length <= 4)
|
||||
return s
|
||||
return s.substring(0, 4).removeSuffix(".")
|
||||
fun Context.toast(message: CharSequence) {
|
||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
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 {
|
||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
||||
var size = this.toDouble()
|
||||
var unitIndex = 0
|
||||
while (size >= THRESHOLD && unitIndex < units.size - 1) {
|
||||
size /= DIVISOR
|
||||
unitIndex++
|
||||
}
|
||||
return String.format("%.1f %s", size, units[unitIndex])
|
||||
}
|
||||
|
||||
val URLConnection.responseLength: Long
|
||||
get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong()
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
contentLengthLong
|
||||
} else {
|
||||
contentLength.toLong()
|
||||
}
|
||||
|
||||
val URI.idnHost: String
|
||||
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.Intent
|
||||
import android.text.TextUtils
|
||||
import com.google.zxing.WriterException
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
@@ -34,7 +33,7 @@ class TaskerReceiver : BroadcastReceiver() {
|
||||
} else {
|
||||
Utils.stopVService(context)
|
||||
}
|
||||
} catch (e: WriterException) {
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
@@ -38,17 +38,11 @@ class WidgetProvider : AppWidgetProvider() {
|
||||
})
|
||||
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
|
||||
if (isRunning) {
|
||||
remoteViews.setInt(
|
||||
R.id.layout_switch,
|
||||
"setBackgroundResource",
|
||||
R.drawable.ic_rounded_corner_theme
|
||||
)
|
||||
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
|
||||
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_active)
|
||||
} else {
|
||||
remoteViews.setInt(
|
||||
R.id.layout_switch,
|
||||
"setBackgroundResource",
|
||||
R.drawable.ic_rounded_corner_grey
|
||||
)
|
||||
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_play_24dp)
|
||||
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_inactive)
|
||||
}
|
||||
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
|
||||
@@ -22,11 +22,11 @@ class QSTileService : TileService() {
|
||||
if (state == Tile.STATE_INACTIVE) {
|
||||
qsTile?.state = Tile.STATE_INACTIVE
|
||||
qsTile?.label = getString(R.string.app_name)
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v_idle)
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
} else if (state == Tile.STATE_ACTIVE) {
|
||||
qsTile?.state = Tile.STATE_ACTIVE
|
||||
qsTile?.label = V2RayServiceManager.currentConfig?.remarks
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_v)
|
||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||
}
|
||||
|
||||
qsTile?.updateTile()
|
||||
@@ -36,7 +36,12 @@ class QSTileService : TileService() {
|
||||
super.onStartListening()
|
||||
setState(Tile.STATE_INACTIVE)
|
||||
mMsgReceive = ReceiveMessageHandler(this)
|
||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
||||
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))
|
||||
}
|
||||
|
||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||
}
|
||||
|
||||
@@ -62,7 +67,7 @@ class QSTileService : TileService() {
|
||||
private var mMsgReceive: BroadcastReceiver? = null
|
||||
|
||||
private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() {
|
||||
internal var mReference: SoftReference<QSTileService> = SoftReference(context)
|
||||
var mReference: SoftReference<QSTileService> = SoftReference(context)
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
val context = mReference.get()
|
||||
when (intent?.getIntExtra("key", 0)) {
|
||||
|
||||
@@ -5,10 +5,9 @@ import android.app.Service
|
||||
interface ServiceControl {
|
||||
fun getService(): Service
|
||||
|
||||
fun startService(parameters: String)
|
||||
fun startService()
|
||||
|
||||
fun stopService()
|
||||
|
||||
fun vpnProtect(socket: Int): Boolean
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
|
||||
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
|
||||
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 {
|
||||
|
||||
|
||||
|
||||
class UpdateTask(context: Context, params: WorkerParameters) :
|
||||
CoroutineWorker(context, params) {
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
private val notification =
|
||||
NotificationCompat.Builder(applicationContext, SUBSCRIPTION_UPDATE_CHANNEL)
|
||||
.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(SUBSCRIPTION_UPDATE_CHANNEL)
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
SUBSCRIPTION_UPDATE_CHANNEL,
|
||||
SUBSCRIPTION_UPDATE_CHANNEL_NAME,
|
||||
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,8 +1,13 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
@@ -25,7 +30,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun startService(parameters: String) {
|
||||
override fun startService() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@@ -40,4 +45,12 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import android.util.Log
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
@@ -27,8 +27,8 @@ import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import com.v2ray.ang.util.V2rayConfigUtil
|
||||
import go.Seq
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import libv2ray.Libv2ray
|
||||
import libv2ray.V2RayPoint
|
||||
@@ -44,7 +44,7 @@ object V2RayServiceManager {
|
||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
||||
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
||||
|
||||
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback())
|
||||
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
||||
private val mMsgReceive = ReceiveMessageHandler()
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
@@ -52,12 +52,8 @@ object V2RayServiceManager {
|
||||
var serviceControl: SoftReference<ServiceControl>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
val context = value?.get()?.getService()?.applicationContext
|
||||
context?.let {
|
||||
v2rayPoint.packageName = Utils.packagePath(context)
|
||||
v2rayPoint.packageCodePath = context.applicationInfo.nativeLibraryDir + "/"
|
||||
Seq.setContext(context)
|
||||
}
|
||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
||||
}
|
||||
var currentConfig: ServerConfig? = null
|
||||
|
||||
@@ -67,12 +63,17 @@ object V2RayServiceManager {
|
||||
private var mNotificationManager: NotificationManager? = null
|
||||
|
||||
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) {
|
||||
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
||||
}else{
|
||||
} else {
|
||||
context.toast(R.string.toast_services_start)
|
||||
}
|
||||
val intent = if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
|
||||
val intent = if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") {
|
||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||
} else {
|
||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||
@@ -88,7 +89,6 @@ object V2RayServiceManager {
|
||||
override fun shutdown(): Long {
|
||||
val serviceControl = serviceControl?.get() ?: return -1
|
||||
// called by go
|
||||
// shutdown the whole vpn service
|
||||
return try {
|
||||
serviceControl.stopService()
|
||||
0
|
||||
@@ -102,9 +102,9 @@ object V2RayServiceManager {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun protect(l: Long): Long {
|
||||
val serviceControl = serviceControl?.get() ?: return 0
|
||||
return if (serviceControl.vpnProtect(l.toInt())) 0 else 1
|
||||
override fun protect(l: Long): Boolean {
|
||||
val serviceControl = serviceControl?.get() ?: return true
|
||||
return serviceControl.vpnProtect(l.toInt())
|
||||
}
|
||||
|
||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
||||
@@ -116,7 +116,7 @@ object V2RayServiceManager {
|
||||
val serviceControl = serviceControl?.get() ?: return -1
|
||||
//Logger.d(s)
|
||||
return try {
|
||||
serviceControl.startService(s)
|
||||
serviceControl.startService()
|
||||
lastQueryTime = System.currentTimeMillis()
|
||||
startSpeedNotification()
|
||||
0
|
||||
@@ -125,48 +125,49 @@ object V2RayServiceManager {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun startV2rayPoint() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||
if (!v2rayPoint.isRunning) {
|
||||
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
|
||||
if (!result.status)
|
||||
return
|
||||
if (v2rayPoint.isRunning) {
|
||||
return
|
||||
}
|
||||
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
|
||||
if (!result.status)
|
||||
return
|
||||
|
||||
try {
|
||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
service.registerReceiver(mMsgReceive, mFilter)
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
|
||||
v2rayPoint.configureFileContent = result.content
|
||||
v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
|
||||
currentConfig = config
|
||||
v2rayPoint.enableLocalDNS = settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) ?: false
|
||||
v2rayPoint.forwardIpv6 = settingsStorage?.decodeBool(AppConfig.PREF_FORWARD_IPV6) ?: false
|
||||
v2rayPoint.proxyOnly = settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" != "VPN"
|
||||
|
||||
try {
|
||||
v2rayPoint.runLoop()
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||
showNotification()
|
||||
try {
|
||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||
cancelNotification()
|
||||
service.registerReceiver(mMsgReceive, mFilter)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
|
||||
v2rayPoint.configureFileContent = result.content
|
||||
v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
|
||||
currentConfig = config
|
||||
|
||||
try {
|
||||
v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false)
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, e.toString())
|
||||
}
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||
showNotification()
|
||||
} else {
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||
cancelNotification()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +175,7 @@ object V2RayServiceManager {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
|
||||
if (v2rayPoint.isRunning) {
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
v2rayPoint.stopLoop()
|
||||
} catch (e: Exception) {
|
||||
@@ -217,6 +218,9 @@ object V2RayServiceManager {
|
||||
AppConfig.MSG_STATE_RESTART -> {
|
||||
startV2rayPoint()
|
||||
}
|
||||
AppConfig.MSG_MEASURE_DELAY -> {
|
||||
measureV2rayDelay()
|
||||
}
|
||||
}
|
||||
|
||||
when (intent?.action) {
|
||||
@@ -232,6 +236,37 @@ object V2RayServiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
private fun measureV2rayDelay() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val service = serviceControl?.get()?.getService() ?: return@launch
|
||||
var time = -1L
|
||||
var errstr = ""
|
||||
if (v2rayPoint.isRunning) {
|
||||
try {
|
||||
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl())
|
||||
} catch (e: Exception) {
|
||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
||||
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) {
|
||||
service.getString(R.string.connection_test_error, errstr)
|
||||
} else {
|
||||
service.getString(R.string.connection_test_available, time)
|
||||
}
|
||||
|
||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotification() {
|
||||
val service = serviceControl?.get()?.getService() ?: return
|
||||
val startMainIntent = Intent(service, MainActivity::class.java)
|
||||
@@ -265,14 +300,14 @@ object V2RayServiceManager {
|
||||
}
|
||||
|
||||
mBuilder = NotificationCompat.Builder(service, channelId)
|
||||
.setSmallIcon(R.drawable.ic_v)
|
||||
.setSmallIcon(R.drawable.ic_stat_name)
|
||||
.setContentTitle(currentConfig?.remarks)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.addAction(R.drawable.ic_close_grey_800_24dp,
|
||||
.addAction(R.drawable.ic_delete_24dp,
|
||||
service.getString(R.string.notification_action_stop_v2ray),
|
||||
stopV2RayPendingIntent)
|
||||
//.build()
|
||||
@@ -284,8 +319,8 @@ object V2RayServiceManager {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(): String {
|
||||
val channelId = "RAY_NG_M_CH_ID"
|
||||
val channelName = "V2rayNG Background Service"
|
||||
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
||||
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
||||
val chan = NotificationChannel(channelId,
|
||||
channelName, NotificationManager.IMPORTANCE_HIGH)
|
||||
chan.lightColor = Color.DKGRAY
|
||||
@@ -306,7 +341,7 @@ object V2RayServiceManager {
|
||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
||||
if (mBuilder != null) {
|
||||
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
|
||||
mBuilder?.setSmallIcon(R.drawable.ic_v)
|
||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
|
||||
} else if (proxyTraffic > directTraffic) {
|
||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
|
||||
} else {
|
||||
@@ -326,7 +361,7 @@ object V2RayServiceManager {
|
||||
return mNotificationManager
|
||||
}
|
||||
|
||||
fun startSpeedNotification() {
|
||||
private fun startSpeedNotification() {
|
||||
if (mSubscription == null &&
|
||||
v2rayPoint.isRunning &&
|
||||
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) {
|
||||
@@ -341,16 +376,16 @@ object V2RayServiceManager {
|
||||
var proxyTotal = 0L
|
||||
val text = StringBuilder()
|
||||
outboundTags?.forEach {
|
||||
val up = v2rayPoint.queryStats(it, "uplink")
|
||||
val down = v2rayPoint.queryStats(it, "downlink")
|
||||
val up = v2rayPoint.queryStats(it, AppConfig.UPLINK)
|
||||
val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK)
|
||||
if (up + down > 0) {
|
||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
||||
proxyTotal += up + down
|
||||
}
|
||||
}
|
||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, "uplink")
|
||||
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, "downlink")
|
||||
val zeroSpeed = (proxyTotal == 0L && directUplink == 0L && directDownlink == 0L)
|
||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
||||
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
|
||||
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
||||
if (!zeroSpeed || !lastZeroSpeed) {
|
||||
if (proxyTotal == 0L) {
|
||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
||||
@@ -375,7 +410,7 @@ object V2RayServiceManager {
|
||||
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
|
||||
}
|
||||
|
||||
fun stopSpeedNotification() {
|
||||
private fun stopSpeedNotification() {
|
||||
if (mSubscription != null) {
|
||||
mSubscription?.unsubscribe() //stop queryStats
|
||||
mSubscription = null
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
|
||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
|
||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.SpeedtestUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import go.Seq
|
||||
import kotlinx.coroutines.*
|
||||
import libv2ray.Libv2ray
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class V2RayTestService : Service() {
|
||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Seq.setContext(this)
|
||||
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.getIntExtra("key", 0)) {
|
||||
MSG_MEASURE_CONFIG -> {
|
||||
val contentPair = intent.getSerializableExtra("content") as Pair<String, String>
|
||||
realTestScope.launch {
|
||||
val result = SpeedtestUtil.realPing(contentPair.second)
|
||||
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result))
|
||||
}
|
||||
}
|
||||
MSG_MEASURE_CONFIG_CANCEL -> {
|
||||
realTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
}
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.v2ray.ang.service
|
||||
|
||||
import android.app.*
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@@ -8,12 +8,14 @@ import android.net.*
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.StrictMode
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.ERoutingMode
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.MyContextWrapper
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -22,19 +24,32 @@ import java.io.File
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
class V2RayVpnService : VpnService(), ServiceControl {
|
||||
companion object {
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN4_CLIENT = "26.26.26.1"
|
||||
private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2"
|
||||
private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1"
|
||||
private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2"
|
||||
private const val TUN2SOCKS = "libtun2socks.so"
|
||||
}
|
||||
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
private lateinit var mInterface: ParcelFileDescriptor
|
||||
private var isRunning = false
|
||||
|
||||
/**
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
*
|
||||
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
|
||||
* satisfies default network capabilities but only THE default network. Unfortunately we need to have
|
||||
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
||||
*/
|
||||
//val fd: Int get() = mInterface.fd
|
||||
private lateinit var process: Process
|
||||
|
||||
/**destroy
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return our VPN interface: https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
*
|
||||
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
|
||||
* satisfies default network capabilities but only THE default network. Unfortunately we need to have
|
||||
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
||||
*/
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
private val defaultNetworkRequest by lazy {
|
||||
NetworkRequest.Builder()
|
||||
@@ -51,10 +66,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
override fun onAvailable(network: Network) {
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
// it's a good idea to refresh capabilities
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
setUnderlyingNetworks(null)
|
||||
}
|
||||
@@ -73,18 +90,17 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
stopV2Ray()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
stopV2Ray()
|
||||
super.onLowMemory()
|
||||
}
|
||||
// override fun onLowMemory() {
|
||||
// stopV2Ray()
|
||||
// super.onLowMemory()
|
||||
// }
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopV2Ray()
|
||||
V2RayServiceManager.cancelNotification()
|
||||
}
|
||||
|
||||
private fun setup(parameters: String) {
|
||||
|
||||
private fun setup() {
|
||||
val prepare = prepare(this)
|
||||
if (prepare != null) {
|
||||
return
|
||||
@@ -93,35 +109,35 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
// If the old interface has exactly the same parameters, use it!
|
||||
// Configure a builder while parsing the parameters.
|
||||
val builder = Builder()
|
||||
val enableLocalDns = settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) ?: false
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: "0"
|
||||
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||
|
||||
parameters.split(" ")
|
||||
.map { it.split(",") }
|
||||
.forEach {
|
||||
when (it[0][0]) {
|
||||
'm' -> builder.setMtu(java.lang.Short.parseShort(it[1]).toInt())
|
||||
's' -> builder.addSearchDomain(it[1])
|
||||
'a' -> builder.addAddress(it[1], Integer.parseInt(it[2]))
|
||||
'r' -> {
|
||||
if (routingMode == "1" || routingMode == "3") {
|
||||
if (it[1] == "::") { //not very elegant, should move Vpn setting in Kotlin, simplify go code
|
||||
builder.addRoute("2000::", 3)
|
||||
} else {
|
||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach { cidr ->
|
||||
val addr = cidr.split('/')
|
||||
builder.addRoute(addr[0], addr[1].toInt())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
builder.addRoute(it[1], Integer.parseInt(it[2]))
|
||||
}
|
||||
}
|
||||
'd' -> builder.addDnsServer(it[1])
|
||||
}
|
||||
}
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
||||
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
||||
|
||||
if(!enableLocalDns) {
|
||||
builder.setMtu(VPN_MTU)
|
||||
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
|
||||
val addr = it.split('/')
|
||||
builder.addRoute(addr[0], addr[1].toInt())
|
||||
}
|
||||
} else {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
}
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
||||
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
||||
} else {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
} else {
|
||||
Utils.getVpnDnsServers()
|
||||
.forEach {
|
||||
if (Utils.isPureIpAddress(it)) {
|
||||
@@ -169,23 +185,69 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
// Create a new interface using the builder and save the parameters.
|
||||
try {
|
||||
mInterface = builder.establish()!!
|
||||
isRunning = true
|
||||
runTun2socks()
|
||||
} catch (e: Exception) {
|
||||
// non-nullable lateinit var
|
||||
e.printStackTrace()
|
||||
stopV2Ray()
|
||||
}
|
||||
}
|
||||
|
||||
sendFd()
|
||||
private fun runTun2socks() {
|
||||
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||
val cmd = arrayListOf(File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||
"--netif-netmask", "255.255.255.252",
|
||||
"--socks-server-addr", "127.0.0.1:${socksPort}",
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||
"--enable-udprelay",
|
||||
"--loglevel", "notice")
|
||||
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||
cmd.add("--netif-ip6addr")
|
||||
cmd.add(PRIVATE_VLAN6_ROUTER)
|
||||
}
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
||||
cmd.add("--dnsgw")
|
||||
cmd.add("127.0.0.1:${localDnsPort}")
|
||||
}
|
||||
Log.d(packageName, cmd.toString())
|
||||
|
||||
try {
|
||||
val proBuilder = ProcessBuilder(cmd)
|
||||
proBuilder.redirectErrorStream(true)
|
||||
process = proBuilder
|
||||
.directory(applicationContext.filesDir)
|
||||
.start()
|
||||
Thread(Runnable {
|
||||
Log.d(packageName,"$TUN2SOCKS check")
|
||||
process.waitFor()
|
||||
Log.d(packageName,"$TUN2SOCKS exited")
|
||||
if (isRunning) {
|
||||
Log.d(packageName,"$TUN2SOCKS restart")
|
||||
runTun2socks()
|
||||
}
|
||||
}).start()
|
||||
Log.d(packageName, process.toString())
|
||||
|
||||
sendFd()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendFd() {
|
||||
val fd = mInterface.fileDescriptor
|
||||
val path = File(Utils.packagePath(applicationContext), "sock_path").absolutePath
|
||||
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||
Log.d(packageName, path)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
var tries = 0
|
||||
while (true) try {
|
||||
Thread.sleep(1000L shl tries)
|
||||
Thread.sleep(50L shl tries)
|
||||
Log.d(packageName, "sendFd tries: $tries")
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
@@ -212,6 +274,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
// val emptyInfo = VpnNetworkInfo()
|
||||
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
||||
// saveVpnNetworkInfo(configName, info)
|
||||
isRunning = false;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||
@@ -220,6 +283,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(packageName, "tun2socks destroy")
|
||||
process.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.d(packageName, e.toString())
|
||||
}
|
||||
|
||||
V2RayServiceManager.stopV2rayPoint()
|
||||
|
||||
if (isForced) {
|
||||
@@ -235,7 +305,6 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,8 +312,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun startService(parameters: String) {
|
||||
setup(parameters)
|
||||
override fun startService() {
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun stopService() {
|
||||
@@ -255,4 +324,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||
return protect(socket)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,41 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
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.Utils
|
||||
|
||||
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) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
// Handles the home button press by delegating to the onBackPressedDispatcher.
|
||||
// This ensures consistent back navigation behavior.
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
val context = newBase?.let {
|
||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||
}
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Bundle
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||
@@ -15,6 +16,7 @@ import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.LinkedHashSet
|
||||
|
||||
@@ -29,7 +31,6 @@ class LogcatActivity : BaseActivity() {
|
||||
|
||||
title = getString(R.string.title_logcat)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
logcat(false)
|
||||
}
|
||||
|
||||
@@ -38,7 +39,7 @@ class LogcatActivity : BaseActivity() {
|
||||
try {
|
||||
binding.pbWaiting.visibility = View.VISIBLE
|
||||
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
if (shouldFlushLog) {
|
||||
val lst = LinkedHashSet<String>()
|
||||
lst.add("logcat")
|
||||
@@ -81,7 +82,7 @@ class LogcatActivity : BaseActivity() {
|
||||
toast(R.string.toast_success)
|
||||
true
|
||||
}
|
||||
R.id.delete -> {
|
||||
R.id.clear_all -> {
|
||||
logcat(true)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||
import com.v2ray.ang.databinding.LayoutProgressBinding
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||
@@ -34,9 +41,8 @@ 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.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import libv2ray.Libv2ray
|
||||
import me.drakeet.support.toast.ToastCompat
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@@ -53,6 +59,23 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
startV2Ray()
|
||||
}
|
||||
}
|
||||
private val requestSubSettingActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
initGroupTab()
|
||||
}
|
||||
private val tabGroupListener = object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
val selectId = tab?.tag.toString()
|
||||
if (selectId != mainViewModel.subscriptionId) {
|
||||
mainViewModel.subscriptionIdChanged(selectId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
}
|
||||
}
|
||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||
val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
@@ -67,7 +90,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
binding.fab.setOnClickListener {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
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)
|
||||
if (intent == null) {
|
||||
startV2Ray()
|
||||
@@ -80,7 +103,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
binding.layoutTest.setOnClickListener {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
binding.tvTestState.text = getString(R.string.connection_test_testing)
|
||||
setTestState(getString(R.string.connection_test_testing))
|
||||
mainViewModel.testCurrentServerRealPing()
|
||||
} else {
|
||||
// tv_test_state.text = getString(R.string.connection_test_fail)
|
||||
@@ -101,63 +124,97 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
binding.drawerLayout.addDrawerListener(toggle)
|
||||
toggle.syncState()
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
binding.version.text = "v${BuildConfig.VERSION_NAME} (${Libv2ray.checkVersionX()})"
|
||||
|
||||
setupViewModelObserver()
|
||||
migrateLegacy()
|
||||
initGroupTab()
|
||||
setupViewModel()
|
||||
mainViewModel.copyAssets(assets)
|
||||
|
||||
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 setupViewModelObserver() {
|
||||
mainViewModel.updateListAction.observe(this) {
|
||||
val index = it ?: return@observe
|
||||
private fun setupViewModel() {
|
||||
mainViewModel.updateListAction.observe(this) { index ->
|
||||
if (index >= 0) {
|
||||
adapter.notifyItemChanged(index)
|
||||
} else {
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
mainViewModel.updateTestResultAction.observe(this) { binding.tvTestState.text = it }
|
||||
mainViewModel.isRunning.observe(this) {
|
||||
val isRunning = it ?: return@observe
|
||||
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
|
||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||
adapter.isRunning = isRunning
|
||||
if (isRunning) {
|
||||
binding.fab.setImageResource(R.drawable.ic_v)
|
||||
binding.tvTestState.text = getString(R.string.connection_connected)
|
||||
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))
|
||||
binding.layoutTest.isFocusable = true
|
||||
} else {
|
||||
binding.fab.setImageResource(R.drawable.ic_v_idle)
|
||||
binding.tvTestState.text = getString(R.string.connection_not_connected)
|
||||
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))
|
||||
binding.layoutTest.isFocusable = false
|
||||
}
|
||||
hideCircle()
|
||||
}
|
||||
mainViewModel.startListenBroadcast()
|
||||
}
|
||||
|
||||
private fun migrateLegacy() {
|
||||
GlobalScope.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun initGroupTab() {
|
||||
binding.tabGroup.removeOnTabSelectedListener(tabGroupListener)
|
||||
binding.tabGroup.removeAllTabs()
|
||||
binding.tabGroup.isVisible = false
|
||||
|
||||
val (listId, listRemarks) = mainViewModel.getSubscriptions(this)
|
||||
if (listId == null || listRemarks == null) {
|
||||
return
|
||||
}
|
||||
|
||||
for (it in listRemarks.indices) {
|
||||
val tab = binding.tabGroup.newTab()
|
||||
tab.text = listRemarks[it]
|
||||
tab.tag = listId[it]
|
||||
binding.tabGroup.addTab(tab)
|
||||
}
|
||||
val selectIndex =
|
||||
listId.indexOf(mainViewModel.subscriptionId).takeIf { it >= 0 } ?: (listId.count() - 1)
|
||||
binding.tabGroup.selectTab(binding.tabGroup.getTabAt(selectIndex))
|
||||
binding.tabGroup.addOnTabSelectedListener(tabGroupListener)
|
||||
binding.tabGroup.isVisible = true
|
||||
}
|
||||
|
||||
fun startV2Ray() {
|
||||
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
showCircle()
|
||||
// toast(R.string.toast_services_start)
|
||||
V2RayServiceManager.startV2Ray(this)
|
||||
hideCircle()
|
||||
}
|
||||
|
||||
fun restartV2Ray() {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
Utils.stopVService(this)
|
||||
}
|
||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
startV2Ray()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
@@ -184,18 +241,27 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
R.id.import_manually_vmess -> {
|
||||
startActivity(Intent().putExtra("createConfigType", EConfigType.VMESS.value).
|
||||
setClass(this, ServerActivity::class.java))
|
||||
importManually(EConfigType.VMESS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_vless -> {
|
||||
importManually(EConfigType.VLESS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_ss -> {
|
||||
startActivity(Intent().putExtra("createConfigType", EConfigType.SHADOWSOCKS.value).
|
||||
setClass(this, ServerActivity::class.java))
|
||||
importManually(EConfigType.SHADOWSOCKS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_socks -> {
|
||||
startActivity(Intent().putExtra("createConfigType", EConfigType.SOCKS.value).
|
||||
setClass(this, ServerActivity::class.java))
|
||||
importManually(EConfigType.SOCKS.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_trojan -> {
|
||||
importManually(EConfigType.TROJAN.value)
|
||||
true
|
||||
}
|
||||
R.id.import_manually_wireguard -> {
|
||||
importManually(EConfigType.WIREGUARD.value)
|
||||
true
|
||||
}
|
||||
R.id.import_config_custom_clipboard -> {
|
||||
@@ -215,11 +281,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
|
||||
// R.id.sub_setting -> {
|
||||
// startActivity<SubSettingActivity>()
|
||||
// true
|
||||
// }
|
||||
|
||||
R.id.sub_update -> {
|
||||
importConfigViaSub()
|
||||
true
|
||||
@@ -239,22 +300,73 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
true
|
||||
}
|
||||
|
||||
// R.id.settings -> {
|
||||
// startActivity<SettingsActivity>("isRunning" to isRunning)
|
||||
// true
|
||||
// }
|
||||
// R.id.logcat -> {
|
||||
// startActivity<LogcatActivity>()
|
||||
// true
|
||||
// }
|
||||
R.id.real_ping_all -> {
|
||||
mainViewModel.testAllRealPing()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.service_restart -> {
|
||||
restartV2Ray()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.del_all_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeAllServer()
|
||||
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()
|
||||
true
|
||||
}
|
||||
R.id.del_invalid_config -> {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeInvalidServer()
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||
//do noting
|
||||
}
|
||||
.show()
|
||||
true
|
||||
}
|
||||
R.id.sort_by_test_results -> {
|
||||
MmkvManager.sortByTestResults()
|
||||
mainViewModel.reloadServerList()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun importManually(createConfigType : Int) {
|
||||
startActivity(
|
||||
Intent()
|
||||
.putExtra("createConfigType", createConfigType)
|
||||
.putExtra("subscriptionId", mainViewModel.subscriptionId)
|
||||
.setClass(this, ServerActivity::class.java)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* import config from qrcode
|
||||
*/
|
||||
fun importQRcode(forConfig: Boolean): Boolean {
|
||||
private fun importQRcode(forConfig: Boolean): Boolean {
|
||||
// try {
|
||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
||||
@@ -290,7 +402,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
/**
|
||||
* import config from clipboard
|
||||
*/
|
||||
fun importClipboard()
|
||||
private fun importClipboard()
|
||||
: Boolean {
|
||||
try {
|
||||
val clipboard = Utils.getClipboard(this)
|
||||
@@ -302,20 +414,30 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
return true
|
||||
}
|
||||
|
||||
fun importBatchConfig(server: String?, subid: String = "") {
|
||||
var count = AngConfigManager.importBatchConfig(server, subid)
|
||||
if (count <= 0) {
|
||||
count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid)
|
||||
}
|
||||
if (count > 0) {
|
||||
toast(R.string.toast_success)
|
||||
mainViewModel.reloadServerList()
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
private fun importBatchConfig(server: String?) {
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
|
||||
delay(500L)
|
||||
launch(Dispatchers.Main) {
|
||||
if (count > 0) {
|
||||
toast(R.string.toast_success)
|
||||
mainViewModel.reloadServerList()
|
||||
} else if (countSub > 0) {
|
||||
initGroupTab()
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importConfigCustomClipboard()
|
||||
private fun importConfigCustomClipboard()
|
||||
: Boolean {
|
||||
try {
|
||||
val configText = Utils.getClipboard(this)
|
||||
@@ -334,7 +456,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
/**
|
||||
* import config from local config file
|
||||
*/
|
||||
fun importConfigCustomLocal(): Boolean {
|
||||
private fun importConfigCustomLocal(): Boolean {
|
||||
try {
|
||||
showFileChooser()
|
||||
} catch (e: Exception) {
|
||||
@@ -344,7 +466,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
return true
|
||||
}
|
||||
|
||||
fun importConfigCustomUrlClipboard()
|
||||
private fun importConfigCustomUrlClipboard()
|
||||
: Boolean {
|
||||
try {
|
||||
val url = Utils.getClipboard(this)
|
||||
@@ -362,13 +484,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
/**
|
||||
* import config from url
|
||||
*/
|
||||
fun importConfigCustomUrl(url: String?): Boolean {
|
||||
private fun importConfigCustomUrl(url: String?): Boolean {
|
||||
try {
|
||||
if (!Utils.isValidUrl(url)) {
|
||||
toast(R.string.toast_invalid_url)
|
||||
return false
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val configText = try {
|
||||
Utils.getUrlContentWithCustomUserAgent(url)
|
||||
} catch (e: Exception) {
|
||||
@@ -389,40 +511,24 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
/**
|
||||
* import config from sub
|
||||
*/
|
||||
fun importConfigViaSub()
|
||||
: Boolean {
|
||||
try {
|
||||
toast(R.string.title_sub_update)
|
||||
MmkvManager.decodeSubscriptions().forEach {
|
||||
if (TextUtils.isEmpty(it.first)
|
||||
|| TextUtils.isEmpty(it.second.remarks)
|
||||
|| TextUtils.isEmpty(it.second.url)
|
||||
) {
|
||||
return@forEach
|
||||
}
|
||||
val url = it.second.url
|
||||
if (!Utils.isValidUrl(url)) {
|
||||
return@forEach
|
||||
}
|
||||
Log.d(ANG_PACKAGE, url)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val configText = try {
|
||||
Utils.getUrlContentWithCustomUserAgent(url)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
launch(Dispatchers.Main) {
|
||||
toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure))
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
importBatchConfig(Utils.decode(configText), it.first)
|
||||
}
|
||||
private fun importConfigViaSub() : Boolean {
|
||||
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)
|
||||
mainViewModel.reloadServerList()
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -437,7 +543,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
|
||||
try {
|
||||
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||
} catch (ex: android.content.ActivityNotFoundException) {
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
toast(R.string.toast_require_file_manager)
|
||||
}
|
||||
}
|
||||
@@ -453,8 +559,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
* read content from 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)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(permission)
|
||||
.subscribe {
|
||||
if (it) {
|
||||
try {
|
||||
@@ -472,15 +583,19 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
/**
|
||||
* import customize config
|
||||
*/
|
||||
fun importCustomizeConfig(server: String?) {
|
||||
private fun importCustomizeConfig(server: String?) {
|
||||
try {
|
||||
if (server == null || TextUtils.isEmpty(server)) {
|
||||
toast(R.string.toast_none_data)
|
||||
return
|
||||
}
|
||||
mainViewModel.appendCustomConfigServer(server)
|
||||
toast(R.string.toast_success)
|
||||
adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
|
||||
if (mainViewModel.appendCustomConfigServer(server)) {
|
||||
mainViewModel.reloadServerList()
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
|
||||
} catch (e: Exception) {
|
||||
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
@@ -488,6 +603,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTestState(content: String?) {
|
||||
binding.tvTestState.text = content
|
||||
}
|
||||
|
||||
// val mConnection = object : ServiceConnection {
|
||||
// override fun onServiceDisconnected(name: ComponentName?) {
|
||||
// }
|
||||
@@ -498,61 +617,37 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
// }
|
||||
|
||||
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)
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
fun showCircle() {
|
||||
binding.fabProgressCircle.show()
|
||||
}
|
||||
|
||||
fun hideCircle() {
|
||||
try {
|
||||
Observable.timer(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (binding.fabProgressCircle.isShown) {
|
||||
binding.fabProgressCircle.hide()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
// Handle navigation view item clicks here.
|
||||
when (item.itemId) {
|
||||
//R.id.server_profile -> activityClass = MainActivity::class.java
|
||||
R.id.sub_setting -> {
|
||||
startActivity(Intent(this, SubSettingActivity::class.java))
|
||||
requestSubSettingActivity.launch(Intent(this,SubSettingActivity::class.java))
|
||||
}
|
||||
R.id.settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java)
|
||||
.putExtra("isRunning", mainViewModel.isRunning.value == true))
|
||||
}
|
||||
R.id.feedback -> {
|
||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
||||
R.id.user_asset_setting -> {
|
||||
startActivity(Intent(this, UserAssetActivity::class.java))
|
||||
}
|
||||
R.id.promotion -> {
|
||||
Utils.openUri(this, AppConfig.promotionUrl)
|
||||
}
|
||||
R.id.donate -> {
|
||||
// startActivity<InappBuyActivity>()
|
||||
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||
}
|
||||
R.id.logcat -> {
|
||||
startActivity(Intent(this, LogcatActivity::class.java))
|
||||
}
|
||||
R.id.about-> {
|
||||
startActivity(Intent(this, AboutActivity::class.java))
|
||||
}
|
||||
}
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
return true
|
||||
|
||||
@@ -2,14 +2,16 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AngApplication.Companion.application
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||
@@ -38,29 +40,43 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
private var mActivity: MainActivity = activity
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val share_method: Array<out String> by lazy {
|
||||
mActivity.resources.getStringArray(R.array.share_method)
|
||||
}
|
||||
var isRunning = false
|
||||
|
||||
override fun getItemCount() = mActivity.mainViewModel.serverList.size + 1
|
||||
override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
if (holder is MainViewHolder) {
|
||||
val guid = mActivity.mainViewModel.serverList.getOrNull(position) ?: return
|
||||
val config = mActivity.mainViewModel.serversCache.getOrElse(guid) { MmkvManager.decodeServerConfig(guid) } ?: return
|
||||
val guid = mActivity.mainViewModel.serversCache[position].guid
|
||||
val config = mActivity.mainViewModel.serversCache[position].config
|
||||
// //filter
|
||||
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
|
||||
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
|
||||
// ) {
|
||||
// holder.itemMainBinding.cardView.visibility = View.GONE
|
||||
// } else {
|
||||
// holder.itemMainBinding.cardView.visibility = View.VISIBLE
|
||||
// }
|
||||
|
||||
val outbound = config.getProxyOutbound()
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
|
||||
holder.itemMainBinding.tvName.text = config.remarks
|
||||
holder.itemMainBinding.btnRadio.isChecked = guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: ""
|
||||
if (aff?.testDelayMillis?:0L < 0L) {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, android.R.color.holo_red_dark))
|
||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
||||
} else {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
||||
}
|
||||
if (guid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
|
||||
} else {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||
}
|
||||
holder.itemMainBinding.tvSubscription.text = ""
|
||||
val json = subStorage?.decodeString(config.subscriptionId)
|
||||
if (!json.isNullOrBlank()) {
|
||||
@@ -81,7 +97,10 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
|
||||
}
|
||||
}
|
||||
holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}"
|
||||
|
||||
val strState = "${outbound?.getServerAddress()?.dropLast(3)}*** : ${outbound?.getServerPort() ?: ""}"
|
||||
|
||||
holder.itemMainBinding.tvStatistics.text = strState
|
||||
|
||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
||||
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
||||
@@ -123,9 +142,20 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
}
|
||||
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
||||
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
mActivity.mainViewModel.removeServer(guid)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, mActivity.mainViewModel.serverList.size)
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
removeServer(guid, position)
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||
//do noting
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
removeServer(guid, position)
|
||||
}
|
||||
} else {
|
||||
application.toast(R.string.toast_action_not_allowed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,16 +163,16 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
val selected = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
||||
if (guid != selected) {
|
||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||
notifyItemChanged(mActivity.mainViewModel.serverList.indexOf(selected))
|
||||
notifyItemChanged(mActivity.mainViewModel.serverList.indexOf(guid))
|
||||
if (!TextUtils.isEmpty(selected)) {
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected?:""))
|
||||
}
|
||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||
if (isRunning) {
|
||||
mActivity.showCircle()
|
||||
Utils.stopVService(mActivity)
|
||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
V2RayServiceManager.startV2Ray(mActivity)
|
||||
mActivity.hideCircle()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,7 +184,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
||||
} else {
|
||||
holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
||||
Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}")
|
||||
Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +198,12 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeServer(guid: String,position:Int) {
|
||||
mActivity.mainViewModel.removeServer(guid)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_ITEM ->
|
||||
@@ -178,7 +214,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == mActivity.mainViewModel.serverList.size) {
|
||||
return if (position == mActivity.mainViewModel.serversCache.size) {
|
||||
VIEW_TYPE_FOOTER
|
||||
} else {
|
||||
VIEW_TYPE_ITEM
|
||||
@@ -199,10 +235,10 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
|
||||
|
||||
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
||||
BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
|
||||
BaseViewHolder(itemFooterBinding.root)
|
||||
|
||||
override fun onItemDismiss(position: Int) {
|
||||
val guid = mActivity.mainViewModel.serverList.getOrNull(position) ?: return
|
||||
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
|
||||
if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
||||
// mActivity.alert(R.string.del_config_comfirm) {
|
||||
// positiveButton(android.R.string.ok) {
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.AppManagerUtil
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityBypassListBinding
|
||||
import com.v2ray.ang.dto.AppInfo
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.v2RayApplication
|
||||
import com.v2ray.ang.util.AppManagerUtil
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URL
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import java.text.Collator
|
||||
|
||||
class PerAppProxyActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityBypassListBinding
|
||||
|
||||
private var adapter: PerAppProxyAdapter? = 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -48,19 +40,17 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||
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)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map {
|
||||
if (blacklist != null) {
|
||||
it.forEach { one ->
|
||||
if ((blacklist.contains(one.packageName))) {
|
||||
if (blacklist.contains(one.packageName)) {
|
||||
one.isSelected = 1
|
||||
} else {
|
||||
one.isSelected = 0
|
||||
@@ -96,100 +86,120 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.pbWaiting.visibility = View.GONE
|
||||
}
|
||||
/***
|
||||
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
var dst = 0
|
||||
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
dst += dy
|
||||
if (dst > threshold) {
|
||||
header_view.hide()
|
||||
dst = 0
|
||||
} else if (dst < -20) {
|
||||
header_view.show()
|
||||
dst = 0
|
||||
}
|
||||
}
|
||||
|
||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
var dst = 0
|
||||
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 3
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
dst += dy
|
||||
if (dst > threshold) {
|
||||
binding.headerView.hide()
|
||||
dst = 0
|
||||
} else if (dst < -20) {
|
||||
binding.headerView.show()
|
||||
dst = 0
|
||||
}
|
||||
}
|
||||
|
||||
var hiding = false
|
||||
fun View.hide() {
|
||||
val target = -height.toFloat()
|
||||
if (hiding || translationY == target) return
|
||||
animate()
|
||||
.translationY(target)
|
||||
.setInterpolator(AccelerateInterpolator(2F))
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
hiding = false
|
||||
}
|
||||
})
|
||||
hiding = true
|
||||
}
|
||||
|
||||
var showing = false
|
||||
fun View.show() {
|
||||
val target = 0f
|
||||
if (showing || translationY == target) return
|
||||
animate()
|
||||
.translationY(target)
|
||||
.setInterpolator(DecelerateInterpolator(2F))
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
showing = false
|
||||
}
|
||||
})
|
||||
showing = true
|
||||
}
|
||||
var hiding = false
|
||||
fun View.hide() {
|
||||
val target = -height.toFloat()
|
||||
if (hiding || translationY == target) return
|
||||
animate()
|
||||
.translationY(target)
|
||||
.setInterpolator(AccelerateInterpolator(2F))
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
hiding = false
|
||||
}
|
||||
})
|
||||
hiding = true
|
||||
}
|
||||
|
||||
var showing = false
|
||||
fun View.show() {
|
||||
val target = 0f
|
||||
if (showing || translationY == target) return
|
||||
animate()
|
||||
.translationY(target)
|
||||
.setInterpolator(DecelerateInterpolator(2F))
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
showing = false
|
||||
}
|
||||
})
|
||||
showing = true
|
||||
}
|
||||
})
|
||||
***/
|
||||
|
||||
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 ->
|
||||
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)
|
||||
|
||||
binding.etSearch.setOnEditorActionListener { v, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
//hide
|
||||
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
/***
|
||||
et_search.setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
//hide
|
||||
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
|
||||
val key = v.text.toString().uppercase()
|
||||
val apps = ArrayList<AppInfo>()
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
appsAll?.forEach {
|
||||
apps.add(it)
|
||||
}
|
||||
} else {
|
||||
appsAll?.forEach {
|
||||
if (it.appName.uppercase().indexOf(key) >= 0) {
|
||||
apps.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
||||
binding.recyclerView.adapter = adapter
|
||||
adapter?.notifyDataSetChanged()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val key = v.text.toString().toUpperCase()
|
||||
val apps = ArrayList<AppInfo>()
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
appsAll?.forEach {
|
||||
apps.add(it)
|
||||
}
|
||||
} else {
|
||||
appsAll?.forEach {
|
||||
if (it.appName.toUpperCase().indexOf(key) >= 0) {
|
||||
apps.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
||||
recycler_view.adapter = adapter
|
||||
adapter?.notifyDataSetChanged()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
***/
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
adapter?.let {
|
||||
defaultSharedPreferences.edit().putStringSet(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist).apply()
|
||||
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_bypass_list, menu)
|
||||
|
||||
val searchItem = menu.findItem(R.id.search_view)
|
||||
if (searchItem != null) {
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
filterProxyApp(newText?:"")
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
@@ -199,21 +209,27 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
if (it.blacklist.containsAll(pkgNames)) {
|
||||
it.apps.forEach {
|
||||
val packageName = it.packageName
|
||||
adapter?.blacklist!!.remove(packageName)
|
||||
adapter?.blacklist?.remove(packageName)
|
||||
}
|
||||
} else {
|
||||
it.apps.forEach {
|
||||
val packageName = it.packageName
|
||||
adapter?.blacklist!!.add(packageName)
|
||||
adapter?.blacklist?.add(packageName)
|
||||
}
|
||||
|
||||
}
|
||||
it.notifyDataSetChanged()
|
||||
true
|
||||
} ?: false
|
||||
R.id.select_proxy_app -> {
|
||||
selectProxyApp()
|
||||
|
||||
true
|
||||
}
|
||||
R.id.import_proxy_app -> {
|
||||
importProxyApp()
|
||||
true
|
||||
}
|
||||
R.id.export_proxy_app -> {
|
||||
exportProxyApp()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
@@ -222,41 +238,55 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
private fun selectProxyApp() {
|
||||
toast(R.string.msg_downloading_content)
|
||||
val url = AppConfig.androidpackagenamelistUrl
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val content = try {
|
||||
URL(url).readText()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val content = Utils.getUrlContext(url, 5000)
|
||||
launch(Dispatchers.Main) {
|
||||
Log.d(ANG_PACKAGE, content)
|
||||
selectProxyApp(content)
|
||||
selectProxyApp(content, true)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectProxyApp(content: String): Boolean {
|
||||
private fun importProxyApp() {
|
||||
val content = Utils.getClipboard(applicationContext)
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
return
|
||||
}
|
||||
selectProxyApp(content, false)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
|
||||
private fun exportProxyApp() {
|
||||
var lst = binding.switchBypassApps.isChecked.toString()
|
||||
|
||||
adapter?.blacklist?.forEach block@{
|
||||
lst = lst + System.getProperty("line.separator") + it
|
||||
}
|
||||
Utils.setClipboard(applicationContext, lst)
|
||||
toast(R.string.toast_success)
|
||||
}
|
||||
|
||||
private fun selectProxyApp(content: String, force: Boolean): Boolean {
|
||||
try {
|
||||
var proxyApps = content
|
||||
if (TextUtils.isEmpty(content)) {
|
||||
val assets = Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
||||
proxyApps = assets.lines().toString()
|
||||
val proxyApps = if (TextUtils.isEmpty(content)) {
|
||||
Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
||||
} else {
|
||||
content
|
||||
}
|
||||
if (TextUtils.isEmpty(proxyApps)) {
|
||||
return false
|
||||
}
|
||||
|
||||
adapter?.blacklist!!.clear()
|
||||
adapter?.blacklist?.clear()
|
||||
|
||||
if (binding.switchBypassApps.isChecked) {
|
||||
adapter?.let {
|
||||
it.apps.forEach block@{
|
||||
val packageName = it.packageName
|
||||
Log.d(ANG_PACKAGE, packageName)
|
||||
if (proxyApps.indexOf(packageName) < 0) {
|
||||
adapter?.blacklist!!.add(packageName)
|
||||
if (!inProxyApps(proxyApps, packageName, force)) {
|
||||
adapter?.blacklist?.add(packageName)
|
||||
println(packageName)
|
||||
return@block
|
||||
}
|
||||
@@ -268,8 +298,8 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
it.apps.forEach block@{
|
||||
val packageName = it.packageName
|
||||
Log.d(ANG_PACKAGE, packageName)
|
||||
if (proxyApps.indexOf(packageName) >= 0) {
|
||||
adapter?.blacklist!!.add(packageName)
|
||||
if (inProxyApps(proxyApps, packageName, force)) {
|
||||
adapter?.blacklist?.add(packageName)
|
||||
println(packageName)
|
||||
return@block
|
||||
}
|
||||
@@ -283,4 +313,40 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean {
|
||||
if (force) {
|
||||
if (packageName == "com.google.android.webview") {
|
||||
return false
|
||||
}
|
||||
if (packageName.startsWith("com.google")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return proxyApps.indexOf(packageName) >= 0
|
||||
}
|
||||
|
||||
private fun filterProxyApp(content: String): Boolean {
|
||||
val apps = ArrayList<AppInfo>()
|
||||
|
||||
val key = content.uppercase()
|
||||
if (key.isNotEmpty()) {
|
||||
appsAll?.forEach {
|
||||
if (it.appName.uppercase().indexOf(key) >= 0
|
||||
|| it.packageName.uppercase().indexOf(key) >= 0) {
|
||||
apps.add(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appsAll?.forEach {
|
||||
apps.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
||||
binding.recyclerView.adapter = adapter
|
||||
adapter?.notifyDataSetChanged()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.v2ray.ang.R
|
||||
@@ -36,7 +35,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
||||
VIEW_TYPE_HEADER -> {
|
||||
val view = View(ctx)
|
||||
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 3)
|
||||
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0)
|
||||
BaseViewHolder(view)
|
||||
}
|
||||
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
|
||||
@@ -66,10 +65,10 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
||||
itemBypassBinding.packageName.text = appInfo.packageName
|
||||
if (appInfo.isSystemApp) {
|
||||
itemBypassBinding.name.text = String.format("** %1s", appInfo.appName)
|
||||
itemBypassBinding.name.setTextColor(Color.RED)
|
||||
//name.textColor = Color.RED
|
||||
} else {
|
||||
itemBypassBinding.name.text = appInfo.appName
|
||||
itemBypassBinding.name.setTextColor(Color.DKGRAY)
|
||||
//name.textColor = Color.DKGRAY
|
||||
}
|
||||
|
||||
itemView.setOnClickListener(this)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import com.v2ray.ang.R
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -21,8 +20,7 @@ class RoutingSettingsActivity : BaseActivity() {
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
title = getString(R.string.routing_settings_title)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
title = getString(R.string.title_pref_routing_custom)
|
||||
|
||||
val fragments = ArrayList<Fragment>()
|
||||
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT))
|
||||
@@ -31,7 +29,7 @@ class RoutingSettingsActivity : BaseActivity() {
|
||||
|
||||
val adapter = FragmentAdapter(this, fragments)
|
||||
binding.viewpager.adapter = adapter
|
||||
binding.tablayout.setTabTextColors(Color.BLACK, Color.RED)
|
||||
//tablayout.setTabTextColors(Color.BLACK, Color.RED)
|
||||
TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position ->
|
||||
tab.text = titles[position]
|
||||
}.attach()
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity.RESULT_OK
|
||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import android.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_routing_settings.*
|
||||
import android.view.MenuInflater
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.v2RayApplication
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URL
|
||||
|
||||
class RoutingSettingsFragment : Fragment() {
|
||||
private lateinit var binding: FragmentRoutingSettingsBinding
|
||||
@@ -29,7 +28,7 @@ class RoutingSettingsFragment : Fragment() {
|
||||
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?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
@@ -49,8 +48,8 @@ class RoutingSettingsFragment : Fragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val content = defaultSharedPreferences.getString(requireArguments().getString(routing_arg), "")
|
||||
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
||||
val content = settingsStorage?.getString(requireArguments().getString(routing_arg), "")
|
||||
binding.etRoutingContent.text = Utils.getEditable(content)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
@@ -62,9 +61,7 @@ class RoutingSettingsFragment : Fragment() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.save_routing -> {
|
||||
val content = et_routing_content.text.toString()
|
||||
defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply()
|
||||
activity?.toast(R.string.toast_success)
|
||||
saveRouting()
|
||||
true
|
||||
}
|
||||
R.id.del_routing -> {
|
||||
@@ -88,7 +85,7 @@ class RoutingSettingsFragment : Fragment() {
|
||||
|
||||
private fun saveRouting() {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -116,7 +113,7 @@ class RoutingSettingsFragment : Fragment() {
|
||||
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val content = it.data?.getStringExtra("SCAN_RESULT")
|
||||
binding.etRoutingContent.text = Utils.getEditable(content!!)
|
||||
binding.etRoutingContent.text = Utils.getEditable(content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,29 +126,26 @@ class RoutingSettingsFragment : Fragment() {
|
||||
|
||||
fun setDefaultRules(): Boolean {
|
||||
var url = AppConfig.v2rayCustomRoutingListUrl
|
||||
var tag = ""
|
||||
when (requireArguments().getString(routing_arg)) {
|
||||
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
||||
url += AppConfig.TAG_AGENT
|
||||
tag = AppConfig.TAG_PROXY
|
||||
}
|
||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
||||
url += AppConfig.TAG_DIRECT
|
||||
tag = AppConfig.TAG_DIRECT
|
||||
}
|
||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
||||
url += AppConfig.TAG_BLOCKED
|
||||
tag = AppConfig.TAG_BLOCKED
|
||||
}
|
||||
}
|
||||
url += tag
|
||||
|
||||
activity?.toast(R.string.msg_downloading_content)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val content = try {
|
||||
URL(url).readText()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val content = Utils.getUrlContext(url, 5000)
|
||||
launch(Dispatchers.Main) {
|
||||
val routingList = if (TextUtils.isEmpty(content)) {
|
||||
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
|
||||
Utils.readTextFromAssets(activity?.v2RayApplication, "custom_routing_$tag")
|
||||
} else {
|
||||
content
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.*
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
|
||||
class ScScannerActivity : BaseActivity() {
|
||||
|
||||
@@ -32,8 +32,8 @@ class ScScannerActivity : BaseActivity() {
|
||||
|
||||
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "")
|
||||
if (count > 0) {
|
||||
val (count, countSub) = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
|
||||
if (count + countSub > 0) {
|
||||
toast(R.string.toast_success)
|
||||
} else {
|
||||
toast(R.string.toast_failure)
|
||||
|
||||
@@ -1,65 +1,60 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import com.google.zxing.Result
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view
|
||||
|
||||
mScannerView?.setAutoFocus(true)
|
||||
val formats = ArrayList<BarcodeFormat>()
|
||||
formats.add(BarcodeFormat.QR_CODE)
|
||||
mScannerView?.setFormats(formats)
|
||||
|
||||
setContentView(mScannerView) // Set the scanner view as the content view
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
|
||||
launchScan()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
mScannerView!!.setResultHandler(this) // Register ourselves as a handler for scan results.
|
||||
mScannerView!!.startCamera() // Start camera on resume
|
||||
private fun launchScan(){
|
||||
scanQrCode.launch(
|
||||
ScannerConfig.build {
|
||||
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() {
|
||||
super.onPause()
|
||||
mScannerView!!.stopCamera() // Stop camera on pause
|
||||
}
|
||||
|
||||
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 handleResult(result: QRResult) {
|
||||
if (result is QRResult.QRSuccess ) {
|
||||
finished(result.content.rawValue?:"")
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun finished(text: String) {
|
||||
val intent = Intent()
|
||||
intent.putExtra("SCAN_RESULT", text)
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
setResult(AppCompatActivity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -69,19 +64,28 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.scan_code -> {
|
||||
launchScan()
|
||||
true
|
||||
}
|
||||
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)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.subscribe {
|
||||
if (it) {
|
||||
try {
|
||||
showFileChooser()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else
|
||||
toast(R.string.toast_permission_denied)
|
||||
}
|
||||
.request(permission)
|
||||
.subscribe {
|
||||
if (it) {
|
||||
try {
|
||||
showFileChooser()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else
|
||||
toast(R.string.toast_permission_denied)
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
@@ -106,7 +110,7 @@ class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler {
|
||||
try {
|
||||
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||
finished(text)
|
||||
finished(text?:"")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast(e.message.toString())
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
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 com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
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.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.XTLS
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
|
||||
import com.v2ray.ang.extension.removeWhiteSpace
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.MmkvManager.ID_MAIN
|
||||
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.android.synthetic.main.activity_server_socks.*
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.*
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.et_address
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.et_id
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.et_path
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.et_port
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.et_remarks
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.et_request_host
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.sp_allow_insecure
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.sp_header_type
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.sp_header_type_title
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.sp_network
|
||||
import kotlinx.android.synthetic.main.activity_server_vmess.sp_stream_security
|
||||
import com.v2ray.ang.util.Utils.getIpv6Address
|
||||
|
||||
class ServerActivity : BaseActivity() {
|
||||
|
||||
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 isRunning by lazy {
|
||||
intent.getBooleanExtra("isRunning", false)
|
||||
@@ -46,8 +48,13 @@ class ServerActivity : BaseActivity() {
|
||||
&& editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER)
|
||||
}
|
||||
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 {
|
||||
intent.getStringExtra("subscriptionId")
|
||||
}
|
||||
|
||||
private val securitys: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.securitys)
|
||||
}
|
||||
@@ -75,37 +82,159 @@ class ServerActivity : BaseActivity() {
|
||||
private val allowinsecures: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.allowinsecures)
|
||||
}
|
||||
private val uTlsItems: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.streamsecurity_utls)
|
||||
}
|
||||
private val alpns: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.streamsecurity_alpn)
|
||||
}
|
||||
|
||||
// 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
|
||||
// protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic.
|
||||
private val et_remarks: EditText by lazy { findViewById(R.id.et_remarks) }
|
||||
private val et_address: EditText by lazy { findViewById(R.id.et_address) }
|
||||
private val et_port: EditText by lazy { findViewById(R.id.et_port) }
|
||||
private val et_id: EditText by lazy { findViewById(R.id.et_id) }
|
||||
private val et_alterId: EditText? by lazy { findViewById(R.id.et_alterId) }
|
||||
private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
|
||||
private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
|
||||
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_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 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 container_fingerprint: LinearLayout? by lazy { findViewById(R.id.l3) }
|
||||
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_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 tv_path: TextView? by lazy { findViewById(R.id.tv_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 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
title = getString(R.string.title_server)
|
||||
|
||||
val config = MmkvManager.decodeServerConfig(editGuid)
|
||||
when(config?.configType ?: createConfigType) {
|
||||
when (config?.configType ?: createConfigType) {
|
||||
EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess)
|
||||
EConfigType.CUSTOM -> return
|
||||
EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
|
||||
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
|
||||
// EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
|
||||
// EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
|
||||
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
|
||||
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
|
||||
EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard)
|
||||
}
|
||||
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])
|
||||
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)
|
||||
sp_header_type?.adapter = adapter
|
||||
sp_header_type_title?.text = if (networks[position] == "grpc")
|
||||
getString(R.string.server_lab_mode_type) else
|
||||
getString(R.string.server_lab_head_type)
|
||||
config?.getProxyOutbound()?.getTransportSettingDetails()?.let { transportDetails ->
|
||||
sp_header_type.setSelection(Utils.arrayFind(types, transportDetails[0]))
|
||||
et_request_host.text = Utils.getEditable(transportDetails[1])
|
||||
et_path.text = Utils.getEditable(transportDetails[2])
|
||||
sp_header_type?.setSelection(Utils.arrayFind(types, transportDetails[0]))
|
||||
et_request_host?.text = Utils.getEditable(transportDetails[1])
|
||||
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<*>?) {
|
||||
// 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(parent: AdapterView<*>?) {
|
||||
|
||||
override fun onNothingSelected(p0: AdapterView<*>?) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -114,44 +243,117 @@ class ServerActivity : BaseActivity() {
|
||||
} else {
|
||||
clearServer()
|
||||
}
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* bingding seleced server config
|
||||
* binding selected server config
|
||||
*/
|
||||
private fun bindingServer(config: ServerConfig): Boolean {
|
||||
val outbound = config.getProxyOutbound() ?: return false
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return false
|
||||
|
||||
et_remarks.text = Utils.getEditable(config.remarks)
|
||||
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_alterId?.text =
|
||||
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
|
||||
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) {
|
||||
et_security.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
|
||||
val flow = Utils.arrayFind(flows, outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty())
|
||||
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
|
||||
val flow = Utils.arrayFind(
|
||||
flows,
|
||||
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty()
|
||||
)
|
||||
if (flow >= 0) {
|
||||
//sp_flow.setSelection(flow)
|
||||
sp_flow?.setSelection(flow)
|
||||
}
|
||||
} else if (config.configType == EConfigType.WIREGUARD) {
|
||||
et_public_key?.text =
|
||||
Utils.getEditable(outbound.settings?.peers?.get(0)?.publicKey.orEmpty())
|
||||
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 security = Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
|
||||
val securityEncryptions =
|
||||
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||
val security =
|
||||
Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
|
||||
if (security >= 0) {
|
||||
sp_security?.setSelection(security)
|
||||
}
|
||||
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return true
|
||||
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
|
||||
if (streamSecurity >= 0) {
|
||||
sp_stream_security?.setSelection(streamSecurity)
|
||||
(streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting ->
|
||||
val allowinsecure = Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
|
||||
if (allowinsecure >= 0) {
|
||||
sp_allow_insecure?.setSelection(allowinsecure)
|
||||
(streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting ->
|
||||
container_sni?.visibility = View.VISIBLE
|
||||
container_fingerprint?.visibility = View.VISIBLE
|
||||
container_alpn?.visibility = View.VISIBLE
|
||||
et_sni?.text = Utils.getEditable(tlsSetting.serverName)
|
||||
tlsSetting.fingerprint?.let {
|
||||
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint)
|
||||
sp_stream_fingerprint?.setSelection(utlsIndex)
|
||||
}
|
||||
et_request_host.text = Utils.getEditable(tlsSetting.serverName)
|
||||
tlsSetting.alpn?.let {
|
||||
val alpnIndex = Utils.arrayFind(
|
||||
alpns,
|
||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())?:""
|
||||
)
|
||||
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)
|
||||
@@ -169,6 +371,7 @@ class ServerActivity : BaseActivity() {
|
||||
et_address.text = null
|
||||
et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
|
||||
et_id.text = null
|
||||
et_alterId?.text = Utils.getEditable("0")
|
||||
sp_security?.setSelection(0)
|
||||
sp_network?.setSelection(0)
|
||||
|
||||
@@ -177,9 +380,17 @@ class ServerActivity : BaseActivity() {
|
||||
et_path?.text = null
|
||||
sp_stream_security?.setSelection(0)
|
||||
sp_allow_insecure?.setSelection(0)
|
||||
et_sni?.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
|
||||
}
|
||||
|
||||
@@ -200,11 +411,29 @@ class ServerActivity : BaseActivity() {
|
||||
toast(R.string.server_lab_port)
|
||||
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())) {
|
||||
toast(R.string.server_lab_id)
|
||||
if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.SHADOWSOCKS) {
|
||||
toast(R.string.server_lab_id3)
|
||||
} else {
|
||||
toast(R.string.server_lab_id)
|
||||
}
|
||||
return false
|
||||
}
|
||||
sp_stream_security?.let {
|
||||
if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) {
|
||||
toast(R.string.server_lab_stream_security)
|
||||
return false
|
||||
}
|
||||
}
|
||||
et_alterId?.let {
|
||||
val alterId = Utils.parseInt(it.text.toString())
|
||||
if (alterId < 0) {
|
||||
toast(R.string.server_lab_alterid)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
config.remarks = et_remarks.text.toString().trim()
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
@@ -213,8 +442,15 @@ class ServerActivity : BaseActivity() {
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
saveServers(server, port, config)
|
||||
}
|
||||
val wireguard = config.outboundBean?.settings
|
||||
wireguard?.peers?.get(0)?.let { _ ->
|
||||
savePeer(wireguard, port)
|
||||
}
|
||||
config.outboundBean?.streamSettings?.let {
|
||||
saveStreamSettings(it, config)
|
||||
saveStreamSettings(it)
|
||||
}
|
||||
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||
config.subscriptionId = subscriptionId?:""
|
||||
}
|
||||
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
@@ -223,34 +459,40 @@ class ServerActivity : BaseActivity() {
|
||||
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.port = port
|
||||
vnext.users[0].id = et_id.text.toString().trim()
|
||||
if (config.configType == EConfigType.VMESS) {
|
||||
vnext.users[0].security = securitys[sp_security.selectedItemPosition]
|
||||
vnext.users[0].alterId = Utils.parseInt(et_alterId?.text.toString())
|
||||
vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0]
|
||||
} else if (config.configType == EConfigType.VLESS) {
|
||||
vnext.users[0].encryption = et_security.text.toString().trim()
|
||||
if (streamSecuritys[sp_stream_security.selectedItemPosition] == XTLS) {
|
||||
// vnext.users[0].flow = flows[sp_flow.selectedItemPosition].ifBlank { V2rayConfig.DEFAULT_FLOW }
|
||||
} else {
|
||||
vnext.users[0].flow = ""
|
||||
}
|
||||
vnext.users[0].encryption = et_security?.text.toString().trim()
|
||||
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0]
|
||||
}
|
||||
}
|
||||
|
||||
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.port = port
|
||||
if (config.configType == EConfigType.SHADOWSOCKS) {
|
||||
server.password = et_id.text.toString().trim()
|
||||
server.method = shadowsocksSecuritys[sp_security.selectedItemPosition]
|
||||
server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
|
||||
} else if (config.configType == EConfigType.SOCKS) {
|
||||
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
|
||||
} else {
|
||||
val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = et_security.text.toString().trim()
|
||||
val socksUsersBean =
|
||||
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = et_security?.text.toString().trim()
|
||||
socksUsersBean.pass = et_id.text.toString().trim()
|
||||
server.users = listOf(socksUsersBean)
|
||||
}
|
||||
@@ -259,44 +501,87 @@ class ServerActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean, config: ServerConfig) {
|
||||
val network = if (sp_network != null) networks[sp_network.selectedItemPosition] else DEFAULT_NETWORK
|
||||
val type = if (sp_header_type != null) transportTypes(network)[sp_header_type.selectedItemPosition] else ""
|
||||
val requestHost = if (et_request_host != null) et_request_host.text.toString().trim() else ""
|
||||
val path = if (et_path != null) et_path.text.toString().trim() else ""
|
||||
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 {
|
||||
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) {
|
||||
val network = sp_network?.selectedItemPosition ?: return
|
||||
val type = sp_header_type?.selectedItemPosition ?: return
|
||||
val requestHost = et_request_host?.text?.toString()?.trim() ?: return
|
||||
val path = et_path?.text?.toString()?.trim() ?: return
|
||||
val sniField = et_sni?.text?.toString()?.trim() ?: return
|
||||
val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
|
||||
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
||||
val utlsIndex = sp_stream_fingerprint?.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(
|
||||
transport = network,
|
||||
headerType = type,
|
||||
transport = networks[network],
|
||||
headerType = transportTypes(networks[network])[type],
|
||||
host = requestHost,
|
||||
path = path,
|
||||
seed = path,
|
||||
quicSecurity = requestHost,
|
||||
key = path,
|
||||
mode = type,
|
||||
serviceName = path
|
||||
mode = transportTypes(networks[network])[type],
|
||||
serviceName = path,
|
||||
authority = requestHost,
|
||||
)
|
||||
val allowInsecure = if (sp_allow_insecure == null || allowinsecures[sp_allow_insecure.selectedItemPosition].isBlank()) {
|
||||
false//settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
|
||||
} else {
|
||||
allowinsecures[sp_allow_insecure.selectedItemPosition].toBoolean()
|
||||
if (sniField.isNotBlank()) {
|
||||
sni = sniField
|
||||
}
|
||||
val defaultTls = if (config.configType == EConfigType.TROJAN) V2rayConfig.TLS else ""
|
||||
val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) {
|
||||
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
|
||||
} else {
|
||||
allowinsecures[allowInsecureField].toBoolean()
|
||||
}
|
||||
|
||||
streamSetting.populateTlsSettings(
|
||||
if (sp_stream_security != null) streamSecuritys[sp_stream_security.selectedItemPosition] else defaultTls,
|
||||
allowInsecure,
|
||||
sni
|
||||
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> {
|
||||
return if (network == "tcp") {
|
||||
tcpTypes
|
||||
} else if (network == "kcp" || network == "quic") {
|
||||
kcpAndQuicTypes
|
||||
} else if (network == "grpc") {
|
||||
grpcModes
|
||||
} else {
|
||||
arrayOf("---")
|
||||
return when (network) {
|
||||
"tcp" -> {
|
||||
tcpTypes
|
||||
}
|
||||
|
||||
"kcp", "quic" -> {
|
||||
kcpAndQuicTypes
|
||||
}
|
||||
|
||||
"grpc" -> {
|
||||
grpcModes
|
||||
}
|
||||
|
||||
else -> {
|
||||
arrayOf("---")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,12 +590,24 @@ class ServerActivity : BaseActivity() {
|
||||
*/
|
||||
private fun deleteServer(): Boolean {
|
||||
if (editGuid.isNotEmpty()) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeServer(editGuid)
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
if (editGuid != mainStorage?.decodeString(KEY_SELECTED_SERVER)) {
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
MmkvManager.removeServer(editGuid)
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||
// do nothing
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
MmkvManager.removeServer(editGuid)
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
application.toast(R.string.toast_action_not_allowed)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -337,10 +634,12 @@ class ServerActivity : BaseActivity() {
|
||||
deleteServer()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.save_config -> {
|
||||
saveServer()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.blacksquircle.ui.editorkit.utils.EditorTheme
|
||||
import com.blacksquircle.ui.language.json.JsonLanguage
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.*
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
|
||||
@@ -38,6 +39,9 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
setContentView(view)
|
||||
title = getString(R.string.title_server)
|
||||
|
||||
if (!Utils.getDarkModeStatus(this)) {
|
||||
binding.editor.colorScheme = EditorTheme.INTELLIJ_LIGHT
|
||||
}
|
||||
binding.editor.language = JsonLanguage()
|
||||
val config = MmkvManager.decodeServerConfig(editGuid)
|
||||
if (config != null) {
|
||||
@@ -45,7 +49,6 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
} else {
|
||||
clearServer()
|
||||
}
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +91,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
MmkvManager.encodeServerConfig(editGuid, config)
|
||||
@@ -108,6 +111,9 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||
MmkvManager.removeServer(editGuid)
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||
// do nothing
|
||||
}
|
||||
.show()
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -2,16 +2,26 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.preference.*
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import com.v2ray.ang.R
|
||||
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.extension.toast
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
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.viewmodel.SettingsViewModel
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
@@ -22,217 +32,254 @@ class SettingsActivity : BaseActivity() {
|
||||
|
||||
title = getString(R.string.title_settings)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
settingsViewModel.startListenPreferenceChange()
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
private val perAppProxy by lazy { findPreference(AppConfig.PREF_PER_APP_PROXY) as CheckBoxPreference }
|
||||
private val localDns by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
||||
private val fakeDns by lazy { findPreference(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||
private val localDnsPort by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||
private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) }
|
||||
private val sppedEnabled by lazy { findPreference(AppConfig.PREF_SPEED_ENABLED) as CheckBoxPreference }
|
||||
private val sniffingEnabled by lazy { findPreference(AppConfig.PREF_SNIFFING_ENABLED) as CheckBoxPreference }
|
||||
private val proxySharing by lazy { findPreference(AppConfig.PREF_PROXY_SHARING) as CheckBoxPreference }
|
||||
private val domainStrategy by lazy { findPreference(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) as ListPreference }
|
||||
private val routingMode by lazy { findPreference(AppConfig.PREF_ROUTING_MODE) as ListPreference }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
private val forwardIpv6 by lazy { findPreference(AppConfig.PREF_FORWARD_IPV6) as CheckBoxPreference }
|
||||
private val enableLocalDns by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_ENABLED) as CheckBoxPreference }
|
||||
private val domesticDns by lazy { findPreference(AppConfig.PREF_DOMESTIC_DNS) as EditTextPreference }
|
||||
private val remoteDns by lazy { findPreference(AppConfig.PREF_REMOTE_DNS) as EditTextPreference }
|
||||
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 fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
||||
|
||||
// val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference }
|
||||
private val routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
|
||||
|
||||
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) }
|
||||
|
||||
// val socksPort by lazy { findPreference(PREF_SOCKS_PORT) as EditTextPreference }
|
||||
// val httpPort by lazy { findPreference(PREF_HTTP_PORT) as EditTextPreference }
|
||||
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 routingCustom: Preference by lazy { findPreference(AppConfig.PREF_ROUTING_CUSTOM) }
|
||||
// val donate: Preference by lazy { findPreference(PREF_DONATE) }
|
||||
// val licenses: Preference by lazy { findPreference(PREF_LICENSES) }
|
||||
// val feedback: Preference by lazy { findPreference(PREF_FEEDBACK) }
|
||||
// val tgGroup: Preference by lazy { findPreference(PREF_TG_GROUP) }
|
||||
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 mode by lazy { findPreference(AppConfig.PREF_MODE) as ListPreference }
|
||||
|
||||
private fun restartProxy() {
|
||||
Utils.stopVService(requireContext())
|
||||
V2RayServiceManager.startV2Ray(requireContext())
|
||||
}
|
||||
|
||||
private fun isRunning(): Boolean {
|
||||
return false //TODO no point of adding logic now since Settings will be changed soon
|
||||
}
|
||||
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
||||
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
|
||||
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
||||
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
|
||||
private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
|
||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||
|
||||
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_settings)
|
||||
|
||||
perAppProxy.setOnPreferenceClickListener {
|
||||
if (isRunning()) {
|
||||
Utils.stopVService(requireContext())
|
||||
}
|
||||
perAppProxy?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(activity, PerAppProxyActivity::class.java))
|
||||
perAppProxy.isChecked = true
|
||||
true
|
||||
}
|
||||
sppedEnabled.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
sniffingEnabled.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
proxySharing.setOnPreferenceClickListener {
|
||||
if (proxySharing.isChecked)
|
||||
activity?.toast(R.string.toast_warning_pref_proxysharing)
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
domainStrategy.setOnPreferenceChangeListener { _, _ ->
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
routingMode.setOnPreferenceChangeListener { _, _ ->
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
routingCustom.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
Utils.stopVService(requireContext())
|
||||
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
|
||||
perAppProxy?.isChecked = true
|
||||
false
|
||||
}
|
||||
|
||||
forwardIpv6.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
enableLocalDns.setOnPreferenceClickListener {
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
domesticDns.setOnPreferenceChangeListener { _, any ->
|
||||
// domesticDns.summary = any as String
|
||||
val nval = any as String
|
||||
domesticDns.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
|
||||
remoteDns.setOnPreferenceChangeListener { _, any ->
|
||||
// remoteDns.summary = any as String
|
||||
val nval = any as String
|
||||
remoteDns.summary = if (nval == "") AppConfig.DNS_AGENT else nval
|
||||
if (isRunning())
|
||||
restartProxy()
|
||||
true
|
||||
}
|
||||
localDns?.setOnPreferenceChangeListener{ _, any ->
|
||||
localDns?.setOnPreferenceChangeListener { _, any ->
|
||||
updateLocalDns(any as Boolean)
|
||||
true
|
||||
}
|
||||
localDnsPort?.setOnPreferenceChangeListener { _, any ->
|
||||
val nval = any as String
|
||||
localDnsPort?.summary = if (TextUtils.isEmpty(nval)) "10807" else nval
|
||||
localDnsPort?.summary =
|
||||
if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
|
||||
true
|
||||
}
|
||||
vpnDns?.setOnPreferenceChangeListener { _, any ->
|
||||
vpnDns?.summary = any as String
|
||||
true
|
||||
}
|
||||
mode.setOnPreferenceChangeListener { _, newValue ->
|
||||
|
||||
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 ->
|
||||
val nval = any as String
|
||||
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
|
||||
true
|
||||
}
|
||||
httpPort?.setOnPreferenceChangeListener { _, any ->
|
||||
val nval = any as String
|
||||
httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval
|
||||
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 ->
|
||||
updateMode(newValue.toString())
|
||||
true
|
||||
}
|
||||
mode.dialogLayoutResource = R.layout.preference_with_help_link
|
||||
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
||||
//loglevel.summary = "LogLevel"
|
||||
|
||||
// donate.onClick {
|
||||
// startActivity<InappBuyActivity>()
|
||||
// }
|
||||
|
||||
// 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)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// socksPort.setOnPreferenceChangeListener { preference, any ->
|
||||
// socksPort.summary = any as String
|
||||
// true
|
||||
// }
|
||||
// httpPort.setOnPreferenceChangeListener { preference, any ->
|
||||
// httpPort.summary = any as String
|
||||
// true
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
||||
updateMode(defaultSharedPreferences.getString(AppConfig.PREF_MODE, "VPN"))
|
||||
var remoteDnsString = defaultSharedPreferences.getString(AppConfig.PREF_REMOTE_DNS, "")
|
||||
domesticDns.summary = defaultSharedPreferences.getString(AppConfig.PREF_DOMESTIC_DNS, "")
|
||||
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, "VPN"))
|
||||
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||
fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
||||
localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
||||
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
||||
|
||||
if (TextUtils.isEmpty(remoteDnsString)) {
|
||||
remoteDnsString = AppConfig.DNS_AGENT
|
||||
}
|
||||
if ( domesticDns.summary == "") {
|
||||
domesticDns.summary = AppConfig.DNS_DIRECT
|
||||
}
|
||||
remoteDns.summary = remoteDnsString
|
||||
vpnDns?.summary = defaultSharedPreferences.getString(AppConfig.PREF_VPN_DNS, remoteDnsString)
|
||||
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false))
|
||||
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)
|
||||
muxConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8")
|
||||
muxXudpConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
||||
|
||||
// socksPort.summary = defaultSharedPreferences.getString(PREF_SOCKS_PORT, "10808")
|
||||
// lanconnPort.summary = defaultSharedPreferences.getString(PREF_HTTP_PORT, "")
|
||||
updateFragment(settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false))
|
||||
fragment?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false)
|
||||
fragmentPackets?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
|
||||
fragmentLength?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
||||
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
||||
|
||||
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||
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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMode(mode: String?) {
|
||||
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
||||
val vpn = mode == "VPN"
|
||||
perAppProxy.isEnabled = vpn
|
||||
perAppProxy.isChecked = PreferenceManager.getDefaultSharedPreferences(requireActivity())
|
||||
.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
||||
perAppProxy?.isEnabled = vpn
|
||||
perAppProxy?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
||||
localDns?.isEnabled = vpn
|
||||
fakeDns?.isEnabled = vpn
|
||||
localDnsPort?.isEnabled = vpn
|
||||
vpnDns?.isEnabled = vpn
|
||||
if (vpn) {
|
||||
updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false))
|
||||
updateLocalDns(
|
||||
settingsStorage.getBoolean(
|
||||
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +288,77 @@ class SettingsActivity : BaseActivity() {
|
||||
localDnsPort?.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) {
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
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.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.service.SubscriptionUpdater
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SubEditActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySubEditBinding
|
||||
@@ -36,7 +44,6 @@ class SubEditActivity : BaseActivity() {
|
||||
} else {
|
||||
clearServer()
|
||||
}
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +52,8 @@ class SubEditActivity : BaseActivity() {
|
||||
private fun bindingServer(subItem: SubscriptionItem): Boolean {
|
||||
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
||||
binding.etUrl.text = Utils.getEditable(subItem.url)
|
||||
|
||||
binding.chkEnable.isChecked = subItem.enabled
|
||||
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -55,7 +63,7 @@ class SubEditActivity : BaseActivity() {
|
||||
private fun clearServer(): Boolean {
|
||||
binding.etRemarks.text = null
|
||||
binding.etUrl.text = null
|
||||
|
||||
binding.chkEnable.isChecked = true
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -75,15 +83,17 @@ class SubEditActivity : BaseActivity() {
|
||||
|
||||
subItem.remarks = binding.etRemarks.text.toString()
|
||||
subItem.url = binding.etUrl.text.toString()
|
||||
subItem.enabled = binding.chkEnable.isChecked
|
||||
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
||||
|
||||
if (TextUtils.isEmpty(subItem.remarks)) {
|
||||
toast(R.string.sub_setting_remarks)
|
||||
return false
|
||||
}
|
||||
if (TextUtils.isEmpty(subItem.url)) {
|
||||
toast(R.string.sub_setting_url)
|
||||
return false
|
||||
}
|
||||
// if (TextUtils.isEmpty(subItem.url)) {
|
||||
// toast(R.string.sub_setting_url)
|
||||
// return false
|
||||
// }
|
||||
|
||||
subStorage?.encode(subId, Gson().toJson(subItem))
|
||||
toast(R.string.toast_success)
|
||||
@@ -101,6 +111,9 @@ class SubEditActivity : BaseActivity() {
|
||||
MmkvManager.removeSubscription(editSubId)
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) {_, _ ->
|
||||
// do nothing
|
||||
}
|
||||
.show()
|
||||
}
|
||||
return true
|
||||
@@ -129,4 +142,5 @@ class SubEditActivity : BaseActivity() {
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.v2ray.ang.R
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||
import com.v2ray.ang.databinding.LayoutProgressBinding
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SubSettingActivity : BaseActivity() {
|
||||
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) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -27,8 +35,6 @@ class SubSettingActivity : BaseActivity() {
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -39,9 +45,6 @@ class SubSettingActivity : BaseActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -50,6 +53,30 @@ class SubSettingActivity : BaseActivity() {
|
||||
startActivity(Intent(this, SubEditActivity::class.java))
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,31 @@ package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
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 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
|
||||
|
||||
@@ -18,18 +35,69 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
||||
val subItem = mActivity.subscriptions[position].second
|
||||
holder.itemSubSettingBinding.tvName.text = subItem.remarks
|
||||
holder.itemSubSettingBinding.tvUrl.text = subItem.url
|
||||
if (subItem.enabled) {
|
||||
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorAccent)
|
||||
} else {
|
||||
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(0)
|
||||
}
|
||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||
|
||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||
mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java)
|
||||
mActivity.startActivity(
|
||||
Intent(mActivity, SubEditActivity::class.java)
|
||||
.putExtra("subId", subId)
|
||||
)
|
||||
}
|
||||
holder.itemSubSettingBinding.infoContainer.setOnClickListener {
|
||||
subItem.enabled = !subItem.enabled
|
||||
subStorage?.encode(subId, Gson().toJson(subItem))
|
||||
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 {
|
||||
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
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
@@ -11,7 +11,6 @@ import android.content.Intent
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import com.google.zxing.WriterException
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.databinding.ActivityTaskerBinding
|
||||
@@ -45,7 +44,7 @@ class TaskerActivity : BaseActivity() {
|
||||
val adapter = ArrayAdapter(this,
|
||||
android.R.layout.simple_list_item_single_choice, lstData)
|
||||
listview = findViewById<View>(R.id.listview) as ListView
|
||||
listview!!.adapter = adapter
|
||||
listview?.adapter = adapter
|
||||
|
||||
init()
|
||||
}
|
||||
@@ -65,7 +64,7 @@ class TaskerActivity : BaseActivity() {
|
||||
listview?.setItemChecked(pos, true)
|
||||
}
|
||||
}
|
||||
} catch (e: WriterException) {
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
}
|
||||
@@ -91,7 +90,7 @@ class TaskerActivity : BaseActivity() {
|
||||
|
||||
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
|
||||
intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
|
||||
setResult(Activity.RESULT_OK, intent)
|
||||
setResult(AppCompatActivity.RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -114,3 +113,4 @@ class TaskerActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.AngConfigManager
|
||||
import java.net.URLDecoder
|
||||
|
||||
class UrlSchemeActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityLogcatBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLogcatBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
|
||||
try {
|
||||
intent.apply {
|
||||
if (action == Intent.ACTION_SEND) {
|
||||
if ("text/plain" == type) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
parseUri(it)
|
||||
}
|
||||
}
|
||||
} else if (action == Intent.ACTION_VIEW) {
|
||||
when (data?.host) {
|
||||
"install-config" -> {
|
||||
val uri: Uri? = intent.data
|
||||
val shareUrl = uri?.getQueryParameter("url") ?: ""
|
||||
parseUri(shareUrl)
|
||||
}
|
||||
|
||||
"install-sub" -> {
|
||||
val uri: Uri? = intent.data
|
||||
val shareUrl = uri?.getQueryParameter("url") ?: ""
|
||||
parseUri(shareUrl)
|
||||
}
|
||||
|
||||
else -> {
|
||||
toast(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
} catch (e: Exception) {
|
||||
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, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
||||
if (count + countSub > 0) {
|
||||
toast(R.string.import_subscription_success)
|
||||
} else {
|
||||
toast(R.string.import_subscription_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package com.v2ray.ang.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import com.tbruyelle.rxpermissions.RxPermissions
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||
import com.v2ray.ang.dto.AssetUrlItem
|
||||
import com.v2ray.ang.extension.toTrafficString
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class UserAssetActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivitySubSettingBinding
|
||||
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 builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySubSettingBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
setContentView(view)
|
||||
title = getString(R.string.title_user_asset_setting)
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = UserAssetAdapter()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_asset, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
R.id.add_file -> {
|
||||
showFileChooser()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.add_url -> {
|
||||
val intent = Intent(this, UserAssetUrlActivity::class.java)
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
R.id.download_file -> {
|
||||
downloadGeoFiles()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun showFileChooser() {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
} else
|
||||
toast(R.string.toast_permission_denied)
|
||||
}
|
||||
}
|
||||
|
||||
private val chooseFile =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it ->
|
||||
val uri = it.data?.data
|
||||
if (it.resultCode == RESULT_OK && uri != null) {
|
||||
val assetId = Utils.getUuid()
|
||||
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)
|
||||
} catch (e: Exception) {
|
||||
toast(R.string.toast_asset_copy_failed)
|
||||
MmkvManager.removeAssetUrl(assetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFile(uri: Uri): String {
|
||||
val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
|
||||
contentResolver.openInputStream(uri).use { inputStream ->
|
||||
targetFile.outputStream().use { fileOut ->
|
||||
inputStream?.copyTo(fileOut)
|
||||
toast(R.string.toast_success)
|
||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
return targetFile.path
|
||||
}
|
||||
|
||||
private fun getCursorName(uri: Uri): String? = try {
|
||||
contentResolver.query(uri, null, null, null, null)?.let { cursor ->
|
||||
cursor.run {
|
||||
if (moveToFirst()) getString(getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||
else null
|
||||
}.also { cursor.close() }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
private fun downloadGeoFiles() {
|
||||
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
||||
|
||||
toast(R.string.msg_downloading_content)
|
||||
var assets = MmkvManager.decodeAssetUrls()
|
||||
assets = addBuiltInGeoItems(assets)
|
||||
|
||||
assets.forEach {
|
||||
//toast(getString(R.string.msg_downloading_content) + it)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
var result = downloadGeo(it.second, 60000, httpPort)
|
||||
if (!result) {
|
||||
result = downloadGeo(it.second, 60000, 0)
|
||||
}
|
||||
launch(Dispatchers.Main) {
|
||||
if (result) {
|
||||
toast(getString(R.string.toast_success) + " " + it.second.remarks)
|
||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||
} else {
|
||||
toast(getString(R.string.toast_failure) + " " + it.second.remarks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
||||
val targetTemp = File(extDir, item.remarks + "_temp")
|
||||
val target = File(extDir, item.remarks)
|
||||
var conn: HttpURLConnection? = null
|
||||
//Log.d(AppConfig.ANG_PACKAGE, url)
|
||||
|
||||
try {
|
||||
conn = if (httpPort == 0) {
|
||||
URL(item.url).openConnection() as HttpURLConnection
|
||||
} else {
|
||||
URL(item.url).openConnection(
|
||||
Proxy(
|
||||
Proxy.Type.HTTP,
|
||||
InetSocketAddress("127.0.0.1", httpPort)
|
||||
)
|
||||
) as HttpURLConnection
|
||||
}
|
||||
conn.connectTimeout = timeout
|
||||
conn.readTimeout = timeout
|
||||
val inputStream = conn.inputStream
|
||||
val responseCode = conn.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
FileOutputStream(targetTemp).use { output ->
|
||||
inputStream.copyTo(output)
|
||||
}
|
||||
|
||||
targetTemp.renameTo(target)
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.ANG_PACKAGE, Log.getStackTraceString(e))
|
||||
return false
|
||||
} finally {
|
||||
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>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||
return UserAssetViewHolder(
|
||||
ItemRecyclerUserAssetBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||
var assets = MmkvManager.decodeAssetUrls();
|
||||
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)
|
||||
holder.itemUserAssetBinding.assetProperties.text =
|
||||
"${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
|
||||
} else {
|
||||
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else 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 {
|
||||
file?.delete()
|
||||
MmkvManager.removeAssetUrl(item.first)
|
||||
binding.recyclerView.adapter?.notifyItemRemoved(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
var assets = MmkvManager.decodeAssetUrls();
|
||||
assets = addBuiltInGeoItems(assets);
|
||||
return assets.size
|
||||
}
|
||||
}
|
||||
|
||||
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 com.v2ray.ang.dto.AppInfo
|
||||
import rx.Observable
|
||||
import java.util.*
|
||||
|
||||
object AppManagerUtil {
|
||||
fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
|
||||
@@ -22,7 +21,7 @@ object AppManagerUtil {
|
||||
|
||||
val appName = applicationInfo.loadLabel(packageManager).toString()
|
||||
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)
|
||||
apps.add(appInfo)
|
||||
@@ -31,9 +30,10 @@ object AppManagerUtil {
|
||||
return apps
|
||||
}
|
||||
|
||||
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> = Observable.unsafeCreate {
|
||||
it.onNext(loadNetworkAppList(ctx))
|
||||
}
|
||||
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
|
||||
Observable.unsafeCreate {
|
||||
it.onNext(loadNetworkAppList(ctx))
|
||||
}
|
||||
|
||||
val PackageInfo.hasInternetPermission: Boolean
|
||||
get() {
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.service.V2RayTestService
|
||||
import java.io.Serializable
|
||||
|
||||
|
||||
object MessageUtil {
|
||||
|
||||
fun sendMsg2Service(ctx: Context, what: Int, content: String) {
|
||||
fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) {
|
||||
sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
|
||||
}
|
||||
|
||||
fun sendMsg2UI(ctx: Context, what: Int, content: String) {
|
||||
fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
|
||||
sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
|
||||
}
|
||||
|
||||
private fun sendMsg(ctx: Context, action: String, what: Int, content: String) {
|
||||
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
|
||||
try {
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(ctx, V2RayTestService::class.java)
|
||||
intent.putExtra("key", what)
|
||||
intent.putExtra("content", content)
|
||||
ctx.startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
|
||||
try {
|
||||
val intent = Intent()
|
||||
intent.action = action
|
||||
@@ -27,4 +42,4 @@ object MessageUtil {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.v2ray.ang.util
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.dto.AssetUrlItem
|
||||
import com.v2ray.ang.dto.ServerAffiliationInfo
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.SubscriptionItem
|
||||
import java.net.URI
|
||||
|
||||
object MmkvManager {
|
||||
const val ID_MAIN = "MAIN"
|
||||
@@ -12,6 +14,7 @@ object MmkvManager {
|
||||
const val ID_SERVER_RAW = "SERVER_RAW"
|
||||
const val ID_SERVER_AFF = "SERVER_AFF"
|
||||
const val ID_SUB = "SUB"
|
||||
const val ID_ASSET = "ASSET"
|
||||
const val ID_SETTING = "SETTING"
|
||||
const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
|
||||
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 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 assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
fun decodeServerList(): MutableList<String> {
|
||||
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
|
||||
@@ -44,9 +48,9 @@ object MmkvManager {
|
||||
fun encodeServerConfig(guid: String, config: ServerConfig): String {
|
||||
val key = guid.ifBlank { Utils.getUuid() }
|
||||
serverStorage?.encode(key, Gson().toJson(config))
|
||||
val serverList= decodeServerList()
|
||||
val serverList = decodeServerList()
|
||||
if (!serverList.contains(key)) {
|
||||
serverList.add(key)
|
||||
serverList.add(0, key)
|
||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||
if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) {
|
||||
mainStorage?.encode(KEY_SELECTED_SERVER, key)
|
||||
@@ -62,7 +66,7 @@ object MmkvManager {
|
||||
if (mainStorage?.decodeString(KEY_SELECTED_SERVER) == guid) {
|
||||
mainStorage?.remove(KEY_SELECTED_SERVER)
|
||||
}
|
||||
val serverList= decodeServerList()
|
||||
val serverList = decodeServerList()
|
||||
serverList.remove(guid)
|
||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||
serverStorage?.remove(guid)
|
||||
@@ -102,8 +106,8 @@ object MmkvManager {
|
||||
serverAffStorage?.encode(guid, Gson().toJson(aff))
|
||||
}
|
||||
|
||||
fun clearAllTestDelayResults() {
|
||||
serverAffStorage?.allKeys()?.forEach { key ->
|
||||
fun clearAllTestDelayResults(keys: List<String>?) {
|
||||
keys?.forEach { key ->
|
||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||
aff.testDelayMillis = 0
|
||||
serverAffStorage?.encode(key, Gson().toJson(aff))
|
||||
@@ -118,8 +122,9 @@ object MmkvManager {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
val uri = URI(Utils.fixIllegalUrl(url))
|
||||
val subItem = SubscriptionItem()
|
||||
subItem.remarks = "import sub"
|
||||
subItem.remarks = Utils.urlDecode(uri.fragment ?: "import sub")
|
||||
subItem.url = url
|
||||
subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
|
||||
return 1
|
||||
@@ -141,4 +146,55 @@ object MmkvManager {
|
||||
subStorage?.remove(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() {
|
||||
mainStorage?.clearAll()
|
||||
serverStorage?.clearAll()
|
||||
serverAffStorage?.clearAll()
|
||||
}
|
||||
|
||||
fun removeInvalidServer() {
|
||||
serverAffStorage?.allKeys()?.forEach { key ->
|
||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||
if (aff.testDelayMillis < 0L) {
|
||||
removeServer(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sortByTestResults() {
|
||||
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
||||
|
||||
val serverDelays = mutableListOf<ServerDelay>()
|
||||
val serverList = decodeServerList()
|
||||
serverList.forEach { key ->
|
||||
val delay = decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
||||
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
|
||||
}
|
||||
serverDelays.sortBy { it.testDelayMillis }
|
||||
|
||||
serverDelays.forEach {
|
||||
serverList.remove(it.guid)
|
||||
serverList.add(it.guid)
|
||||
}
|
||||
|
||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.Locale
|
||||
|
||||
open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||
companion object {
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun wrap(context: Context, newLocale: Locale?): ContextWrapper {
|
||||
var mContext = context
|
||||
val res: Resources = mContext.resources
|
||||
val configuration: Configuration = res.configuration
|
||||
//注意 Android 7.0 前后的不同处理方法
|
||||
mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
configuration.setLocale(newLocale)
|
||||
val localeList = LocaleList(newLocale)
|
||||
LocaleList.setDefault(localeList)
|
||||
configuration.setLocales(localeList)
|
||||
mContext.createConfigurationContext(configuration)
|
||||
} else {
|
||||
configuration.setLocale(newLocale)
|
||||
mContext.createConfigurationContext(configuration)
|
||||
}
|
||||
return ContextWrapper(mContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
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.qrcode.QRCodeReader
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import java.util.EnumMap
|
||||
|
||||
/**
|
||||
* 描述:解析二维码图片
|
||||
*/
|
||||
object QRCodeDecoder {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
||||
*
|
||||
* @param picturePath 要解析的二维码图片本地路径
|
||||
* @return 返回二维码图片里的内容 或 null
|
||||
*/
|
||||
fun syncDecodeQRCode(picturePath: String): String? {
|
||||
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
|
||||
*
|
||||
* @param bitmap 要解析的二维码图片
|
||||
* @return 返回二维码图片里的内容 或 null
|
||||
*/
|
||||
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
||||
if (bitmap == null) {
|
||||
return null
|
||||
}
|
||||
var source: RGBLuminanceSource? = null
|
||||
try {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
val pixels = IntArray(width * height)
|
||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
source = RGBLuminanceSource(width, height, pixels)
|
||||
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) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
|
||||
*
|
||||
* @param picturePath 本地图片文件路径
|
||||
* @return
|
||||
*/
|
||||
private fun getDecodeAbleBitmap(picturePath: String): Bitmap? {
|
||||
return try {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(picturePath, options)
|
||||
var sampleSize = options.outHeight / 400
|
||||
if (sampleSize <= 0) {
|
||||
sampleSize = 1
|
||||
}
|
||||
options.inSampleSize = sampleSize
|
||||
options.inJustDecodeBounds = false
|
||||
BitmapFactory.decodeFile(picturePath, options)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val allFormats: List<BarcodeFormat> = arrayListOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_93,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.MAXICODE,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.RSS_14,
|
||||
BarcodeFormat.RSS_EXPANDED,
|
||||
BarcodeFormat.UPC_A,
|
||||
BarcodeFormat.UPC_E,
|
||||
BarcodeFormat.UPC_EAN_EXTENSION
|
||||
)
|
||||
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
|
||||
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
|
||||
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.responseLength
|
||||
import kotlinx.coroutines.isActive
|
||||
import libv2ray.Libv2ray
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.Socket
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
object SpeedtestUtil {
|
||||
|
||||
private val tcpTestingSockets = ArrayList<Socket?>()
|
||||
|
||||
suspend fun tcping(url: String, port: Int): Long {
|
||||
var time = -1L
|
||||
for (k in 0 until 2) {
|
||||
val one = socketConnectTime(url, port)
|
||||
if (!coroutineContext.isActive) {
|
||||
break
|
||||
}
|
||||
if (one != -1L && (time == -1L || one < time)) {
|
||||
time = one
|
||||
}
|
||||
}
|
||||
return time
|
||||
}
|
||||
|
||||
fun realPing(config: String): Long {
|
||||
return try {
|
||||
Libv2ray.measureOutboundDelay(config, Utils.getDelayTestUrl())
|
||||
} catch (e: Exception) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
|
||||
-1L
|
||||
}
|
||||
}
|
||||
|
||||
fun ping(url: String): String {
|
||||
try {
|
||||
val command = "/system/bin/ping -c 3 $url"
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
||||
if (!TextUtils.isEmpty(allText)) {
|
||||
val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
|
||||
val temps =
|
||||
tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (temps.count() > 0 && temps[0].length < 10) {
|
||||
return temps[0].toFloat().toInt().toString() + "ms"
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return "-1ms"
|
||||
}
|
||||
|
||||
fun socketConnectTime(url: String, port: Int): Long {
|
||||
try {
|
||||
val socket = Socket()
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.add(socket)
|
||||
}
|
||||
val start = System.currentTimeMillis()
|
||||
socket.connect(InetSocketAddress(url, port), 3000)
|
||||
val time = System.currentTimeMillis() - start
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.remove(socket)
|
||||
}
|
||||
socket.close()
|
||||
return time
|
||||
} catch (e: UnknownHostException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun closeAllTcpSockets() {
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.forEach {
|
||||
it?.close()
|
||||
}
|
||||
tcpTestingSockets.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun testConnection(context: Context, port: Int): String {
|
||||
// return V2RayVpnService.measureV2rayDelay()
|
||||
var result: String
|
||||
var conn: HttpURLConnection? = null
|
||||
|
||||
try {
|
||||
val url = URL(Utils.getDelayTestUrl())
|
||||
|
||||
conn = url.openConnection(
|
||||
Proxy(
|
||||
Proxy.Type.HTTP,
|
||||
InetSocketAddress("127.0.0.1", port)
|
||||
)
|
||||
) as HttpURLConnection
|
||||
conn.connectTimeout = 30000
|
||||
conn.readTimeout = 30000
|
||||
conn.setRequestProperty("Connection", "close")
|
||||
conn.instanceFollowRedirects = false
|
||||
conn.useCaches = false
|
||||
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
val code = conn.responseCode
|
||||
val elapsed = SystemClock.elapsedRealtime() - start
|
||||
|
||||
if (code == 204 || code == 200 && conn.responseLength == 0L) {
|
||||
result = context.getString(R.string.connection_test_available, elapsed)
|
||||
} else {
|
||||
throw IOException(
|
||||
context.getString(
|
||||
R.string.connection_test_error_status_code,
|
||||
code
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// network exception
|
||||
Log.d(
|
||||
AppConfig.ANG_PACKAGE,
|
||||
"testConnection IOException: " + Log.getStackTraceString(e)
|
||||
)
|
||||
result = context.getString(R.string.connection_test_error, e.message)
|
||||
} catch (e: Exception) {
|
||||
// library exception, eg sumsung
|
||||
Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e))
|
||||
result = context.getString(R.string.connection_test_error, e.message)
|
||||
} finally {
|
||||
conn?.disconnect()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun getLibVersion(): String {
|
||||
return Libv2ray.checkVersionX()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +1,37 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
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_NO
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import android.provider.Settings
|
||||
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.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
import android.webkit.URLUtil
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.extension.responseLength
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.io.IOException
|
||||
import java.net.*
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import java.util.*
|
||||
|
||||
object Utils {
|
||||
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||
private val tcpTestingSockets = ArrayList<Socket?>()
|
||||
|
||||
/**
|
||||
* convert string to editalbe for kotlin
|
||||
@@ -42,8 +39,8 @@ object Utils {
|
||||
* @param text
|
||||
* @return
|
||||
*/
|
||||
fun getEditable(text: String): Editable {
|
||||
return Editable.Factory.getInstance().newEditable(text)
|
||||
fun getEditable(text: String?): Editable {
|
||||
return Editable.Factory.getInstance().newEditable(text?:"")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,14 +59,14 @@ object Utils {
|
||||
* parseInt
|
||||
*/
|
||||
fun parseInt(str: String): Int {
|
||||
return try {
|
||||
Integer.parseInt(str)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
0
|
||||
}
|
||||
return parseInt(str, 0)
|
||||
}
|
||||
|
||||
fun parseInt(str: String?, default: Int): Int {
|
||||
return str?.toIntOrNull() ?: default
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get text from clipboard
|
||||
*/
|
||||
@@ -99,25 +96,21 @@ object Utils {
|
||||
/**
|
||||
* base64 decode
|
||||
*/
|
||||
fun decode(text: String): String {
|
||||
tryDecodeBase64(text)?.let { return it }
|
||||
if (text.endsWith('=')) {
|
||||
// try again for some loosely formatted base64
|
||||
tryDecodeBase64(text.trimEnd('='))?.let { return it }
|
||||
}
|
||||
return ""
|
||||
fun decode(text: String?): String {
|
||||
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) } ?: ""
|
||||
}
|
||||
|
||||
fun tryDecodeBase64(text: String): String? {
|
||||
|
||||
fun tryDecodeBase64(text: String?): String? {
|
||||
try {
|
||||
return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8"))
|
||||
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.i(AppConfig.ANG_PACKAGE, "Parse base64 standard failed $e")
|
||||
Log.i(ANG_PACKAGE, "Parse base64 standard failed $e")
|
||||
}
|
||||
try {
|
||||
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(charset("UTF-8"))
|
||||
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.i(AppConfig.ANG_PACKAGE, "Parse base64 url safe failed $e")
|
||||
Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e")
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -127,7 +120,7 @@ object Utils {
|
||||
*/
|
||||
fun encode(text: String): String {
|
||||
return try {
|
||||
Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP)
|
||||
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
@@ -138,18 +131,16 @@ object Utils {
|
||||
* get remote dns servers from preference
|
||||
*/
|
||||
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) }
|
||||
if (ret.isEmpty()) {
|
||||
return listOf(AppConfig.DNS_AGENT)
|
||||
return listOf(AppConfig.DNS_PROXY)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
fun getVpnDnsServers(): List<String> {
|
||||
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)
|
||||
?: settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS)
|
||||
?: AppConfig.DNS_AGENT
|
||||
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)?:AppConfig.DNS_VPN
|
||||
return vpnDns.split(",").filter { isPureIpAddress(it) }
|
||||
// allow empty, in that case dns will use system default
|
||||
}
|
||||
@@ -166,36 +157,6 @@ object Utils {
|
||||
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
|
||||
*/
|
||||
@@ -208,7 +169,7 @@ object Utils {
|
||||
//CIDR
|
||||
if (addr.indexOf("/") > 0) {
|
||||
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]
|
||||
}
|
||||
}
|
||||
@@ -224,7 +185,7 @@ object Utils {
|
||||
// addr = addr.toLowerCase()
|
||||
val octets = addr.split('.').toTypedArray()
|
||||
if (octets.size == 4) {
|
||||
if(octets[3].indexOf(":") > 0) {
|
||||
if (octets[3].indexOf(":") > 0) {
|
||||
addr = addr.substring(0, addr.indexOf(":"))
|
||||
}
|
||||
return isIpv4Address(addr)
|
||||
@@ -239,7 +200,7 @@ object Utils {
|
||||
}
|
||||
|
||||
fun isPureIpAddress(value: String): Boolean {
|
||||
return (isIpv4Address(value) || isIpv6Address(value))
|
||||
return isIpv4Address(value) || isIpv6Address(value)
|
||||
}
|
||||
|
||||
fun isIpv4Address(value: String): Boolean {
|
||||
@@ -258,7 +219,7 @@ object Utils {
|
||||
}
|
||||
|
||||
private fun isCoreDNSAddress(s: String): Boolean {
|
||||
return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic")
|
||||
return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") || s == "localhost"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,10 +227,16 @@ object Utils {
|
||||
*/
|
||||
fun isValidUrl(value: String?): Boolean {
|
||||
try {
|
||||
if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) {
|
||||
if (value.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
if (Patterns.WEB_URL.matcher(value).matches()
|
||||
|| Patterns.DOMAIN_NAME.matcher(value).matches()
|
||||
|| URLUtil.isValidUrl(value)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} catch (e: WriterException) {
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
@@ -312,7 +279,7 @@ object Utils {
|
||||
|
||||
fun urlDecode(url: String): String {
|
||||
return try {
|
||||
URLDecoder.decode(url, "UTF-8")
|
||||
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
url
|
||||
@@ -321,151 +288,85 @@ object Utils {
|
||||
|
||||
fun urlEncode(url: String): String {
|
||||
return try {
|
||||
URLEncoder.encode(url, "UTF-8")
|
||||
URLEncoder.encode(url, Charsets.UTF_8.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
fun testConnection(context: Context, port: Int): String {
|
||||
var result: String
|
||||
var conn: HttpURLConnection? = null
|
||||
|
||||
try {
|
||||
val url = URL("https",
|
||||
"www.google.com",
|
||||
"/generate_204")
|
||||
|
||||
conn = url.openConnection(
|
||||
Proxy(Proxy.Type.HTTP,
|
||||
InetSocketAddress("127.0.0.1", port + 1))) as HttpURLConnection
|
||||
conn.connectTimeout = 30000
|
||||
conn.readTimeout = 30000
|
||||
conn.setRequestProperty("Connection", "close")
|
||||
conn.instanceFollowRedirects = false
|
||||
conn.useCaches = false
|
||||
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
val code = conn.responseCode
|
||||
val elapsed = SystemClock.elapsedRealtime() - start
|
||||
|
||||
if (code == 204 || code == 200 && conn.responseLength == 0L) {
|
||||
result = context.getString(R.string.connection_test_available, elapsed)
|
||||
} else {
|
||||
throw IOException(context.getString(R.string.connection_test_error_status_code, code))
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// network exception
|
||||
Log.d(AppConfig.ANG_PACKAGE,"testConnection IOException: "+Log.getStackTraceString(e))
|
||||
result = context.getString(R.string.connection_test_error, e.message)
|
||||
} catch (e: Exception) {
|
||||
// library exception, eg sumsung
|
||||
Log.d(AppConfig.ANG_PACKAGE,"testConnection Exception: "+Log.getStackTraceString(e))
|
||||
result = context.getString(R.string.connection_test_error, e.message)
|
||||
} finally {
|
||||
conn?.disconnect()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* package path
|
||||
*/
|
||||
fun packagePath(context: Context): String {
|
||||
var path = context.filesDir.toString()
|
||||
path = path.replace("files", "")
|
||||
//path += "tun2socks"
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
it.readText()
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* ping
|
||||
*/
|
||||
fun ping(url: String): String {
|
||||
fun userAssetPath(context: Context?): String {
|
||||
if (context == null)
|
||||
return ""
|
||||
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
|
||||
?: return context.getDir(AppConfig.DIR_ASSETS, 0).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(Charsets.UTF_8)
|
||||
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
||||
}
|
||||
|
||||
fun getUrlContext(url: String, timeout: Int): String {
|
||||
var result: String
|
||||
var conn: HttpURLConnection? = null
|
||||
|
||||
try {
|
||||
val command = "/system/bin/ping -c 3 $url"
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
||||
if (allText.isNotBlank()) {
|
||||
val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
|
||||
val temps = tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (temps.count() > 0 && temps[0].length < 10) {
|
||||
return temps[0].toFloat().toInt().toString() + "ms"
|
||||
}
|
||||
}
|
||||
conn = URL(url).openConnection() as HttpURLConnection
|
||||
conn.connectTimeout = timeout
|
||||
conn.readTimeout = timeout
|
||||
conn.setRequestProperty("Connection", "close")
|
||||
conn.instanceFollowRedirects = false
|
||||
conn.useCaches = false
|
||||
//val code = conn.responseCode
|
||||
result = conn.inputStream.bufferedReader().readText()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return "-1ms"
|
||||
}
|
||||
|
||||
/**
|
||||
* tcping
|
||||
*/
|
||||
suspend fun tcping(url: String, port: Int): Long {
|
||||
var time = -1L
|
||||
for (k in 0 until 2) {
|
||||
val one = socketConnectTime(url, port)
|
||||
if (!coroutineContext.isActive) {
|
||||
break
|
||||
}
|
||||
if (one != -1L && (time == -1L || one < time)) {
|
||||
time = one
|
||||
}
|
||||
}
|
||||
return time
|
||||
}
|
||||
|
||||
fun socketConnectTime(url: String, port: Int): Long {
|
||||
try {
|
||||
val socket = Socket()
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.add(socket)
|
||||
}
|
||||
val start = System.currentTimeMillis()
|
||||
socket.connect(InetSocketAddress(url, port))
|
||||
val time = System.currentTimeMillis() - start
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.remove(socket)
|
||||
}
|
||||
socket.close()
|
||||
return time
|
||||
} catch (e: UnknownHostException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
fun closeAllTcpSockets() {
|
||||
synchronized(this) {
|
||||
tcpTestingSockets.forEach {
|
||||
it?.close()
|
||||
}
|
||||
tcpTestingSockets.clear()
|
||||
result = ""
|
||||
} finally {
|
||||
conn?.disconnect()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getUrlContentWithCustomUserAgent(urlStr: String?): String {
|
||||
fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String {
|
||||
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("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
||||
url.userInfo?.let {
|
||||
@@ -477,5 +378,80 @@ object Utils {
|
||||
it.bufferedReader().readText()
|
||||
}
|
||||
}
|
||||
|
||||
fun getDarkModeStatus(context: Context): Boolean {
|
||||
return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
|
||||
}
|
||||
|
||||
|
||||
fun setNightMode(context: Context) {
|
||||
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)
|
||||
} else {
|
||||
address
|
||||
}
|
||||
}
|
||||
|
||||
fun getLocale(): Locale {
|
||||
val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto"
|
||||
return when (lang) {
|
||||
"auto" -> getSysLocale()
|
||||
"en" -> Locale.ENGLISH
|
||||
"zh-rCN" -> Locale.CHINA
|
||||
"zh-rTW" -> Locale.TRADITIONAL_CHINESE
|
||||
"vi" -> Locale("vi")
|
||||
"ru" -> Locale("ru")
|
||||
"fa" -> Locale("fa")
|
||||
"bn" -> Locale("bn")
|
||||
else -> getSysLocale()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
LocaleList.getDefault()[0]
|
||||
} else {
|
||||
Locale.getDefault()
|
||||
}
|
||||
|
||||
fun fixIllegalUrl(str: String): String {
|
||||
return str
|
||||
.replace(" ","%20")
|
||||
.replace("|","%7C")
|
||||
}
|
||||
|
||||
fun removeWhiteSpace(str: String?): String? {
|
||||
return str?.replace(" ", "")
|
||||
}
|
||||
|
||||
fun idnToASCII(str: String): String {
|
||||
val url = URL(str)
|
||||
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file)
|
||||
.toExternalForm()
|
||||
}
|
||||
|
||||
fun isTv(context: Context): Boolean =
|
||||
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
|
||||
fun getDelayTestUrl(second: Boolean = false): String {
|
||||
return if (second) {
|
||||
AppConfig.DelayTestUrl2
|
||||
} else {
|
||||
settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,36 @@ package com.v2ray.ang.util
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.google.gson.*
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
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_BLOCKED
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
|
||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||
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.ERoutingMode
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
|
||||
|
||||
object V2rayConfigUtil {
|
||||
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) }
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
data class Result(var status: Boolean, var content: String)
|
||||
|
||||
@@ -31,12 +49,20 @@ object V2rayConfigUtil {
|
||||
} else {
|
||||
raw
|
||||
}
|
||||
Log.d(ANG_PACKAGE, customConfig)
|
||||
//Log.d(ANG_PACKAGE, customConfig)
|
||||
return Result(true, customConfig)
|
||||
}
|
||||
val outbound = config.getProxyOutbound() ?: return Result(false, "")
|
||||
val result = getV2rayNonCustomConfig(context, outbound)
|
||||
Log.d(ANG_PACKAGE, result.content)
|
||||
val address = outbound.getServerAddress() ?: return Result(false, "")
|
||||
if (!Utils.isIpAddress(address)) {
|
||||
if (!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
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -47,7 +73,11 @@ object V2rayConfigUtil {
|
||||
/**
|
||||
* 生成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 assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||
@@ -58,14 +88,16 @@ object V2rayConfigUtil {
|
||||
//转成Json
|
||||
val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result
|
||||
|
||||
//v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
||||
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL)
|
||||
?: "warning"
|
||||
|
||||
inbounds(v2rayConfig)
|
||||
|
||||
httpRequestObject(outbound)
|
||||
|
||||
updateOutboundWithGlobalSettings(outbound)
|
||||
v2rayConfig.outbounds[0] = outbound
|
||||
|
||||
updateOutboundFragment(v2rayConfig)
|
||||
|
||||
routing(v2rayConfig)
|
||||
|
||||
fakedns(v2rayConfig)
|
||||
@@ -79,6 +111,9 @@ object V2rayConfigUtil {
|
||||
v2rayConfig.stats = null
|
||||
v2rayConfig.policy = null
|
||||
}
|
||||
|
||||
v2rayConfig.remarks = remarks
|
||||
|
||||
result.status = true
|
||||
result.content = v2rayConfig.toPrettyPrinting()
|
||||
return result
|
||||
@@ -89,8 +124,14 @@ object V2rayConfigUtil {
|
||||
*/
|
||||
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
||||
try {
|
||||
//val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT) ?: AppConfig.PORT_SOCKS)
|
||||
//val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT) ?: AppConfig.PORT_HTTP)
|
||||
val socksPort = Utils.parseInt(
|
||||
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 ->
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
||||
@@ -98,12 +139,15 @@ object V2rayConfigUtil {
|
||||
curInbound.listen = "127.0.0.1"
|
||||
}
|
||||
}
|
||||
v2rayConfig.inbounds[0].port = 10808
|
||||
v2rayConfig.inbounds[0].port = socksPort
|
||||
val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED)
|
||||
?: false
|
||||
val sniffAllTlsAndHttp = settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
|
||||
?: false
|
||||
val sniffAllTlsAndHttp =
|
||||
settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
|
||||
?: true
|
||||
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
|
||||
v2rayConfig.inbounds[0].sniffing?.routeOnly =
|
||||
settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
|
||||
if (!sniffAllTlsAndHttp) {
|
||||
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
|
||||
}
|
||||
@@ -111,7 +155,7 @@ object V2rayConfigUtil {
|
||||
v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
|
||||
}
|
||||
|
||||
//v2rayConfig.inbounds[1].port = httpPort
|
||||
v2rayConfig.inbounds[1].port = httpPort
|
||||
|
||||
// if (httpPort > 0) {
|
||||
// val httpCopy = v2rayConfig.inbounds[0].copy()
|
||||
@@ -127,11 +171,10 @@ object V2rayConfigUtil {
|
||||
}
|
||||
|
||||
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.outbounds.filter { it.protocol == "freedom" }.forEach {
|
||||
it.settings?.domainStrategy = "UseIP"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,38 +183,84 @@ object V2rayConfigUtil {
|
||||
*/
|
||||
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
||||
try {
|
||||
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: "", AppConfig.TAG_AGENT, v2rayConfig)
|
||||
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: "", AppConfig.TAG_DIRECT, v2rayConfig)
|
||||
routingUserRule(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
||||
?: "", AppConfig.TAG_BLOCKED, v2rayConfig)
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
||||
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
||||
|
||||
v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
||||
?: "", TAG_BLOCKED, v2rayConfig
|
||||
)
|
||||
if (routingMode == ERoutingMode.GLOBAL_DIRECT.value) {
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: "", TAG_DIRECT, v2rayConfig
|
||||
)
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: "", TAG_PROXY, v2rayConfig
|
||||
)
|
||||
} else {
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: "", TAG_PROXY, v2rayConfig
|
||||
)
|
||||
routingUserRule(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: "", TAG_DIRECT, v2rayConfig
|
||||
)
|
||||
}
|
||||
|
||||
v2rayConfig.routing.domainStrategy =
|
||||
settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
||||
?: "IPIfNonMatch"
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: "0"
|
||||
|
||||
// Hardcode googleapis.cn
|
||||
// Hardcode googleapis.cn gstatic.com
|
||||
val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
|
||||
type = "field",
|
||||
outboundTag = AppConfig.TAG_AGENT,
|
||||
domain = arrayListOf("domain:googleapis.cn")
|
||||
outboundTag = TAG_PROXY,
|
||||
domain = arrayListOf("domain:googleapis.cn", "domain:gstatic.com")
|
||||
)
|
||||
|
||||
when (routingMode) {
|
||||
"1" -> {
|
||||
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
|
||||
ERoutingMode.BYPASS_LAN.value -> {
|
||||
routingGeo("", "private", TAG_DIRECT, v2rayConfig)
|
||||
}
|
||||
"2" -> {
|
||||
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
|
||||
|
||||
ERoutingMode.BYPASS_MAINLAND.value -> {
|
||||
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
|
||||
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
||||
}
|
||||
"3" -> {
|
||||
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
|
||||
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
|
||||
|
||||
ERoutingMode.BYPASS_LAN_MAINLAND.value -> {
|
||||
routingGeo("", "private", TAG_DIRECT, v2rayConfig)
|
||||
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
|
||||
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
||||
}
|
||||
|
||||
ERoutingMode.GLOBAL_DIRECT.value -> {
|
||||
val globalDirect = V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = TAG_DIRECT,
|
||||
)
|
||||
if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") {
|
||||
globalDirect.port = "0-65535"
|
||||
} else {
|
||||
globalDirect.ip = arrayListOf("0.0.0.0/0", "::/0")
|
||||
}
|
||||
v2rayConfig.routing.rules.add(globalDirect)
|
||||
}
|
||||
}
|
||||
|
||||
if (routingMode != ERoutingMode.GLOBAL_DIRECT.value) {
|
||||
val globalProxy = V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = TAG_PROXY,
|
||||
)
|
||||
if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") {
|
||||
globalProxy.port = "0-65535"
|
||||
} else {
|
||||
globalProxy.ip = arrayListOf("0.0.0.0/0", "::/0")
|
||||
}
|
||||
v2rayConfig.routing.rules.add(globalProxy)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
@@ -179,13 +268,17 @@ object V2rayConfigUtil {
|
||||
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 {
|
||||
if (!TextUtils.isEmpty(code)) {
|
||||
//IP
|
||||
if (ipOrDomain == "ip" || ipOrDomain == "") {
|
||||
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
||||
rulesIP.type = "field"
|
||||
rulesIP.outboundTag = tag
|
||||
rulesIP.ip = ArrayList()
|
||||
rulesIP.ip?.add("geoip:$code")
|
||||
@@ -195,7 +288,6 @@ object V2rayConfigUtil {
|
||||
if (ipOrDomain == "domain" || ipOrDomain == "") {
|
||||
//Domain
|
||||
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
||||
rulesDomain.type = "field"
|
||||
rulesDomain.outboundTag = tag
|
||||
rulesDomain.domain = ArrayList()
|
||||
rulesDomain.domain?.add("geosite:$code")
|
||||
@@ -212,33 +304,27 @@ object V2rayConfigUtil {
|
||||
if (!TextUtils.isEmpty(userRule)) {
|
||||
//Domain
|
||||
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
||||
rulesDomain.type = "field"
|
||||
rulesDomain.outboundTag = tag
|
||||
rulesDomain.domain = ArrayList()
|
||||
|
||||
//IP
|
||||
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
||||
rulesIP.type = "field"
|
||||
rulesIP.outboundTag = tag
|
||||
rulesIP.ip = ArrayList()
|
||||
|
||||
userRule.split(",").map { it.trim() }.forEach {
|
||||
if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
|
||||
if (it.startsWith("ext:") && it.contains("geoip")) {
|
||||
rulesIP.ip?.add(it)
|
||||
} else if (it.isNotEmpty())
|
||||
// if (Utils.isValidUrl(it)
|
||||
// || it.startsWith("geosite:")
|
||||
// || it.startsWith("regexp:")
|
||||
// || it.startsWith("domain:")
|
||||
// || it.startsWith("full:"))
|
||||
{
|
||||
} else if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
|
||||
rulesIP.ip?.add(it)
|
||||
} else if (it.isNotEmpty()) {
|
||||
rulesDomain.domain?.add(it)
|
||||
}
|
||||
}
|
||||
if (rulesDomain.domain?.size!! > 0) {
|
||||
if ((rulesDomain.domain?.size ?: 0) > 0) {
|
||||
v2rayConfig.routing.rules.add(rulesDomain)
|
||||
}
|
||||
if (rulesIP.ip?.size!! > 0) {
|
||||
if ((rulesIP.ip?.size ?: 0) > 0) {
|
||||
v2rayConfig.routing.rules.add(rulesIP)
|
||||
}
|
||||
}
|
||||
@@ -247,7 +333,7 @@ object V2rayConfigUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private fun userRule2Domian(userRule: String): ArrayList<String> {
|
||||
private fun userRule2Domain(userRule: String): ArrayList<String> {
|
||||
val domain = ArrayList<String>()
|
||||
userRule.split(",").map { it.trim() }.forEach {
|
||||
if (it.startsWith("geosite:") || it.startsWith("domain:")) {
|
||||
@@ -264,51 +350,69 @@ object V2rayConfigUtil {
|
||||
try {
|
||||
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
||||
val geositeCn = arrayListOf("geosite:cn")
|
||||
val proxyDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: "")
|
||||
val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: "")
|
||||
val proxyDomain = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: ""
|
||||
)
|
||||
val directDomain = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: ""
|
||||
)
|
||||
// fakedns with all domains to make it always top priority
|
||||
v2rayConfig.dns.servers?.add(0,
|
||||
V2rayConfig.DnsBean.ServersBean(address = "fakedns", domains = geositeCn.plus(proxyDomain).plus(directDomain)))
|
||||
v2rayConfig.dns.servers?.add(
|
||||
0,
|
||||
V2rayConfig.DnsBean.ServersBean(
|
||||
address = "fakedns",
|
||||
domains = geositeCn.plus(proxyDomain).plus(directDomain)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// DNS inbound对象
|
||||
val remoteDns = Utils.getRemoteDnsServers()
|
||||
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
|
||||
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
|
||||
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else "1.1.1.1",
|
||||
port = 53,
|
||||
network = "tcp,udp")
|
||||
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
|
||||
port = 53,
|
||||
network = "tcp,udp"
|
||||
)
|
||||
|
||||
//val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT) ?: AppConfig.PORT_LOCAL_DNS)
|
||||
val localDnsPort = Utils.parseInt(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT),
|
||||
AppConfig.PORT_LOCAL_DNS.toInt()
|
||||
)
|
||||
v2rayConfig.inbounds.add(
|
||||
V2rayConfig.InboundBean(
|
||||
tag = "dns-in",
|
||||
port = 10807,
|
||||
listen = "127.0.0.1",
|
||||
protocol = "dokodemo-door",
|
||||
settings = dnsInboundSettings,
|
||||
sniffing = null))
|
||||
V2rayConfig.InboundBean(
|
||||
tag = "dns-in",
|
||||
port = localDnsPort,
|
||||
listen = "127.0.0.1",
|
||||
protocol = "dokodemo-door",
|
||||
settings = dnsInboundSettings,
|
||||
sniffing = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// DNS outbound对象
|
||||
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
|
||||
v2rayConfig.outbounds.add(
|
||||
V2rayConfig.OutboundBean(
|
||||
protocol = "dns",
|
||||
tag = "dns-out",
|
||||
settings = null,
|
||||
streamSettings = null,
|
||||
mux = null))
|
||||
V2rayConfig.OutboundBean(
|
||||
protocol = "dns",
|
||||
tag = "dns-out",
|
||||
settings = null,
|
||||
streamSettings = null,
|
||||
mux = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// DNS routing tag
|
||||
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
|
||||
type = "field",
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, V2rayConfig.RoutingBean.RulesBean(
|
||||
inboundTag = arrayListOf("dns-in"),
|
||||
outboundTag = "dns-out",
|
||||
domain = null)
|
||||
domain = null
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -321,44 +425,77 @@ object V2rayConfigUtil {
|
||||
try {
|
||||
val hosts = mutableMapOf<String, String>()
|
||||
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 = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
||||
?: ""
|
||||
)
|
||||
remoteDns.forEach {
|
||||
servers.add(it)
|
||||
}
|
||||
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
|
||||
val directDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
||||
?: "")
|
||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: "0"
|
||||
if (directDomain.size > 0 || routingMode == "2" || routingMode == "3") {
|
||||
val domesticDns = Utils.getDomesticDnsServers()
|
||||
val geositeCn = arrayListOf("geosite:cn")
|
||||
val geoipCn = arrayListOf("geoip:cn")
|
||||
if (directDomain.size > 0) {
|
||||
servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, directDomain, geoipCn))
|
||||
}
|
||||
if (routingMode == "2" || routingMode == "3") {
|
||||
servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, geositeCn, geoipCn))
|
||||
}
|
||||
if (Utils.isPureIpAddress(domesticDns.first())) {
|
||||
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
|
||||
type = "field",
|
||||
outboundTag = AppConfig.TAG_DIRECT,
|
||||
port = "53",
|
||||
ip = arrayListOf(domesticDns.first()),
|
||||
domain = null)
|
||||
val domesticDns = Utils.getDomesticDnsServers()
|
||||
val directDomain = userRule2Domain(
|
||||
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")
|
||||
|
||||
if (directDomain.size > 0) {
|
||||
servers.add(
|
||||
V2rayConfig.DnsBean.ServersBean(
|
||||
domesticDns.first(),
|
||||
53,
|
||||
directDomain,
|
||||
if (isCnRoutingMode) geoipCn else null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isCnRoutingMode) {
|
||||
val geositeCn = arrayListOf("geosite: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 = userRule2Domain(
|
||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
||||
?: ""
|
||||
)
|
||||
if (blkDomain.size > 0) {
|
||||
hosts.putAll(blkDomain.map { it to "127.0.0.1" })
|
||||
}
|
||||
@@ -368,17 +505,19 @@ object V2rayConfigUtil {
|
||||
|
||||
// DNS dns对象
|
||||
v2rayConfig.dns = V2rayConfig.DnsBean(
|
||||
servers = servers,
|
||||
hosts = hosts)
|
||||
servers = servers,
|
||||
hosts = hosts
|
||||
)
|
||||
|
||||
// DNS routing
|
||||
if (Utils.isPureIpAddress(remoteDns.first())) {
|
||||
v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean(
|
||||
type = "field",
|
||||
outboundTag = AppConfig.TAG_AGENT,
|
||||
v2rayConfig.routing.rules.add(
|
||||
0, V2rayConfig.RoutingBean.RulesBean(
|
||||
outboundTag = TAG_PROXY,
|
||||
port = "53",
|
||||
ip = arrayListOf(remoteDns.first()),
|
||||
domain = null)
|
||||
domain = null
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -388,10 +527,49 @@ object V2rayConfigUtil {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun httpRequestObject(outbound: V2rayConfig.OutboundBean): Boolean {
|
||||
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
|
||||
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
|
||||
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP) {
|
||||
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP
|
||||
) {
|
||||
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
||||
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
||||
|
||||
@@ -408,9 +586,10 @@ object V2rayConfigUtil {
|
||||
} else {
|
||||
path
|
||||
}
|
||||
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!!
|
||||
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
@@ -418,4 +597,62 @@ object V2rayConfigUtil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
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()) {
|
||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||
V2rayConfig.TLS,
|
||||
allowInsecure,
|
||||
"",
|
||||
fingerprint,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,35 +1,67 @@
|
||||
package com.v2ray.ang.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.AssetManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||
import com.v2ray.ang.R
|
||||
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.util.*
|
||||
import com.v2ray.ang.util.MessageUtil
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import com.v2ray.ang.util.SpeedtestUtil
|
||||
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) {
|
||||
private val mainStorage by lazy { 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 mainStorage by lazy {
|
||||
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 subscriptionId: String = settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "")?:""
|
||||
//var keywordFilter: String = settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
|
||||
private set
|
||||
val serversCache = ConcurrentHashMap<String, ServerConfig>()
|
||||
val serversCache = mutableListOf<ServersCache>()
|
||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||
val updateListAction by lazy { MutableLiveData<Int>() }
|
||||
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||
@@ -38,15 +70,26 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
fun startListenBroadcast() {
|
||||
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, "")
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
Utils.closeAllTcpSockets()
|
||||
Log.i(AppConfig.ANG_PACKAGE, "Main ViewModel is cleared")
|
||||
SpeedtestUtil.closeAllTcpSockets()
|
||||
Log.i(ANG_PACKAGE, "Main ViewModel is cleared")
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@@ -59,51 +102,72 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
fun removeServer(guid: String) {
|
||||
serverList.remove(guid)
|
||||
MmkvManager.removeServer(guid)
|
||||
val index = getPosition(guid)
|
||||
if (index >= 0) {
|
||||
serversCache.removeAt(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun appendCustomConfigServer(server: String) {
|
||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
||||
config.remarks = System.currentTimeMillis().toString()
|
||||
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
|
||||
val key = MmkvManager.encodeServerConfig("", config)
|
||||
serverRawStorage?.encode(key, server)
|
||||
serverList.add(key)
|
||||
serversCache[key] = config
|
||||
fun appendCustomConfigServer(server: String): Boolean {
|
||||
if (server.contains("inbounds")
|
||||
&& server.contains("outbounds")
|
||||
&& server.contains("routing")
|
||||
) {
|
||||
try {
|
||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
||||
config.subscriptionId = subscriptionId
|
||||
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
|
||||
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
||||
val key = MmkvManager.encodeServerConfig("", config)
|
||||
serverRawStorage?.encode(key, server)
|
||||
serverList.add(0, key)
|
||||
serversCache.add(0, ServersCache(key, config))
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun swapServer(fromPosition: Int, toPosition: Int) {
|
||||
Collections.swap(serverList, fromPosition, toPosition)
|
||||
Collections.swap(serversCache, fromPosition, toPosition)
|
||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateCache() {
|
||||
serversCache.clear()
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
serverList.forEach { guid ->
|
||||
MmkvManager.decodeServerConfig(guid)?.let {
|
||||
serversCache[guid] = it
|
||||
}
|
||||
for (guid in serverList) {
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||
if (subscriptionId.isNotEmpty() && subscriptionId != config.subscriptionId) {
|
||||
continue
|
||||
}
|
||||
|
||||
// if (keywordFilter.isEmpty() || config.remarks.contains(keywordFilter)) {
|
||||
serversCache.add(ServersCache(guid, config))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
fun testAllTcping() {
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
Utils.closeAllTcpSockets()
|
||||
MmkvManager.clearAllTestDelayResults()
|
||||
SpeedtestUtil.closeAllTcpSockets()
|
||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||
updateListAction.value = -1 // update all
|
||||
|
||||
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
||||
for (guid in serverList) {
|
||||
serversCache.getOrElse(guid) { MmkvManager.decodeServerConfig(guid) }?.getProxyOutbound()?.let { outbound ->
|
||||
for (item in serversCache) {
|
||||
item.config.getProxyOutbound()?.let { outbound ->
|
||||
val serverAddress = outbound.getServerAddress()
|
||||
val serverPort = outbound.getServerPort()
|
||||
if (serverAddress != null && serverPort != null) {
|
||||
tcpingTestScope.launch {
|
||||
val testResult = Utils.tcping(serverAddress, serverPort)
|
||||
val testResult = SpeedtestUtil.tcping(serverAddress, serverPort)
|
||||
launch(Dispatchers.Main) {
|
||||
MmkvManager.encodeServerTestDelayMillis(guid, testResult)
|
||||
updateListAction.value = serverList.indexOf(guid)
|
||||
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
||||
updateListAction.value = getPosition(item.guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,12 +175,112 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
}
|
||||
|
||||
fun testAllRealPing() {
|
||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
|
||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||
updateListAction.value = -1 // update all
|
||||
|
||||
val serversCopy = serversCache.toList() // Create a copy of the list
|
||||
|
||||
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
||||
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
|
||||
for (item in serversCopy) {
|
||||
val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
|
||||
if (config.status) {
|
||||
MessageUtil.sendMsg2TestService(
|
||||
getApplication(),
|
||||
AppConfig.MSG_MEASURE_CONFIG,
|
||||
Pair(item.guid, config.content)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun testCurrentServerRealPing() {
|
||||
val socksPort = 10808//Utils.parseInt(defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808"))
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val result = Utils.testConnection(getApplication(), socksPort)
|
||||
launch(Dispatchers.Main) {
|
||||
updateTestResultAction.value = result
|
||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
|
||||
}
|
||||
|
||||
fun subscriptionIdChanged(id: String) {
|
||||
if (subscriptionId != id) {
|
||||
subscriptionId = id
|
||||
settingsStorage.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
|
||||
reloadServerList()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubscriptions(context: Context) : Pair<MutableList<String>?, MutableList<String>?> {
|
||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||
if (subscriptionId.isNotEmpty()
|
||||
&& !subscriptions.map { it.first }.contains(subscriptionId)
|
||||
) {
|
||||
subscriptionIdChanged("")
|
||||
}
|
||||
if (subscriptions.isEmpty()) {
|
||||
return null to null
|
||||
}
|
||||
val listId = subscriptions.map { it.first }.toMutableList()
|
||||
listId.add(0, "")
|
||||
val listRemarks = subscriptions.map { it.second.remarks }.toMutableList()
|
||||
listRemarks.add(0, context.getString(R.string.filter_config_all))
|
||||
|
||||
return listId to listRemarks
|
||||
}
|
||||
|
||||
fun getPosition(guid: String): Int {
|
||||
serversCache.forEachIndexed { index, it ->
|
||||
if (it.guid == guid)
|
||||
return index
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,20 +291,34 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
AppConfig.MSG_STATE_RUNNING -> {
|
||||
isRunning.value = true
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||
getApplication<AngApplication>().toast(R.string.toast_services_success)
|
||||
isRunning.value = true
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_START_FAILURE -> {
|
||||
getApplication<AngApplication>().toast(R.string.toast_services_failure)
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
|
||||
updateTestResultAction.value = intent.getStringExtra("content")
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
||||
val resultPair = intent.getSerializableExtra("content") as Pair<String, Long>
|
||||
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
||||
updateListAction.value = getPosition(resultPair.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user