diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..8c5a92fe --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,159 @@ +name: Build APK + +on: + workflow_dispatch: + inputs: + release_tag: + required: false + type: string + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4.2.2 + with: + submodules: 'recursive' + fetch-depth: '0' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3.2.0 + with: + log-accepted-android-sdk-licenses: false + cmdline-tools-version: '12266719' + packages: 'platforms;android-35 build-tools;35.0.0 platform-tools' + + - name: Install NDK + run: | + echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \ + --channel=3 \ + --install "ndk;29.0.13113456" + echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.13113456" >> $GITHUB_ENV + sed -i '10i\ + \ + ndkVersion = "29.0.13113456"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts + + - name: Restore cached libtun2socks + id: cache-libtun2socks-restore + uses: actions/cache/restore@v4 + with: + path: ${{ github.workspace }}/libs + key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }} + + - name: Build libtun2socks + if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true' + run: | + bash compile-tun2socks.sh + + - name: Save libtun2socks + if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/libs + key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }} + + - name: Copy libtun2socks + run: | + cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app + + - name: Fetch AndroidLibXrayLite tag + run: | + pushd AndroidLibXrayLite + CURRENT_TAG=$(git describe --tags --abbrev=0) + echo "Current tag in this repo: $CURRENT_TAG" + echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV + popd + + - name: Download libv2ray + uses: robinraju/release-downloader@v1.12 + with: + repository: '2dust/AndroidLibXrayLite' + tag: ${{ env.CURRENT_TAG }} + fileName: 'libv2ray.aar' + out-file-path: V2rayNG/app/libs/ + + - name: Restore cached libhysteria2 + id: cache-libhysteria2-restore + uses: actions/cache/restore@v4 + with: + path: ${{ github.workspace }}/hysteria/libs + key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }} + + - name: Setup Golang + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' + uses: actions/setup-go@v5.4.0 + with: + go-version-file: 'AndroidLibXrayLite/go.mod' + cache: false + + - name: Build libhysteria2 + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' + run: | + bash libhysteria2.sh + + - name: Save libhysteria2 + if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ github.workspace }}/hysteria/libs + key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }} + + - name: Copy libhysteria2 + run: | + cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app + + - name: Setup Java + uses: actions/setup-java@v4.7.0 + with: + distribution: 'temurin' + java-version: '21' + + - name: Decode Keystore + uses: timheuer/base64-to-file@v1.2.4 + id: android_keystore + with: + fileName: "android_keystore.jks" + encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }} + + - name: Build APK + run: | + cd ${{ github.workspace }}/V2rayNG + echo "sdk.dir=${ANDROID_HOME}" > local.properties + chmod 755 gradlew + ./gradlew licenseFdroidReleaseReport + ./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }} + + - name: Upload arm64-v8a APK + uses: actions/upload-artifact@v4.6.2 + if: ${{ success() }} + with: + name: arm64-v8a + path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk + + - name: Upload armeabi-v7a APK + uses: actions/upload-artifact@v4.6.2 + if: ${{ success() }} + with: + name: armeabi-v7a + path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk + + - name: Upload x86 APK + uses: actions/upload-artifact@v4.6.2 + if: ${{ success() }} + with: + name: x86-apk + path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*x86*.apk + + - name: Upload to release + uses: svenstaro/upload-release-action@v2 + if: github.event.inputs.release_tag != '' + with: + file: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*playstore*/release/*.apk + tag: ${{ github.event.inputs.release_tag }} + file_glob: true + prerelease: true diff --git a/.github/workflows/fastlane.yml b/.github/workflows/fastlane.yml new file mode 100644 index 00000000..4ed66e23 --- /dev/null +++ b/.github/workflows/fastlane.yml @@ -0,0 +1,16 @@ +name: Validate Fastlane metadata + +on: + workflow_dispatch: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate Fastlane Supply Metadata + uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0 diff --git a/.gitignore b/.gitignore index 7c701c49..9582b273 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,66 @@ -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 +# Ignore data and key store files *.dat *.jks + +# Ignore output JSON file V2rayNG/app/release/output.json + +# Ignore IDE and build system directories .idea/ -.gradle/ \ No newline at end of file +.gradle/ +*.iml + +# Ignore local properties and DS_Store files +/local.properties +.DS_Store + +# Ignore build directories and captures +/build +/captures +V2rayNG/app/build +V2rayNG/build +V2rayNG/local.properties + +# Ignore APK and AAR files +*.apk +*.aar + +# Ignore signing properties +signing.properties + +# Ignore shared object files +*.so + +# Ignore Google services JSON +V2rayNG/app/google-services.json + +# Additional common Android/Java ignores +*.log +*.tmp +*.bak +*.swp +*.orig +*.class +*.jar +*.war +*.ear + +# Ignore executable files +*.exe +*.dll +*.obj +*.o +*.pyc +*.pyo + +# Ignore files from other IDEs +.vscode/ +.classpath +.project +.settings/ +*.sublime-workspace +*.sublime-project + +# Ignore OS-specific files +Thumbs.db +.DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..2bdafca3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "hysteria"] + path = hysteria + url = https://github.com/apernet/hysteria +[submodule "AndroidLibXrayLite"] + path = AndroidLibXrayLite + url = https://github.com/2dust/AndroidLibXrayLite +[submodule "badvpn"] + path = badvpn + url = https://github.com/XTLS/badvpn +[submodule "libancillary"] + path = libancillary + url = https://github.com/shadowsocks/libancillary diff --git a/AndroidLibV2rayLite/.gitignore b/AndroidLibV2rayLite/.gitignore deleted file mode 100644 index db2c52e0..00000000 --- a/AndroidLibV2rayLite/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.jar -*.aar diff --git a/AndroidLibV2rayLite/CoreI/Status.go b/AndroidLibV2rayLite/CoreI/Status.go deleted file mode 100644 index 8d025a1c..00000000 --- a/AndroidLibV2rayLite/CoreI/Status.go +++ /dev/null @@ -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 -} diff --git a/AndroidLibV2rayLite/Makefile b/AndroidLibV2rayLite/Makefile deleted file mode 100644 index 620df98b..00000000 --- a/AndroidLibV2rayLite/Makefile +++ /dev/null @@ -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 diff --git a/AndroidLibV2rayLite/Process/Escort/escort.go b/AndroidLibV2rayLite/Process/Escort/escort.go deleted file mode 100644 index b8676cdd..00000000 --- a/AndroidLibV2rayLite/Process/Escort/escort.go +++ /dev/null @@ -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 -} diff --git a/AndroidLibV2rayLite/README.md b/AndroidLibV2rayLite/README.md deleted file mode 100644 index 6d7fa28b..00000000 --- a/AndroidLibV2rayLite/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# AndroidLibV2rayLite - -进入`AndroidLibV2rayLite`文件夹 - -``` -go mod download -``` - - - -编译aar - -``` -gomobile bind -v -o android.aar -target=android ./ -``` - diff --git a/AndroidLibV2rayLite/VPN/vpnservice_support.go b/AndroidLibV2rayLite/VPN/vpnservice_support.go deleted file mode 100644 index 5647815b..00000000 --- a/AndroidLibV2rayLite/VPN/vpnservice_support.go +++ /dev/null @@ -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 -} diff --git a/AndroidLibV2rayLite/VPN/vpnservice_support_test.go b/AndroidLibV2rayLite/VPN/vpnservice_support_test.go deleted file mode 100644 index 9cacbde2..00000000 --- a/AndroidLibV2rayLite/VPN/vpnservice_support_test.go +++ /dev/null @@ -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()) - }) - } -} diff --git a/AndroidLibV2rayLite/gen_assets.sh b/AndroidLibV2rayLite/gen_assets.sh deleted file mode 100644 index 3a102fa2..00000000 --- a/AndroidLibV2rayLite/gen_assets.sh +++ /dev/null @@ -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 diff --git a/AndroidLibV2rayLite/go.mod b/AndroidLibV2rayLite/go.mod deleted file mode 100644 index 83648e61..00000000 --- a/AndroidLibV2rayLite/go.mod +++ /dev/null @@ -1,49 +0,0 @@ -module github.com/2dust/AndroidLibV2rayLite - -go 1.18 - -require ( - github.com/xtls/xray-core v1.5.7 - golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd - golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 -) - -require ( - github.com/cheekybits/genny v1.0.0 // indirect - github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect - github.com/francoispqt/gojay v1.2.13 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect - github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - github.com/klauspost/cpuid/v2 v2.0.12 // indirect - github.com/lucas-clemente/quic-go v0.27.1 // indirect - github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect - github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect - github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pires/go-proxyproto v0.6.2 // indirect - github.com/refraction-networking/utls v1.1.0 // indirect - github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect - github.com/sagernet/sing v0.0.0-20220605012533-e0f722558141 // indirect - github.com/sagernet/sing-shadowsocks v0.0.0-20220605012719-1e22882ea853 // indirect - github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect - github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect - github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect - go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect - golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.11-0.20220325154526-54af36eca237 // indirect - golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect - google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect - google.golang.org/grpc v1.47.0 // indirect - google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - lukechampine.com/blake3 v1.1.7 // indirect -) diff --git a/AndroidLibV2rayLite/go.sum b/AndroidLibV2rayLite/go.sum deleted file mode 100644 index 7699875f..00000000 --- a/AndroidLibV2rayLite/go.sum +++ /dev/null @@ -1,408 +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/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -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/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -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/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -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/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -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/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -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/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -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/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-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= -github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= -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.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -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/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.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -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/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -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/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -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/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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -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/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -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/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.27.1 h1:sOw+4kFSVrdWOYmUjufQ9GBVPqZ+tu+jMtXxXNmRJyk= -github.com/lucas-clemente/quic-go v0.27.1/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= -github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ= -github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= -github.com/marten-seemann/qtls-go1-17 v0.1.1 h1:DQjHPq+aOzUeh9/lixAGunn6rIOQyWChPSI4+hgW7jc= -github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= -github.com/marten-seemann/qtls-go1-18 v0.1.1 h1:qp7p7XXUFL7fpBvSS1sWD+uSqPvzNQK43DH+/qEkj0Y= -github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -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/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= -github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= -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/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/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/refraction-networking/utls v1.1.0 h1:dKXJwSqni/t5csYJ+aQcEgqB7AMWYi6EUc9u3bEmjX0= -github.com/refraction-networking/utls v1.1.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/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/sagernet/sing v0.0.0-20220605012533-e0f722558141 h1:YN9EeHRIFYKei1woz2OucLTSpiNrULAHH+jl6upDemQ= -github.com/sagernet/sing v0.0.0-20220605012533-e0f722558141/go.mod h1:w2HnJzXKHpD6F5Z/9XlSD4qbcpHY2RSZuQnFzqgELMg= -github.com/sagernet/sing-shadowsocks v0.0.0-20220605012719-1e22882ea853 h1:t1pn8v3kPvlaST/fY7GsPV8yIUFJDXzNcmSYQ+nrbEM= -github.com/sagernet/sing-shadowsocks v0.0.0-20220605012719-1e22882ea853/go.mod h1:Afu8UIFlEwxTnlidTMSWqWHIduAWOrwY0tFr2LxBACQ= -github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= -github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -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/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -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/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -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/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -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/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -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/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -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/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -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/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -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.7 h1:xQqUpRtzVHL/to2sDmvgn9GelD0jIwC0I8B36hbDgDo= -github.com/xtls/xray-core v1.5.7/go.mod h1:H4q5qoCwtsYeF06Zsb0tk8oV8UuaQ9NdnOaPv49Cj7I= -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.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd h1:Uo/x0Ir5vQJ+683GXB9Ug+4fcjsbp7z7Ul8UaZbhsRM= -go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -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-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/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/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/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/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-20220518205345-8578da9835fd h1:x1GptNaTtxPAlTVIAJk61fuXg0y17h09DTxyb+VNC/k= -golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -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.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -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-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-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= -golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= -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-20210119212857-b64e53b001e4/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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0= -golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.11-0.20220325154526-54af36eca237 h1:mAhaIX1KEgotq+ju3XYdXUHvll7bzJDTgiDzIAKDdPc= -golang.org/x/tools v0.1.11-0.20220325154526-54af36eca237/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -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/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/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-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM= -google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= -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.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -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/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -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/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= -lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/AndroidLibV2rayLite/interact.go b/AndroidLibV2rayLite/interact.go deleted file mode 100644 index 0035bb4e..00000000 --- a/AndroidLibV2rayLite/interact.go +++ /dev/null @@ -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()) -} diff --git a/AndroidLibV2rayLite/libv2ray.go b/AndroidLibV2rayLite/libv2ray.go deleted file mode 100644 index 8692373d..00000000 --- a/AndroidLibV2rayLite/libv2ray.go +++ /dev/null @@ -1,3 +0,0 @@ -package libv2ray - -//go:generate make all diff --git a/AndroidLibV2rayLite/readme.txt b/AndroidLibV2rayLite/readme.txt deleted file mode 100644 index b2b2a781..00000000 --- a/AndroidLibV2rayLite/readme.txt +++ /dev/null @@ -1 +0,0 @@ -readme.txt diff --git a/AndroidLibV2rayLite/ubuntu-cli-install-android-sdk.sh b/AndroidLibV2rayLite/ubuntu-cli-install-android-sdk.sh deleted file mode 100644 index 126d74ba..00000000 --- a/AndroidLibV2rayLite/ubuntu-cli-install-android-sdk.sh +++ /dev/null @@ -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 diff --git a/AndroidLibV2rayLite/v2logger.go b/AndroidLibV2rayLite/v2logger.go deleted file mode 100644 index 8d63fdd5..00000000 --- a/AndroidLibV2rayLite/v2logger.go +++ /dev/null @@ -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)} - } -} diff --git a/AndroidLibXrayLite b/AndroidLibXrayLite new file mode 160000 index 00000000..8ad3e1dd --- /dev/null +++ b/AndroidLibXrayLite @@ -0,0 +1 @@ +Subproject commit 8ad3e1ddf165d8d67e488346b2faa9153d3e33a4 diff --git a/CR.md b/CR.md index 0ec148b6..b115f898 100644 --- a/CR.md +++ b/CR.md @@ -1,29 +1,40 @@ -v2rayNG 隐私条款 +**v2rayNG 隐私权政策** -最后更新 2017-11-22 +本政策自2023年11月17日起施行 -v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。** +2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。 -1. 信息收集 +v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。** - v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。 +**信息收集** - v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。 +v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。 - 当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。** +v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。 -2. 信息共享 +当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。** - 我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。 +**信息共享** -3. 信息存留 +我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。 - 除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。 +**信息存留** -4. 信息泄露 +除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。 - 我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。** +**信息泄露** -5. 条款修改 +我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。** + +**儿童隐私** + +这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。 + +**条款修改** + +我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。** + +**联系我们** + +如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。 - 我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。** diff --git a/ExtRes/values-vi/strings.xml b/ExtRes/values-vi/strings.xml deleted file mode 100644 index e9f53f4d..00000000 --- a/ExtRes/values-vi/strings.xml +++ /dev/null @@ -1,228 +0,0 @@ - - - Kết nối ngay - Kết nối ngay - Vui lòng thêm một cấu hình vào v2rayNG để sử dụng. - Mở menu ứng dụng - Đóng menu ứng dụng - Đã chuyển dữ liệu! - Không thể chuyển dữ liệu! - - - Ngắt kết nối v2rayNG - Vui lòng cấp quyền cần thiết cho v2rayNG. Bạn đã từ chối các quyền cần thiết như Camera hay Bộ nhớ. - Nhấn để biết thêm - Đang bắt đầu dịch vụ v2rayNG. - Đã dừng dịch vụ v2rayNG. - Đã bắt đầu dịch vụ v2rayNG. - Không thể bắt đầu dịch vụ, hãy thử kiểm tra lại cấu hình hoặc khởi động lại thiết bị. - - - V2RayNG App :3 - Thêm cấu hình - Lưu cấu hình - Xoá cấu hình - Nhập cấu hình từ mã QR - Nhập cấu hình từ bộ nhớ tạm thời - Nhập thủ công [Vmess] - Nhập thủ công [VLESS] - Nhập thủ công [Shadowsocks] - Nhập thủ công [Socks] - Nhập thủ công [Trojan] - Nâng cao / Cấu hình tùy chỉnh - Nhập cấu hình tùy chỉnh từ bộ nhớ tạm thời - Nhập cấu hình tùy chỉnh từ Tệp - Nhập cấu hình tùy chỉnh từ URL - Nhập cấu hình tùy chỉnh quét URL - Bạn có muốn xóa cấu hình ? - Tên cấu hình - Địa chỉ - Cổng - Địa chỉ ID - alterId - Bảo mật - Kiểu kết nối - Nâng cao - Kiểu Head - Chế độ gRPC - Yêu cầu host(host/ws host/h2 host)/Bảo mật QUIC - Đường dẫn (ws path/h2 path)/QUIC key/kcp seed/gRPC serviceName - tls - allowInsecure - Địa chỉ SNI - Địa chỉ - Cổng - Mật khẩu - Bảo mật - Mật khẩu(Bổ sung) - Tên người dùng(Bổ sung) - Mã hoá - flow - Đã thực hiện thành công thao tác của bạn, Nếu có gì đó không ổn, hãy thao tác lại. - Đã xảy ra lỗi, hãy thử kiểm tra lại hoặc thử lại. - Không có gì ở đây - Không đúng protocol - Không thể decode - Vui lòng chọn tệp cấu hình - Vui lòng cài đặt trình quản lý tệp để tiếp tục. - Cấu hình tùy chỉnh - Cấu hình không hợp lệ - Nội dung - Không có dữ liệu nào trong bộ nhớ tạm thời - URL không hợp lệ hoặc không có gì - Vui lòng đảm bảo cấu hình tùy chỉnh này không bị lỗi trước khi sử dụng. v2rayNG được Dịch Tiếng Việt bởi CuynuTT😘 - Cấu hình không hợp lệ - Host(SNI)(Bổ sung) - Không thể sao chép tệp tin, hãy dùng trình quản lý tệp - Thêm tệp - Tải xuống tệp tin - - - Đang tải... - Tìm kiếm - Chọn tất cả - Nhập từ khoá - Bỏ qua kết nối VPN - Tự động chọn ứng dụng Proxy - Đang tải xuống nội dung... - Xuất và sao chép - Nhập từ bộ nhớ tạm thời - - - - Cài đặt - Cài đặt nâng cao - Cài đặt cho VPN - Proxy cho ứng dụng - Chung: Ứng dụng đã chọn sẽ kết nối Proxy, Chưa lựa chọn sẽ kết nối trực tiếp; \nBỏ qua kết nối: Ứng dụng được chọn sẽ trực tiếp kết nối, không lựa chọn Proxy. \nLựa chọn để tự động chọn ứng dụng Proxy trong Menu. - - Cho phép Mux - Bật lên có thể làm tăng tốc độ mạng và chuyển mạng nhanh hơn. - - Cho phép hiển thị tốc độ mạng - Hiển thị tốc độ mạng hiện tại trên thanh thông báo.\nBiểu tượng trên thanh trạng thái có thể thay đổi tùy vào mức sử dụng. - - Cho phép Sniffing - Thử chuyển kết nối hiện tại của bạn qua trung gian để trung gian xử lý kết nối về lại cho bạn (Mặc định là bật, hãy tắt nó nếu kết nối không ổn định.) - - Cho phép DNS cục bộ - DNS được xử lý bởi mô đun của lõi DNS. -(Khuyến cáo, nếu cần lộ trình Bẻ khoá LAN và - địa chỉ mainland) - - Cho phép DNS giả - DNS cục bộ trả về địa chỉ IP giả (Nhanh hơn, nhưng có thể không hoạt động với một số ứng dụng) - - Ưu tiên IPv6 - Ưu tiên sử dụng địa chỉ IPv6 cho kết nối và lộ trình. - - Lộ trình - Tùy chọn tên miền - Tùy chỉnh quy tắc lộ trình - Tùy chỉnh lộ trình - - Điều khiển DNS (Bổ sung) - DNS - - VPN DNS (Chỉ IPv4/v6) - - Domestic DNS (Bổ sung) - DNS - - Cho phép kết nối từ mạng LAN - Các thiết bị khác có thể kết nối đến proxy bởi địa chỉ IP thông qua socks/http, Chỉ bật khi bạn tin tưởng kết nối để tránh kết nối lạ. - Cho phép kết nối từ mạng LAN, Đảm bảo rằng bạn tin tưởng kết nối hiện tại. - - Cho phép đặt lại allowInsecure - Khi kết nối TLS, đặt cài đặt allowInsecure thành mặc định - - Cổng Proxy SOCKS5 - Cổng Proxy SOCKS5 - - Cổng Proxy HTTP - Cổng Proxy HTTP - - Cổng DNS cục bộ - Cổng DNS cục bộ - - Hiển thị thông báo xác nhận xoá cấu hình - Hiển thị thông báo xác nhận xoá cấu hình khi bạn xoá một cấu hình. - - Phản hồi lỗi - Phản hồi cải tiến hoặc bug lên GitHub - Tham gia nhóm Telegram - Không tìm thấy ứng dụng Telegram - - Quảng cáo Server - Quảng cáo,nhấn để biết thêm(Ủng hộ có thể được gỡ bỏ) - - Mức độ nhật ký - Chế độ kết nối - Nhấn vào đây nếu bạn cần trợ giúp - Ngôn ngữ ứng dụng - - Nhật ký hoạt động - Sao chép nhật ký - Xoá nhật ký - Kết nối lại v2rayNG - Xoá tất cả cấu hình - Xoá cấu hình lỗi (Kiểm tra trước) - Xuất và sao chép tất cả cấu hình - Các gói đăng ký - Tên các gói đăng ký - URL gói đăng ký - Sử dụng gói đăng ký này - Cập nhật các gói đăng ký - Ping tất cả máy chủ - Kiểm tra máy chủ - Tệp Geo assets - Sắp xếp lại theo lần kiểm tra cuối cùng - Lọc cấu hình theo các gói đăng ký - Hiển thị tất cả các gói đăng ký - - Bắt đầu dịch vụ - Xác nhận - - Cài đặt lộ trình - Được phân cách bằng dấu chấm phẩy(,),Hãy nhớ nó để lưu lại. - Lưu lại - Xoá - Dò và thay thế - Dò và nối - Đặt luật lệ lộ trình mặc định - - Kiểm tra kết nối - Đang kiểm tra kết nối mạng… - Đã kiểm tra kết nối mạng thành công, Ping hiện tại là %d - Lỗi kết nối mạng hãy thử đổi cấu hình hoặc kiểm tra lại. Mã lỗi: %s - Không có kết nối mạng - Mã lỗi: #%d - Đã kết nối, hãy nhấn vào đây để kiểm tra kết nối mạng. - Chưa kết nối, hãy thêm một cấu hình để kết nối. Đừng để bị lừa đảo bởi cấu hình mất tiền,Dịch TV bởi CuynuTT😘 - - - Xuất ra mã QR (Chụp màn hình để lưu) - Sao chép cấu hình này - Sao chép thành cấu hình tùy chỉnh - - - - proxy URL hoặc IP - direct URL hoặc IP - URL đã chặn hoặc IP - - - - Proxy Global - Bẻ khoá địa chỉ LAN rồi proxy - Bẻ khoá địa chỉ mainland rồi proxy - Bẻ khoá LAN và địa chỉ mainland rồi proxy - Trực tiếp Global - - - - Chế độ VPN - Chế độ Proxy - - - diff --git a/ExtRes/values-zh-rCN/strings.xml b/ExtRes/values-zh-rCN/strings.xml deleted file mode 100644 index 7c12fd00..00000000 --- a/ExtRes/values-zh-rCN/strings.xml +++ /dev/null @@ -1,226 +0,0 @@ - - - 开关 - 开关 - 初次使用此功能请先用APP添加配置 - Open navigation drawer - Close navigation drawer - 数据迁移成功! - 数据迁移失败啦! - - - 停止 - 无法取得权D:\vssHotel\SourceCode\Hotel.root\Hotel\Clubank.Hotel\FrontCounter\CheckoutListForm.cs限 - 点击了解更多 - 启动服务中 - 关闭中 - 启动服务成功 - 启动服务失败 - - - 配置文件 - 添加配置 - 保存配置 - 删除配置 - 扫描二维码 - 从剪贴板导入 - 手动输入[Vmess] - 手动输入[VLESS] - 手动输入[Shadowsocks] - 手动输入[Socks] - 手动输入[Trojan] - 自定义配置 - 从剪贴板导入自定义配置 - 从本地导入自定义配置 - 剪贴板URL导入自定义配置 - 扫描URL导入自定义配置 - 确认删除? - 别名(remarks) - 地址(address) - 端口(port) - 用户ID(id) - 额外ID(alterId) - 加密方式(security) - 传输协议(network) - 底层传输方式(transport) - 伪装类型(type) - gRPC 传输模式 (mode) - 伪装域名(host)(host/ws host/h2 host)/QUIC 加密方式 - path(ws path/h2 path)/QUIC 加密密钥/kcp seed/gRPC serviceName - 传输层安全(tls) - 跳过证书验证(allowInsecure) - SNI - 服务器地址 - 服务器端口 - 密码 - 加密方式 - 密码(可选) - 用户名(可选) - 加密(encryption) - 流控(flow) - 成功 - 失败 - 没有数据 - 不正确的协议 - 解码失败 - 选择一个配置文件 - 请安装一个文件管理器 - 自定义配置 - 无效的配置文件 - 内容 - 剪贴板中没有数据 - 无效的网址 - 确保inbounds port和设置中的一致 - 配置格式错误 - Host(SNI)(可选) - 失败, 请使用文件管理器 - 添加文件 - 下载文件 - - - - 正在加载 - 搜索 - 全选 - 输入关键字 - 绕行模式 - 自动选中需代理应用 - 正在下载内容 - 导出至剪贴板 - 从剪贴板导入 - - - 设置 - 进阶设置 - VPN 设置 - 分应用代理 - 常规:勾选的App被代理,未勾选的直连;\n绕行模式:勾选的App直连,未勾选的被代理.\n不明白者在菜单中选择自动选中需代理应用 - - 启用Mux多路复用 - 开启可能会加速,关闭可能会减少断流 - - 启用速度显示 - 在通知中显示当前速度\n小图标显示流量的路由情况 - - 启用流量探测 - 从流量中探测域名 (默认启用) - - 启用本地DNS - DNS 请求导入 core 由 DNS 模块处理(推荐启用 如果需要路由绕过局域网及大陆地址) - - 启用虚拟DNS - 本地返回虚构解析结果 (减低延时 但个别应用可能无法使用) - - IPv6优先 - App优先使用IPv6地址连接服务器,同时开启VPN的IPv6路由 - - 路由设置 - 域名策略 - 预定义规则 - 自定义规则 - - 远程DNS (可选) - DNS - - VPN DNS (仅支持 IPv4/v6) - - 境内DNS (可选) - DNS - - 允许来自局域网的连接 - 其他设备可以使用socks/http协议通过您的IP地址连接到代理,仅在受信任的网络中启用以避免未经授权的连接 - 允许来自局域网的连接,请确保处于受信网络 - - 跳过证书验证(allowInsecure) - 传输层安全选tls时,默认跳过证书验证(allowInsecure) - - SOCKS5代理端口 - SOCKS5代理端口 - - HTTP代理端口 - HTTP代理端口 - - 本地DNS端口 - 本地DNS端口 - - 删除配置文件确认 - 删除配置文件是否需要用户二次确认 - - 反馈 - 反馈改进或漏洞至 GitHub - 加入Telegram Group - 未找到Telegram app - - 推广 - 一些推广,点击查看详情(捐赠可去除) - - 日志级别 - 模式 - 点此查看更多帮助 - 语言 - - Logcat - 复制 - 清除 - 服务重启 - 删除全部配置 - 删除无效配置(先测试) - 导出全部(非自定义)配置至剪贴板 - 订阅分组设置 - 备注 - 可选地址(url) - 启用更新 - 更新订阅 - 测试全部配置Tcping - 测试全部配置真连接 - Geo 资源文件 - 按测试结果排序 - 过滤配置文件 - 所有订阅分组 - - 启动服务 - 确定 - - 路由设置 - 用逗号(,)隔开,可以一行多个,记得保存 - 保存 - 清空 - 扫描并替换 - 扫描并追加 - 设置默认路由规则 - - "检查网络连接" - "测试中…" - "连接成功:延时 %d 毫秒" - "失败:%s" - "无互联网连接" - "状态码无效(#%d)" - "已连接,点击测试连接" - "未连接" - - - 二维码 - 导出至剪贴板 - 导出完整配置至剪贴板 - - share_method - - 代理的网址或IP - 直连的网址或IP - 阻止的网址或IP - - - - 全局代理 - 绕过局域网地址而后代理 - 绕过大陆地址而后代理 - 绕过局域网及大陆地址而后代理 - 全局直连 - - - - VPN - 仅代理 - - - diff --git a/ExtRes/values-zh-rTW/strings.xml b/ExtRes/values-zh-rTW/strings.xml deleted file mode 100644 index 927e1f05..00000000 --- a/ExtRes/values-zh-rTW/strings.xml +++ /dev/null @@ -1,226 +0,0 @@ - - - 開關 - 開關 - 首次使用此功能,請使用此應用程式新增伺服器 - 開啟導覽匣 - 關閉導覽匣 - 資料遷移成功! - 資料遷移失敗! - - - 停止 - 無法取得此權限 - 瞭解更多 - 啟動服務 - 停止服務 - 啟動服務成功 - 啟動服務失敗 - - - 組態檔案 - 新增組態 - 儲存組態 - 刪除組態 - 從 QR Code 匯入組態 - 從剪貼簿匯入組態 - 手動鍵入 [Vmess] - 手動鍵入 [VLESS] - 手動鍵入 [Shadowsocks] - 手動鍵入 [Socks] - 手動鍵入 [Trojan] - 自訂組態 - 從剪貼簿匯入自訂組態 - 從 URL 匯入自訂組態 - 從 URL 匯入自訂組態 - 掃描 URL 匯入自訂組態 - 確定刪除? - 備註 - 位址 - - 使用者 ID - alterId - 安全性 - 網路 - 底層傳輸方式 (transport) - 標頭類型 - gRPC 傳輸模式 (mode) - 要求主機 (host)(host/ws host/h2 host)/QUIC 加密方式 - path(ws path/h2 path)/QUIC 加密金鑰/kcp seed/gRPC serviceName - 傳輸層安全 (tls) - 跳過憑證驗證 (allowInsecure) - SNI - 伺服器位址 - 伺服器埠 - 密碼 - 加密方式 - 密碼 (可選) - 使用者名稱 (可選) - 加密 (encryption) - 流程 (flow) - 成功 - 失敗 - 無資料 - 通訊協定不正確 - 解碼失敗 - 選取一個組態檔 - 請安裝檔案總管。 - 自訂組態 - 無效組態 - 內容 - 剪貼簿內無資料 - URL 無效 - ​​確保 inbounds port 和設定中的一致 - 組態格式不正確 - Host(SNI)(可選) - 失敗,請使用檔案總管 - 新增檔案 - 下載檔案 - - - 載入 - 搜尋 - 全選 - 輸入關鍵字 - 略過模式 - 自動選中需 Proxy 應用 - 正在下載內容 - 匯出至剪貼簿 - 從剪貼簿匯入 - - - - 設定 - 進階 - VPN 設定 - Proxy 個別應用程式 - 常規:勾選的 App 啟用 Proxy,未勾選的直接連線;\n繞行模式:勾選的 App 直接連線,未勾選的啟用 Proxy。\n可在選單中選擇自動選中需 Proxy 應用 - - 啟用 Mux - 啟用或許會加快網路速度,切換或許會閃爍 - - 啟用速度顯示 - 在通知中顯示當前速度\n小圖示顯示流量的轉送狀況 - - 啟用流量監聽 - 從流量中監聽網域 (預設啟用) - - 啟用本機 DNS - DNS 請求匯入 core 由 DNS 模塊處理 (建議啟用,如果需要轉送略過區域網路及中國大陸) - - 啟用假 DNS - 本機退回假解析結果 (減低延時,但個別應用可能無法使用) - - IPv6 偏好 - App 優先使用 IPv6 位址連線伺服器,同时開啟 VPN 的 IPv6 路由 - - 轉送設定 - 網域策略 - 轉送模式 - 自訂轉送 - - 遠端 DNS (可選) - DNS - - VPN DNS (僅支援 IPv4/v6) - - 國內 DNS (可選) - DNS - - 允許來自區域網路的連線 - 其他裝置可以使用 socks/http 協定透過您的 IP 位址連線到 Proxy,僅在受信任的網路中啟用以避免未經授權的連線 - 允許來自區域網路的連線,請確保處於受信網路 - - 跳過憑證驗證 (allowInsecure) - 傳輸層安全選 tls 時,預設跳過憑證驗證 (allowInsecure) - - SOCKS5 Proxy 埠 - SOCKS5 Proxy 埠 - - HTTP Proxy 埠 - HTTP Proxy 埠 - - 本機 DNS 埠 - 本機 DNS 埠 - - 刪除配置文件確認 - 刪除配置文件是否需要用戶二次確認 - - 意見回饋 - 前往 GitHub 回報錯誤 - 加入 Telegram 群組 - 未找到 Telegram 應用程式 - - 推廣 - 一些推廣,輕觸以檢視 (捐贈可去除) - - 記錄層級 - 模式 - 輕觸以檢視說明 - 語言 - - Logcat - 複製 - 清除 - 服務重啟 - 刪除全部組態 - 刪除無效組態 (先偵測) - 匯出全部 (非自訂) 組態至剪貼簿 - 訂閱分組設定 - 備註 - Optional URL - 啟用更新 - 更新訂閱 - 偵測所有組態 Tcping - 偵測所有組態真延遲 - Geo 資源檔案 - 依偵測結果排序 - 過濾組態 - 所有訂閱分組 - - 啟動服務 - 確定 - - 轉送設定 - 以半形逗號「,」分隔,並手動儲存 - 儲存 - 清除 - 掃描並取代 - 掃描並附加 - 設定預設轉送規則 - - "測試連線能力" - "測試中……" - "成功:%d ms延遲" - "測試網際網路連線失敗:%s" - "無法使用網際網路" - "錯誤碼:(#%d)" - "已連線,輕觸以檢查連線能力" - "未連線" - - - QR Code - 匯出至剪貼簿 - 匯出完整組態至剪貼簿 - - - - Proxy URL 或 IP - 直接連線 URL 或 IP - 已封鎖的 URL 或 IP - - - - 全域 Proxy - 略過區域網路的 Proxy - 略過中國大陸的 Proxy - 略過區域網路及中國大陸的 Proxy - 直接連線 - - - - VPN - 僅 Proxy - - - diff --git a/ExtRes/values/strings.xml b/ExtRes/values/strings.xml deleted file mode 100644 index 6e047f2b..00000000 --- a/ExtRes/values/strings.xml +++ /dev/null @@ -1,229 +0,0 @@ - - - v2rayNG - Switch - Switch - First use of this feature, please use the app to add server - Open navigation drawer - Close navigation drawer - Data migration success! - Data migration failed! - - - Stop - Unable to obtain the permission - click for more - Start Services - Stop Services - Start Services Success - Start Services Failure - - - Configuration file - Add config - Save config - Delete config - Import config from QRcode - Import config from Clipboard - Type manually[Vmess] - Type manually[VLESS] - Type manually[Shadowsocks] - Type manually[Socks] - Type manually[Trojan] - custom config - Import custom config from Clipboard - Import custom config from locally - Import custom config from URL - Import custom config scan URL - Confirm delete? - remarks - address - port - id - alterId - security - Network - Transport - head type - gRPC mode - request host(host/ws host/h2 host)/QUIC security - path(ws path/h2 path)/QUIC key/kcp seed/gRPC serviceName - tls - allowInsecure - SNI - address - port - password - security - Password(Optional) - User(Optional) - encryption - flow - Success - Failure - There is nothing - Incorrect protocol - Decoding failed - Select a Config File - Please install a File Manager. - Customize Config - Invalid Config - Content - There is no data in the clipboard - Invalid URL - Ensure inbounds port is consistent with the settings - Config malformed - Host(SNI)(Optional) - File copy failed, please use File Manager - Add files - Download files - - - Loading - Search - Select all - Enter keywords - Bypass Mode - Auto select proxy app - Downloading content - Export to Clipboard - Import from Clipboard - - - - Settings - Advanced Settings - VPN Settings - Per-app proxy - General: Checked App is proxy, unchecked direct connection; \nbypass mode: checked app directly connected, unchecked proxy. \nThe option to automatically select the proxy application in the menu - - Enable Mux - Enable maybe speed up network and switch network maybe flash - - Enable speed display - Display current speed in the notification.\nNotification icon would change based on - usage. - - Enable Sniffing - Try sniff domain from the packet (default on) - - Enable local DNS - DNS processed by core‘s DNS module (Recommended, if need routing Bypassing LAN and - mainland address) - - Enable fake DNS - local DNS returns fake IP address (faster, but it may not work for some apps) - - Prefer IPv6 - Prefer IPv6 address and routes - - Routing - Domain strategy - Predefined rules - Custom rules - - Remote DNS (Optional) - DNS - - VPN DNS (only IPv4/v6) - - Domestic DNS (Optional) - DNS - - Allow connections from the LAN - Other devices can connect to proxy by your ip address through socks/http, Only enable in trusted network to avoid unauthorized connection - Allow connections from the LAN, Make sure you are in a trusted network - - allowInsecure - When TLS, the default allowInsecure - - SOCKS5 proxy port - SOCKS5 proxy port - - HTTP proxy port - HTTP proxy port - - Local DNS port - Local DNS port - - Delete configuration file confirmation - Whether to delete the configuration file requires a second confirmation by the user - - Feedback - Feedback enhancements or bugs to GitHub - Join Telegram Group - Telegram app not found - - Promotion - Promotion,click for details(Donation can be removed) - - Log Level - Mode - Click me for more help - Language - - Logcat - Copy - Clear - Service restart - Delete all config - Delete invalid config(Test first) - Export non-custom configs to clipboard - Subscription group setting - remarks - Optional URL - enable update - Update subscription - Tcping all configuration - Real delay all configuration - Geo asset files - Sorting by test results - Filter configuration file - All subscription groups - - Start Service - Confirm - - Routing Settings - Separated by commas(,),remember to save - Save - Clear - Scan and replace - Scan and append - set default routing rules - - Check Connectivity - Testing… - Success: HTTP connection took %dms - Fail to detect internet connection: %s - Internet Unavailable - Error code: #%d - Connected, tap to check connection - Not connected - - - QRcode - Export to clipboard - Export full configuration to clipboard - - - - proxy URL or IP - direct URL or IP - blocked URL or IP - - - - Global proxy - Bypassing the LAN address then proxy - Bypass mainland address then proxy - Bypassing LAN and mainland address then proxy - Global direct - - - - VPN - Proxy only - - - diff --git a/README.md b/README.md index ce3938a9..4bd6f8ec 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,21 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core) [![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop) -[![Kotlin Version](https://img.shields.io/badge/Kotlin-1.6.10-blue.svg)](https://kotlinlang.org) +[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.21-blue.svg)](https://kotlinlang.org) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master) [![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng) [![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases) [![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/v2rayn) - -Get it on Google Play - +### Telegram Channel +[github_2dust](https://t.me/github_2dust) ### Usage #### Geoip and Geosite - geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device) - download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy) -- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually +- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually - possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6) ### More in our [wiki](https://github.com/2dust/v2rayNG/wiki) @@ -26,8 +25,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. For WSA, VPN permission need to be granted via `appops set [package name] ACTIVATE_VPN allow` diff --git a/V2rayNG/.gitignore b/V2rayNG/.gitignore deleted file mode 100644 index 4d128e87..00000000 --- a/V2rayNG/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea -.DS_Store -/build -/captures -*.apk -signing.properties diff --git a/V2rayNG/app/.gitignore b/V2rayNG/app/.gitignore deleted file mode 100644 index 2abde4aa..00000000 --- a/V2rayNG/app/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/build -/google-services.json diff --git a/V2rayNG/app/build.gradle b/V2rayNG/app/build.gradle deleted file mode 100644 index 38b358da..00000000 --- a/V2rayNG/app/build.gradle +++ /dev/null @@ -1,121 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' - -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() - } -} diff --git a/V2rayNG/app/build.gradle.kts b/V2rayNG/app/build.gradle.kts new file mode 100644 index 00000000..1624786c --- /dev/null +++ b/V2rayNG/app/build.gradle.kts @@ -0,0 +1,191 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + id("com.jaredsburrows.license") +} + +android { + namespace = "com.v2ray.ang" + compileSdk = 35 + + defaultConfig { + applicationId = "com.v2ray.ang" + minSdk = 21 + targetSdk = 35 + versionCode = 658 + versionName = "1.10.8" + multiDexEnabled = true + + val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') + splits { + abi { + isEnable = true + reset() + if (abiFilterList != null && abiFilterList.isNotEmpty()) { + include(*abiFilterList.toTypedArray()) + } else { + include( + "arm64-v8a", + "armeabi-v7a", + "x86_64", + "x86" + ) + } + isUniversalApk = abiFilterList.isNullOrEmpty() + } + } + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + flavorDimensions.add("distribution") + productFlavors { + create("fdroid") { + dimension = "distribution" + applicationIdSuffix = ".fdroid" + buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"") + } + create("playstore") { + dimension = "distribution" + buildConfigField("String", "DISTRIBUTION", "\"Play Store\"") + } + } + + sourceSets { + getByName("main") { + jniLibs.srcDirs("libs") + } + } + + + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + applicationVariants.all { + val variant = this + val isFdroid = variant.productFlavors.any { it.name == "fdroid" } + if (isFdroid) { + val versionCodes = + mapOf( + "armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0 + ) + + variant.outputs + .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } + .forEach { output -> + val abi = output.getFilter("ABI") ?: "universal" + output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk" + if (versionCodes.containsKey(abi)) { + output.versionCodeOverride = + (100 * variant.versionCode + versionCodes[abi]!!).plus(5000000) + } else { + return@forEach + } + } + } else { + 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 + } + + packaging { + jniLibs { + useLegacyPackaging = true + } + } + +} + +dependencies { + // Core Libraries + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar")))) + + // AndroidX Core Libraries + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + implementation(libs.preference.ktx) + implementation(libs.recyclerview) + implementation(libs.androidx.swiperefreshlayout) + + // UI Libraries + implementation(libs.material) + implementation(libs.toasty) + implementation(libs.editorkit) + implementation(libs.flexbox) + + // Data and Storage Libraries + implementation(libs.mmkv.static) + implementation(libs.gson) + + // Reactive and Utility Libraries + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) + + // Language and Processing Libraries + implementation(libs.language.base) + implementation(libs.language.json) + + // Intent and Utility Libraries + implementation(libs.quickie.foss) + implementation(libs.core) + + // AndroidX Lifecycle and Architecture Components + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.livedata.ktx) + implementation(libs.lifecycle.runtime.ktx) + + // Background Task Libraries + implementation(libs.work.runtime.ktx) + implementation(libs.work.multiprocess) + + // Multidex Support + implementation(libs.multidex) + + // Testing Libraries + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + testImplementation(libs.org.mockito.mockito.inline) + testImplementation(libs.mockito.kotlin) + coreLibraryDesugaring(libs.desugar.jdk.libs) +} diff --git a/V2rayNG/app/libs/Readme.md b/V2rayNG/app/libs/Readme.md deleted file mode 100644 index 0f2efdb3..00000000 --- a/V2rayNG/app/libs/Readme.md +++ /dev/null @@ -1 +0,0 @@ -https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite diff --git a/V2rayNG/app/libs/libv2ray.aar b/V2rayNG/app/libs/libv2ray.aar deleted file mode 100644 index df2a17b3..00000000 Binary files a/V2rayNG/app/libs/libv2ray.aar and /dev/null differ diff --git a/V2rayNG/app/proguard-rules.pro b/V2rayNG/app/proguard-rules.pro index c088e544..481bb434 100644 --- a/V2rayNG/app/proguard-rules.pro +++ b/V2rayNG/app/proguard-rules.pro @@ -1,14 +1,10 @@ # 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. +# You can control the set of applied configuration files using the +# proguardFiles setting 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: @@ -16,43 +12,10 @@ # 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 +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable -# 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.** { *;} +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java b/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java deleted file mode 100644 index e221e714..00000000 --- a/V2rayNG/app/src/androidTest/java/com/v2ray/ang/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.v2ray.ang; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/dev/res/values/strings.xml b/V2rayNG/app/src/dev/res/values/strings.xml new file mode 100644 index 00000000..697ffdc5 --- /dev/null +++ b/V2rayNG/app/src/dev/res/values/strings.xml @@ -0,0 +1,4 @@ + + + v2rayNG (DEV) + \ No newline at end of file diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml index 6f38be00..00e4b747 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -1,107 +1,161 @@ + tools:ignore="MissingLeanbackLauncher"> + android:anyDensity="true" + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" + android:xlargeScreens="true" /> - - + + + + + - - - - + + - - + + + + + + android:exported="true" + android:launchMode="singleTask" + android:theme="@style/AppThemeDayNight.NoActionBar"> + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + - + android:name=".ui.CheckUpdateActivity" + android:exported="false" /> - - - - - - - - - + android:name=".ui.AboutActivity" + android:exported="false" /> @@ -111,59 +165,107 @@ + - + + + + + android:name=".receiver.WidgetProvider" + android:exported="true" + android:process=":RunSoLibV2RayDaemon"> + android:name="android.appwidget.provider" + android:resource="@xml/app_widget_provider" /> + + + + + + android:name=".service.QSTileService" + android:exported="true" + android:foregroundServiceType="specialUse" + android:icon="@drawable/ic_stat_name" + android:label="@string/app_tile_name" + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" + android:process=":RunSoLibV2RayDaemon" + tools:targetApi="24"> + + android:exported="true" + android:icon="@mipmap/ic_launcher"> + android:exported="true" + android:process=":RunSoLibV2RayDaemon" + tools:ignore="ExportedReceiver"> + + + + + + + + + diff --git a/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl deleted file mode 100644 index 2a492f78..00000000 --- a/V2rayNG/app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl +++ /dev/null @@ -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); -} diff --git a/V2rayNG/app/src/main/assets/custom_routing_black b/V2rayNG/app/src/main/assets/custom_routing_black new file mode 100644 index 00000000..f15daacd --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_black @@ -0,0 +1,142 @@ +[ + { + "remarks": "绕过bittorrent", + "outboundTag": "direct", + "protocol": [ + "bittorrent" + ] + }, + { + "remarks": "Google cn", + "outboundTag": "proxy", + "domain": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ] + }, + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "代理海外公共DNSIP", + "outboundTag": "proxy", + "ip": [ + "1.1.1.1", + "1.0.0.1", + "2606:4700:4700::1111", + "2606:4700:4700::1001", + "1.1.1.2", + "1.0.0.2", + "2606:4700:4700::1112", + "2606:4700:4700::1002", + "1.1.1.3", + "1.0.0.3", + "2606:4700:4700::1113", + "2606:4700:4700::1003", + "8.8.8.8", + "8.8.4.4", + "2001:4860:4860::8888", + "2001:4860:4860::8844", + "94.140.14.14", + "94.140.15.15", + "2a10:50c0::ad1:ff", + "2a10:50c0::ad2:ff", + "94.140.14.15", + "94.140.15.16", + "2a10:50c0::bad1:ff", + "2a10:50c0::bad2:ff", + "94.140.14.140", + "94.140.14.141", + "2a10:50c0::1:ff", + "2a10:50c0::2:ff", + "208.67.222.222", + "208.67.220.220", + "2620:119:35::35", + "2620:119:53::53", + "208.67.222.123", + "208.67.220.123", + "2620:119:35::123", + "2620:119:53::123", + "9.9.9.9", + "149.112.112.112", + "2620:fe::9", + "2620:fe::fe", + "9.9.9.11", + "149.112.112.11", + "2620:fe::11", + "2620:fe::fe:11", + "9.9.9.10", + "149.112.112.10", + "2620:fe::10", + "2620:fe::fe:10", + "77.88.8.8", + "77.88.8.1", + "2a02:6b8::feed:0ff", + "2a02:6b8:0:1::feed:0ff", + "77.88.8.88", + "77.88.8.2", + "2a02:6b8::feed:bad", + "2a02:6b8:0:1::feed:bad", + "77.88.8.7", + "77.88.8.3", + "2a02:6b8::feed:a11", + "2a02:6b8:0:1::feed:a11" + ] + }, + { + "remarks": "代理海外公共DNS域名", + "outboundTag": "proxy", + "domain": [ + "domain:cloudflare-dns.com", + "domain:one.one.one.one", + "domain:dns.google", + "domain:adguard-dns.com", + "domain:opendns.com", + "domain:umbrella.com", + "domain:quad9.net", + "domain:yandex.net" + ] + }, + { + "remarks": "代理IP", + "outboundTag": "proxy", + "ip": [ + "geoip:facebook", + "geoip:fastly", + "geoip:google", + "geoip:netflix", + "geoip:telegram", + "geoip:twitter" + ] + }, + { + "remarks": "代理GFW", + "outboundTag": "proxy", + "domain": [ + "geosite:gfw", + "geosite:greatfire" + ] + }, + { + "remarks": "最终直连", + "port": "0-65535", + "outboundTag": "direct" + } +] \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_global b/V2rayNG/app/src/main/assets/custom_routing_global new file mode 100644 index 00000000..21bfb24d --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_global @@ -0,0 +1,27 @@ +[ + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "最终代理", + "port": "0-65535", + "outboundTag": "proxy" + } +] \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_white b/V2rayNG/app/src/main/assets/custom_routing_white new file mode 100644 index 00000000..e3c360c0 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_white @@ -0,0 +1,96 @@ +[ + { + "remarks": "Google cn", + "outboundTag": "proxy", + "domain": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ] + }, + { + "remarks": "阻断udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "绕过局域网IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "绕过局域网域名", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "绕过中国公共DNSIP", + "outboundTag": "direct", + "ip": [ + "223.5.5.5", + "223.6.6.6", + "2400:3200::1", + "2400:3200:baba::1", + "119.29.29.29", + "1.12.12.12", + "120.53.53.53", + "2402:4e00::", + "2402:4e00:1::", + "180.76.76.76", + "2400:da00::6666", + "114.114.114.114", + "114.114.115.115", + "114.114.114.119", + "114.114.115.119", + "114.114.114.110", + "114.114.115.110", + "180.184.1.1", + "180.184.2.2", + "101.226.4.6", + "218.30.118.6", + "123.125.81.6", + "140.207.198.6", + "1.2.4.8", + "210.2.4.8", + "52.80.66.66", + "117.50.22.22", + "2400:7fc0:849e:200::4", + "2404:c2c0:85d8:901::4", + "117.50.10.10", + "52.80.52.52", + "2400:7fc0:849e:200::8", + "2404:c2c0:85d8:901::8", + "117.50.60.30", + "52.80.60.30" + ] + }, + { + "remarks": "绕过中国公共DNS域名", + "outboundTag": "direct", + "domain": [ + "domain:alidns.com", + "domain:doh.pub", + "domain:dot.pub", + "domain:360.cn", + "domain:onedns.net" + ] + }, + { + "remarks": "绕过中国IP", + "outboundTag": "direct", + "ip": [ + "geoip:cn" + ] + }, + { + "remarks": "绕过中国域名", + "outboundTag": "direct", + "domain": [ + "geosite:cn" + ] + } +] \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/custom_routing_white_iran b/V2rayNG/app/src/main/assets/custom_routing_white_iran new file mode 100644 index 00000000..97ff1227 --- /dev/null +++ b/V2rayNG/app/src/main/assets/custom_routing_white_iran @@ -0,0 +1,37 @@ +[ + { + "remarks": "Block udp443", + "outboundTag": "block", + "port": "443", + "network": "udp" + }, + { + "remarks": "Direct LAN IP", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "remarks": "Direct LAN domains", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "remarks": "Bypass Iran domains", + "outboundTag": "direct", + "domain": [ + "domain:ir", + "geosite:category-ir" + ] + }, + { + "remarks": "Bypass Iran IP", + "outboundTag": "direct", + "ip": [ + "geoip:ir" + ] + } +] diff --git a/V2rayNG/app/src/main/assets/open_source_licenses.html b/V2rayNG/app/src/main/assets/open_source_licenses.html new file mode 100644 index 00000000..0cfd1daa --- /dev/null +++ b/V2rayNG/app/src/main/assets/open_source_licenses.html @@ -0,0 +1,1285 @@ + + + + + Open source licenses + + +

Notice for packages:

+
    +
  • Camera Core +
    +
    Copyright © 2019 The Android Open Source Project
    +
    +
    +
  • +
+ +
                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+
+
BSD License
+https://chromium.googlesource.com/libyuv/libyuv/+/refs/heads/main/README.chromium
+
+
+ + +
                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+
+
+
    +
  • MMKV +
    +
    Copyright © 20xx Tencent Wechat, Inc.
    +
    +
    +
  • +
+ +
BSD 3-Clause License
+
+Copyright (c) [year], [fullname]
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+ + +
Android Software Development Kit License
+https://developer.android.com/studio/terms.html
+
+
+ + +
ML Kit Terms of Service
+https://developers.google.com/ml-kit/terms
+
+
+
    +
  • editorkit +
    +
    Copyright © 20xx Dmitrii Rubtsov
    +
    +
    +
  • +
  • language-base +
    +
    Copyright © 20xx Dmitrii Rubtsov
    +
    +
    +
  • +
  • language-json +
    +
    Copyright © 20xx Dmitrii Rubtsov
    +
    +
    +
  • +
+ +
Apache 2.0 License
+https://github.com/massivemadness/EditorKit/blob/master/LICENSE
+
+
+ + +
MIT-0
+https://spdx.org/licenses/MIT-0.html
+
+
+ + +
MIT License
+
+Copyright (c) [year] [fullname]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
+ + diff --git a/V2rayNG/app/src/main/assets/proxy_packagename.txt b/V2rayNG/app/src/main/assets/proxy_packagename.txt index ecac8251..12abf7db 100644 --- a/V2rayNG/app/src/main/assets/proxy_packagename.txt +++ b/V2rayNG/app/src/main/assets/proxy_packagename.txt @@ -4,6 +4,7 @@ au.com.shiftyjelly.pocketcasts bbc.mobile.news.ww be.mygod.vpnhotspot ch.protonmail.android +cm.aptoide.pt co.wanqu.android com.alphainventor.filemanager com.amazon.kindle @@ -34,7 +35,9 @@ com.chrome.canary com.chrome.dev com.cl.newt66y com.cradle.iitc_mobile +org.exarhteam.iitc_mobile com.cygames.shadowverse +com.dcard.freedom com.devhd.feedly com.devolver.reigns2 com.discord @@ -108,6 +111,7 @@ com.ifttt.ifttt com.imgur.mobile com.innologica.inoreader com.instagram.android +com.instagram.lite com.instapaper.android com.jarvanh.vpntether com.kapp.youtube.final @@ -115,6 +119,7 @@ com.klinker.android.twitter_l com.lastpass.lpandroid com.linecorp.linelite com.lingodeer +com.ltnnews.news com.mediapods.tumbpods com.mgoogle.android.gms com.microsoft.emmx @@ -159,6 +164,7 @@ com.slack com.snaptube.premium com.sololearn com.sonelli.juicessh +com.sparkslab.dcardreader com.spotify.music com.tencent.huatuo com.termux @@ -173,10 +179,13 @@ com.twitter.android com.u91porn com.u9porn com.ubisoft.dance.justdance2015companion +com.udn.news com.utopia.pxview -com.valvesoftware.android.steam.communimunity com.valvesoftware.android.steam.community +com.vanced.manager com.vanced.android.youtube +com.vanced.android.apps.youtube.music +com.mgoogle.android.gms com.vimeo.android.videoapp com.vivaldi.browser com.vivaldi.browser.snapshot @@ -186,10 +195,12 @@ com.wire com.wuxiangai.refactor com.xda.labs com.xvideos.app +com.yahoo.mobile.client.android.superapp com.yandex.browser com.yandex.browser.beta com.yandex.browser.alpha com.z28j.feel +com.zhiliaoapp.musically con.medium.reader de.apkgrabber de.robv.android.xposed.installer @@ -210,6 +221,7 @@ jp.bokete.app.android jp.naver.line.android jp.pxv.android luo.speedometergpspro +m.cna.com.tw.App mark.via.gp me.tshine.easymark net.teeha.android.url_shortener @@ -226,6 +238,7 @@ org.mozilla.firefox_beta org.mozilla.focus org.schabi.newpipe org.telegram.messenger +org.telegram.messenger.web org.telegram.multi org.telegram.plus org.thunderdog.challegram @@ -239,3 +252,162 @@ tw.com.gamer.android.activecenter videodownloader.downloadvideo.downloader uk.co.bbc.learningenglish com.ted.android +de.danoeh.antennapod +com.kiwibrowser.browser +nekox.messenger +com.nextcloud.client +com.aurora.store +com.aurora.adroid +chat.simplex.app +im.vector.app +network.loki.messenger +eu.siacs.conversations +xyz.nextalone.nagram +net.programmierecke.radiodroid2 +im.fdx.v2ex +ml.docilealligator.infinityforreddit +com.bytemyth.ama +app.vanadium.browser +com.cakewallet.cake_wallet +org.purplei2p.i2pd +dk.tacit.android.foldersync.lite +com.nononsenseapps.feeder +com.m2049r.xmrwallet +com.paypal.android.p2pmobile +com.google.android.apps.googlevoice +com.readdle.spark +org.torproject.torbrowser +com.deepl.mobiletranslator +com.microsoft.bing +com.keylesspalace.tusky +com.ottplay.ottplay +ru.iptvremote.android.iptv.pro +jp.naver.line.android +com.xmflsct.app.tooot +com.forem.android +app.revanced.android.youtube +com.mgoogle.android.gms +com.pionex.client +vip.mytokenpocket +im.token.app +com.linekong.mars24 +com.feixiaohao +com.aicoin.appandroid +com.binance.dev +com.kraken.trade +com.okinc.okex.gp +com.authy.authy +air.com.rosettastone.mobile.CoursePlayer +com.blizzard.bma +com.amazon.kindle +com.google.android.apps.fitness +net.tsapps.appsales +com.wemesh.android +com.google.android.apps.googleassistant +allen.town.focus.reader +me.hyliu.fluent_reader_lite +com.aljazeera.mobile +com.ft.news +de.marmaro.krt.ffupdater +myradio.radio.fmradio.liveradio.radiostation +com.google.earth +eu.kanade.tachiyomi.j2k +com.audials +com.microsoft.skydrive +com.mb.android.tg +com.melodis.midomiMusicIdentifier.freemium +com.foxnews.android +ch.threema.app +com.briarproject.briar.android +foundation.e.apps +com.valvesoftware.android.steam.friendsui +com.imback.yeetalk +so.onekey.app.wallet +com.xc3fff0e.xmanager +meditofoundation.medito +com.picol.client +com.streetwriters.notesnook +shanghai.panewsApp.com +org.coursera.android +com.positron_it.zlib +com.blizzard.messenger +com.javdb.javrocket +com.picacomic.fregata +com.fxl.chacha +me.proton.android.drive +com.lastpass.lpandroid +com.tradingview.tradingviewapp +com.deviantart.android.damobile +com.fusionmedia.investing +com.ewa.ewaapp +com.duolingo +com.hellotalk +io.github.huskydg.magisk +com.jsy.xpgbox +com.hostloc.app.hostloc +com.dena.pokota +com.vitorpamplona.amethyst +com.zhiliaoapp.musically +us.spotco.fennec_dos +com.fongmi.android.tv +com.pocketprep.android.itcybersecurity +com.cloudtv +com.glassdoor.app +com.indeed.android.jobsearch +com.linkedin.android +com.github.tvbox.osc.bh +com.example.douban +com.sipnetic.app +com.microsoft.rdc.androidx +org.zwanoo.android.speedtest +com.sonelli.juicessh +com.scmp.newspulse +org.lsposed.manager +mnn.Android +com.thomsonretuers.reuters +com.guardian +com.ttxapps.onesyncv2 +org.fcitx.fcitx5.android.updater +com.tailscale.ipn +tw.nekomimi.nekogram +com.nexon.kartdrift +io.syncapps.lemmy_sync +com.seazon.feedme +com.readwise +de.spiritcroc.riotx +com.openai.chatgpt +io.changenow.changenow +com.poe.android +com.twingate +com.blinkslabs.blinkist.android +com.ichi2.anki +md.obsidian +com.musixmatch.android.lyrify +com.cyber.turbo +com.offsec.nethunter +me.ghui.v2er +com.samruston.twitter +org.adaway +org.swiftapps.swiftbackup +com.zerotier.one +com.quietmobile +com.instagram.barcelona +im.molly.app +com.rvx.android.youtube +com.deepl.mobiletranslator +com.qingsong.yingmi +com.lemurbrowser.exts +com.silverdev.dnartdroid +me.ash.reader +de.tutao.tutanota +dev.imranr.obtainium +com.getsomeheadspace.android +org.cromite.cromite +com.nutomic.syncthingandroid +com.bumble.app +com.cnn.mobile.android.phone +com.google.android.apps.authenticator2 +com.microsoft.copilot +com.netflix.NGP.Storyteller +com.Slack +com.server.auditor.ssh.client \ No newline at end of file diff --git a/V2rayNG/app/src/main/assets/v2ray_config.json b/V2rayNG/app/src/main/assets/v2ray_config.json index 5e009dc8..4f8c3d7e 100644 --- a/V2rayNG/app/src/main/assets/v2ray_config.json +++ b/V2rayNG/app/src/main/assets/v2ray_config.json @@ -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" }, { @@ -94,7 +97,7 @@ } ], "routing": { - "domainStrategy": "IPIfNonMatch", + "domainStrategy": "AsIs", "rules": [] }, "dns": { diff --git a/V2rayNG/app/src/main/ic_launcher-web.png b/V2rayNG/app/src/main/ic_launcher-web.png index f6fc3b8c..03a4ce8a 100644 Binary files a/V2rayNG/app/src/main/ic_launcher-web.png and b/V2rayNG/app/src/main/ic_launcher-web.png differ diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt new file mode 100644 index 00000000..44f680b3 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt @@ -0,0 +1,47 @@ +package com.v2ray.ang + +import android.content.Context +import androidx.multidex.MultiDexApplication +import androidx.work.Configuration +import androidx.work.WorkManager +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.handler.SettingsManager + +class AngApplication : MultiDexApplication() { + companion object { + lateinit var application: AngApplication + } + + /** + * Attaches the base context to the application. + * @param base The base context. + */ + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + application = this + } + + private val workManagerConfiguration: Configuration = Configuration.Builder() + .setDefaultProcessName("${ANG_PACKAGE}:bg") + .build() + + /** + * Initializes the application. + */ + override fun onCreate() { + super.onCreate() + + MMKV.initialize(this) + + SettingsManager.setNightMode() + // Initialize WorkManager with the custom configuration + WorkManager.initialize(this, workManagerConfiguration) + + SettingsManager.initRoutingRulesets(this) + + es.dmoral.toasty.Toasty.Config.getInstance() + .setGravity(android.view.Gravity.BOTTOM, 0, 200) + .apply() + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt new file mode 100644 index 00000000..09e3a9d5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt @@ -0,0 +1,248 @@ +package com.v2ray.ang + + +object AppConfig { + + /** The application's package name. */ + const val ANG_PACKAGE = BuildConfig.APPLICATION_ID + const val TAG = 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" + + /** Preferences mapped to MMKV storage. */ + const val PREF_SNIFFING_ENABLED = "pref_sniffing_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_APPEND_HTTP_PROXY = "pref_append_http_proxy" + const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port" + const val PREF_VPN_DNS = "pref_vpn_dns" + const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan" + const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index" + const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" + const val PREF_ROUTING_RULESET = "pref_routing_ruleset" + 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_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display" + 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_REMOTE_DNS = "pref_remote_dns" + const val PREF_DOMESTIC_DNS = "pref_domestic_dns" + const val PREF_DNS_HOSTS = "pref_dns_hosts" + const val PREF_DELAY_TEST_URL = "pref_delay_test_url" + const val PREF_LOGLEVEL = "pref_core_loglevel" + const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method" + const val PREF_MODE = "pref_mode" + const val PREF_IS_BOOTED = "pref_is_booted" + const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release" + const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources" + + /** Cache keys. */ + const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" + const val CACHE_KEYWORD_FILTER = "cache_keyword_filter" + + /** Protocol identifiers. */ + const val PROTOCOL_FREEDOM = "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" + + /** 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" + + /** Network-related constants. */ + const val UPLINK = "uplink" + const val DOWNLINK = "downlink" + + /** URLs for various resources. */ + const val GITHUB_URL = "https://github.com" + const val GITHUB_RAW_URL = "https://raw.githubusercontent.com" + const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download" + const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt" + const val APP_URL = "$GITHUB_URL/2dust/v2rayNG" + const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases" + const val APP_ISSUES_URL = "$APP_URL/issues" + const val APP_WIKI_MODE = "$APP_URL/wiki/Mode" + const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md" + const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" + const val TG_CHANNEL_URL = "https://t.me/github_2dust" + const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204" + const val DELAY_TEST_URL2 = "https://www.google.com/generate_204" + const val IP_API_URL = "https://speed.cloudflare.com/meta" + + /** 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" + const val GEOSITE_PRIVATE = "geosite:private" + const val GEOSITE_CN = "geosite:cn" + const val GEOIP_PRIVATE = "geoip:private" + const val GEOIP_CN = "geoip:cn" + + /** Ports and addresses for various services. */ + const val PORT_LOCAL_DNS = "10853" + const val PORT_SOCKS = "10808" + const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32" + const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128" + const val WIREGUARD_LOCAL_MTU = "1420" + const val LOOPBACK = "127.0.0.1" + + /** Message constants for communication. */ + const val MSG_REGISTER_CLIENT = 1 + const val MSG_STATE_RUNNING = 11 + const val MSG_STATE_NOT_RUNNING = 12 + const val MSG_UNREGISTER_CLIENT = 2 + const val MSG_STATE_START = 3 + const val MSG_STATE_START_SUCCESS = 31 + const val MSG_STATE_START_FAILURE = 32 + 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" + + /** Protocols Scheme **/ + const val VMESS = "vmess://" + const val CUSTOM = "" + const val SHADOWSOCKS = "ss://" + const val SOCKS = "socks://" + const val HTTP = "http://" + const val VLESS = "vless://" + const val TROJAN = "trojan://" + const val WIREGUARD = "wireguard://" + const val TUIC = "tuic://" + const val HYSTERIA2 = "hysteria2://" + const val HY2 = "hy2://" + + /** Give a good name to this, IDK*/ + const val VPN = "VPN" + + // Google API rule constants + const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn" + const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com" + + // Android Private DNS constants + const val DNS_DNSPOD_DOMAIN = "dot.pub" + const val DNS_ALIDNS_DOMAIN = "dns.alidns.com" + const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one" + const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com" + const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com" + const val DNS_GOOGLE_DOMAIN = "dns.google" + const val DNS_QUAD9_DOMAIN = "dns.quad9.net" + const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net" + + const val DEFAULT_PORT = 443 + const val DEFAULT_SECURITY = "auto" + const val DEFAULT_LEVEL = 8 + const val DEFAULT_NETWORK = "tcp" + const val TLS = "tls" + const val REALITY = "reality" + const val HEADER_TYPE_HTTP = "http" + + val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") + val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001") + val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5") + val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9") + val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53") + val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844") + val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9") + val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff") + + //minimum list https://serverfault.com/a/304791 + val ROUTED_IP_LIST = arrayListOf( + "0.0.0.0/5", + "8.0.0.0/7", + "11.0.0.0/8", + "12.0.0.0/6", + "16.0.0.0/4", + "32.0.0.0/3", + "64.0.0.0/2", + "128.0.0.0/3", + "160.0.0.0/5", + "168.0.0.0/6", + "172.0.0.0/12", + "172.32.0.0/11", + "172.64.0.0/10", + "172.128.0.0/9", + "173.0.0.0/8", + "174.0.0.0/7", + "176.0.0.0/4", + "192.0.0.0/9", + "192.128.0.0/11", + "192.160.0.0/13", + "192.169.0.0/16", + "192.170.0.0/15", + "192.172.0.0/14", + "192.176.0.0/12", + "192.192.0.0/10", + "193.0.0.0/8", + "194.0.0.0/7", + "196.0.0.0/6", + "200.0.0.0/5", + "208.0.0.0/4", + "240.0.0.0/4" + ) + + val PRIVATE_IP_LIST = arrayListOf( + "0.0.0.0/8", + "10.0.0.0/8", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "169.254.0.0/16", + "224.0.0.0/4" + ) + + val GEO_FILES_SOURCES = arrayListOf( + "Loyalsoldier/v2ray-rules-dat", + "runetfreedom/russia-v2ray-rules-dat", + "Chocolate4U/Iran-v2ray-rules" + ) + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt new file mode 100644 index 00000000..219e35e9 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt @@ -0,0 +1,11 @@ +package com.v2ray.ang.dto + +import android.graphics.drawable.Drawable + +data class AppInfo( + val appName: String, + val packageName: String, + val appIcon: Drawable, + val isSystemApp: Boolean, + var isSelected: Int +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt new file mode 100644 index 00000000..5a8d1e60 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt @@ -0,0 +1,9 @@ +package com.v2ray.ang.dto + +data class AssetUrlItem( + var remarks: String = "", + var url: String = "", + val addedTime: Long = System.currentTimeMillis(), + var lastUpdated: Long = -1, + var locked: Boolean? = false, +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt new file mode 100644 index 00000000..be4f62e5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt @@ -0,0 +1,10 @@ +package com.v2ray.ang.dto + +data class CheckUpdateResult( + val hasUpdate: Boolean, + val latestVersion: String? = null, + val releaseNotes: String? = null, + val downloadUrl: String? = null, + val error: String? = null, + val isPreRelease: Boolean = false +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt new file mode 100644 index 00000000..c8870248 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt @@ -0,0 +1,9 @@ +package com.v2ray.ang.dto + +data class ConfigResult( + var status: Boolean, + var guid: String? = null, + var content: String = "", + var socksPort: Int? = null, +) + diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt new file mode 100644 index 00000000..ed7cad3b --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt @@ -0,0 +1,22 @@ +package com.v2ray.ang.dto + +import com.v2ray.ang.AppConfig + + +enum class EConfigType(val value: Int, val protocolScheme: String) { + VMESS(1, AppConfig.VMESS), + CUSTOM(2, AppConfig.CUSTOM), + SHADOWSOCKS(3, AppConfig.SHADOWSOCKS), + SOCKS(4, AppConfig.SOCKS), + VLESS(5, AppConfig.VLESS), + TROJAN(6, AppConfig.TROJAN), + WIREGUARD(7, AppConfig.WIREGUARD), + + // TUIC(8, AppConfig.TUIC), + HYSTERIA2(9, AppConfig.HYSTERIA2), + HTTP(10, AppConfig.HTTP); + + companion object { + fun fromInt(value: Int) = entries.firstOrNull { it.value == value } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt new file mode 100644 index 00000000..0a7dce56 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt @@ -0,0 +1,23 @@ +package com.v2ray.ang.dto + +import com.google.gson.annotations.SerializedName + +data class GitHubRelease( + @SerializedName("tag_name") + val tagName: String, + @SerializedName("body") + val body: String, + @SerializedName("assets") + val assets: List, + @SerializedName("prerelease") + val prerelease: Boolean = false, + @SerializedName("published_at") + val publishedAt: String = "" +) { + data class Asset( + @SerializedName("name") + val name: String, + @SerializedName("browser_download_url") + val browserDownloadUrl: String + ) +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt new file mode 100644 index 00000000..c4fc6582 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt @@ -0,0 +1,46 @@ +package com.v2ray.ang.dto + +data class Hysteria2Bean( + val server: String?, + val auth: String?, + val lazy: Boolean? = true, + val obfs: ObfsBean? = null, + val socks5: Socks5Bean? = null, + val http: Socks5Bean? = null, + val tls: TlsBean? = null, + val transport: TransportBean? = null, + val bandwidth: BandwidthBean? = null, +) { + data class ObfsBean( + val type: String?, + val salamander: SalamanderBean? + ) { + data class SalamanderBean( + val password: String?, + ) + } + + data class Socks5Bean( + val listen: String?, + ) + + data class TlsBean( + val sni: String?, + val insecure: Boolean?, + val pinSHA256: String?, + ) + + data class TransportBean( + val type: String?, + val udp: TransportUdpBean? + ) { + data class TransportUdpBean( + val hopInterval: String?, + ) + } + + data class BandwidthBean( + val down: String?, + val up: String?, + ) +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt new file mode 100644 index 00000000..97814fbb --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt @@ -0,0 +1,12 @@ +package com.v2ray.ang.dto + +data class IPAPIInfo( + var ip: String? = null, + var clientIp: String? = null, + var ip_addr: String? = null, + var query: String? = null, + var country: String? = null, + var country_name: String? = null, + var country_code: String? = null, + var countryCode: String? = null +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt new file mode 100644 index 00000000..46333d06 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt @@ -0,0 +1,20 @@ +package com.v2ray.ang.dto + +enum class Language(val code: String) { + AUTO("auto"), + ENGLISH("en"), + CHINA("zh-rCN"), + TRADITIONAL_CHINESE("zh-rTW"), + VIETNAMESE("vi"), + RUSSIAN("ru"), + PERSIAN("fa"), + ARABIC("ar"), + BANGLA("bn"), + BAKHTIARI("bqi-rIR"); + + companion object { + fun fromCode(code: String): Language { + return entries.find { it.code == code } ?: AUTO + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt new file mode 100644 index 00000000..0e8091b9 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt @@ -0,0 +1,18 @@ +package com.v2ray.ang.dto + +enum class NetworkType(val type: String) { + TCP("tcp"), + KCP("kcp"), + WS("ws"), + HTTP_UPGRADE("httpupgrade"), + XHTTP("xhttp"), + HTTP("http"), + H2("h2"), + + //QUIC("quic"), + GRPC("grpc"); + + companion object { + fun fromString(type: String?) = entries.find { it.type == type } ?: TCP + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt new file mode 100644 index 00000000..7a1f7346 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt @@ -0,0 +1,120 @@ +package com.v2ray.ang.dto + +import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.AppConfig.PORT_SOCKS +import com.v2ray.ang.AppConfig.TAG_BLOCKED +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.AppConfig.TAG_PROXY +import com.v2ray.ang.util.Utils + +data class ProfileItem( + val configVersion: Int = 4, + val configType: EConfigType, + var subscriptionId: String = "", + var addedTime: Long = System.currentTimeMillis(), + + var remarks: String = "", + var server: String? = null, + var serverPort: String? = null, + + var password: String? = null, + var method: String? = null, + var flow: String? = null, + var username: String? = null, + + var network: String? = null, + var headerType: String? = null, + var host: String? = null, + var path: String? = null, + var seed: String? = null, + var quicSecurity: String? = null, + var quicKey: String? = null, + var mode: String? = null, + var serviceName: String? = null, + var authority: String? = null, + var xhttpMode: String? = null, + var xhttpExtra: String? = null, + + var security: String? = null, + var sni: String? = null, + var alpn: String? = null, + var fingerPrint: String? = null, + var insecure: Boolean? = null, + + var publicKey: String? = null, + var shortId: String? = null, + var spiderX: String? = null, + + var secretKey: String? = null, + var preSharedKey: String? = null, + var localAddress: String? = null, + var reserved: String? = null, + var mtu: Int? = null, + + var obfsPassword: String? = null, + var portHopping: String? = null, + var portHoppingInterval: String? = null, + var pinSHA256: String? = null, + var bandwidthDown: String? = null, + var bandwidthUp: String? = null, + + ) { + companion object { + fun create(configType: EConfigType): ProfileItem { + return ProfileItem(configType = configType) + } + } + + fun getAllOutboundTags(): MutableList { + return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED) + } + + fun getServerAddressAndPort(): String { + if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) { + return "$LOOPBACK:$PORT_SOCKS" + } + return Utils.getIpv6Address(server) + ":" + serverPort + } + + override fun equals(other: Any?): Boolean { + if (other == null) return false + val obj = other as ProfileItem + + return (this.server == obj.server + && this.serverPort == obj.serverPort + && this.password == obj.password + && this.method == obj.method + && this.flow == obj.flow + && this.username == obj.username + + && this.network == obj.network + && this.headerType == obj.headerType + && this.host == obj.host + && this.path == obj.path + && this.seed == obj.seed + && this.quicSecurity == obj.quicSecurity + && this.quicKey == obj.quicKey + && this.mode == obj.mode + && this.serviceName == obj.serviceName + && this.authority == obj.authority + && this.xhttpMode == obj.xhttpMode + + && this.security == obj.security + && this.sni == obj.sni + && this.alpn == obj.alpn + && this.fingerPrint == obj.fingerPrint + && this.publicKey == obj.publicKey + && this.shortId == obj.shortId + + && this.secretKey == obj.secretKey + && this.localAddress == obj.localAddress + && this.reserved == obj.reserved + && this.mtu == obj.mtu + + && this.obfsPassword == obj.obfsPassword + && this.portHopping == obj.portHopping + && this.portHoppingInterval == obj.portHoppingInterval + && this.pinSHA256 == obj.pinSHA256 + ) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt new file mode 100644 index 00000000..a98ac167 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt @@ -0,0 +1,20 @@ +package com.v2ray.ang.dto + +enum class RoutingType(val fileName: String) { + WHITE("custom_routing_white"), + BLACK("custom_routing_black"), + GLOBAL("custom_routing_global"), + WHITE_IRAN("custom_routing_white_iran"); + + companion object { + fun fromIndex(index: Int): RoutingType { + return when (index) { + 0 -> WHITE + 1 -> BLACK + 2 -> GLOBAL + 3 -> WHITE_IRAN + else -> WHITE + } + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt new file mode 100644 index 00000000..9005f559 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt @@ -0,0 +1,13 @@ +package com.v2ray.ang.dto + +data class RulesetItem( + var remarks: String? = "", + var ip: List? = null, + var domain: List? = null, + var outboundTag: String = "", + var port: String? = null, + var network: String? = null, + var protocol: List? = null, + var enabled: Boolean = true, + var locked: Boolean? = false, +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt similarity index 100% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerAffiliationInfo.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt new file mode 100644 index 00000000..baa1fe48 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt @@ -0,0 +1,86 @@ +package com.v2ray.ang.dto + +import com.v2ray.ang.AppConfig.TAG_BLOCKED +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.AppConfig.TAG_PROXY + +data class ServerConfig( + val configVersion: Int = 3, + val configType: EConfigType, + var subscriptionId: String = "", + val addedTime: Long = System.currentTimeMillis(), + var remarks: String = "", + val outboundBean: V2rayConfig.OutboundBean? = null, + var fullConfig: V2rayConfig? = null +) { + companion object { + fun create(configType: EConfigType): 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() + ) + ) + + EConfigType.CUSTOM -> + return ServerConfig(configType = configType) + + EConfigType.SHADOWSOCKS, + EConfigType.SOCKS, + EConfigType.HTTP, + EConfigType.TROJAN, + EConfigType.HYSTERIA2 -> + 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() + ) + ) + + EConfigType.WIREGUARD -> + return ServerConfig( + configType = configType, + outboundBean = V2rayConfig.OutboundBean( + protocol = configType.name.lowercase(), + settings = V2rayConfig.OutboundBean.OutSettingsBean( + secretKey = "", + peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean()) + ) + ) + ) + } + } + } + + fun getProxyOutbound(): V2rayConfig.OutboundBean? { + if (configType != EConfigType.CUSTOM) { + return outboundBean + } + return fullConfig?.getProxyOutbound() + } + + fun getAllOutboundTags(): MutableList { + if (configType != EConfigType.CUSTOM) { + return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED) + } + fullConfig?.let { config -> + return config.outbounds.map { it.tag }.toMutableList() + } + return mutableListOf() + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt new file mode 100644 index 00000000..7ea3e6a3 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServersCache.kt @@ -0,0 +1,6 @@ +package com.v2ray.ang.dto + +data class ServersCache( + val guid: String, + val profile: ProfileItem +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt new file mode 100644 index 00000000..8957df78 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt @@ -0,0 +1,16 @@ +package com.v2ray.ang.dto + +data class SubscriptionItem( + 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, + var prevProfile: String? = null, + var nextProfile: String? = null, + var filter: String? = null, + var allowInsecureUrl: Boolean = false, +) + diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt new file mode 100644 index 00000000..155be104 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt @@ -0,0 +1,556 @@ +package com.v2ray.ang.dto + +import com.google.gson.annotations.SerializedName +import com.v2ray.ang.AppConfig +import com.v2ray.ang.util.Utils + +data class V2rayConfig( + var remarks: String? = null, + var stats: Any? = null, + val log: LogBean, + var policy: PolicyBean? = null, + val inbounds: ArrayList, + var outbounds: ArrayList, + var dns: DnsBean? = null, + val routing: RoutingBean, + val api: Any? = null, + val transport: Any? = null, + val reverse: Any? = null, + var fakedns: Any? = null, + val browserForwarder: Any? = null, + var observatory: Any? = null, + var burstObservatory: Any? = null +) { + + data class LogBean( + val access: String? = null, + val error: String? = null, + var loglevel: String? = null, + val dnsLog: Boolean? = null + ) + + data class InboundBean( + var tag: String, + var port: Int, + var protocol: String, + var listen: String? = null, + val settings: Any? = null, + val sniffing: SniffingBean? = null, + val streamSettings: Any? = null, + val allocate: Any? = null + ) { + + data class InSettingsBean( + val auth: String? = null, + val udp: Boolean? = null, + val userLevel: Int? = null, + val address: String? = null, + val port: Int? = null, + val network: String? = null + ) + + data class SniffingBean( + var enabled: Boolean, + val destOverride: ArrayList, + val metadataOnly: Boolean? = null, + var routeOnly: Boolean? = null + ) + } + + 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, + var mux: MuxBean? = MuxBean(false) + ) { + data class OutSettingsBean( + var vnext: List? = null, + var fragment: FragmentBean? = null, + var noises: List? = null, + var servers: List? = null, + /*Blackhole*/ + var response: Response? = null, + /*DNS*/ + val network: 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, + /*Wireguard*/ + var secretKey: String? = null, + val peers: List? = null, + var reserved: List? = null, + var mtu: Int? = null, + var obfsPassword: String? = null, + ) { + + data class VnextBean( + var address: String = "", + var port: Int = AppConfig.DEFAULT_PORT, + var users: List + ) { + + data class UsersBean( + var id: String = "", + var alterId: Int? = null, + var security: String? = null, + var level: Int = AppConfig.DEFAULT_LEVEL, + var encryption: String? = null, + var flow: String? = null + ) + } + + data class FragmentBean( + var packets: String? = null, + var length: String? = null, + var interval: String? = null + ) + + data class NoiseBean( + var type: String? = null, + var packet: String? = null, + var delay: String? = null + ) + + data class ServersBean( + var address: String = "", + var method: String? = null, + var ota: Boolean = false, + var password: String? = null, + var port: Int = AppConfig.DEFAULT_PORT, + var level: Int = AppConfig.DEFAULT_LEVEL, + val email: String? = null, + var flow: String? = null, + val ivCheck: Boolean? = null, + var users: List? = null + ) { + data class SocksUsersBean( + var user: String = "", + var pass: String = "", + var level: Int = AppConfig.DEFAULT_LEVEL + ) + } + + data class Response(var type: String) + + data class WireGuardBean( + var publicKey: String = "", + var preSharedKey: String? = null, + var endpoint: String = "" + ) + } + + data class StreamSettingsBean( + var network: String = AppConfig.DEFAULT_NETWORK, + var security: String? = null, + var tcpSettings: TcpSettingsBean? = null, + var kcpSettings: KcpSettingsBean? = null, + var wsSettings: WsSettingsBean? = null, + var httpupgradeSettings: HttpupgradeSettingsBean? = null, + var xhttpSettings: XhttpSettingsBean? = null, + var httpSettings: HttpSettingsBean? = null, + var tlsSettings: TlsSettingsBean? = null, + var quicSettings: QuicSettingBean? = null, + var realitySettings: TlsSettingsBean? = null, + var grpcSettings: GrpcSettingsBean? = null, + var hy2steriaSettings: Hy2steriaSettingsBean? = null, + val dsSettings: Any? = null, + var sockopt: SockoptBean? = null + ) { + + data class TcpSettingsBean( + var header: HeaderBean = HeaderBean(), + val acceptProxyProtocol: Boolean? = null + ) { + data class HeaderBean( + var type: String = "none", + var request: RequestBean? = null, + var response: Any? = null + ) { + data class RequestBean( + var path: List = ArrayList(), + var headers: HeadersBean = HeadersBean(), + val version: String? = null, + val method: String? = null + ) { + data class HeadersBean( + var Host: List? = ArrayList(), + @SerializedName("User-Agent") + val userAgent: List? = null, + @SerializedName("Accept-Encoding") + val acceptEncoding: List? = null, + val Connection: List? = null, + val Pragma: String? = null + ) + } + } + } + + data class KcpSettingsBean( + var mtu: Int = 1350, + var tti: Int = 50, + var uplinkCapacity: Int = 12, + var downlinkCapacity: Int = 100, + var congestion: Boolean = false, + var readBufferSize: Int = 1, + var writeBufferSize: Int = 1, + var header: HeaderBean = HeaderBean(), + var seed: String? = null + ) { + data class HeaderBean( + var type: String = "none", + var domain: String? = null + ) + } + + data class WsSettingsBean( + var path: String? = null, + var headers: HeadersBean = HeadersBean(), + val maxEarlyData: Int? = null, + val useBrowserForwarding: Boolean? = null, + val acceptProxyProtocol: Boolean? = null + ) { + data class HeadersBean(var Host: String = "") + } + + data class HttpupgradeSettingsBean( + var path: String? = null, + var host: String? = null, + val acceptProxyProtocol: Boolean? = null + ) + + data class XhttpSettingsBean( + var path: String? = null, + var host: String? = null, + var mode: String? = null, + var extra: Any? = null, + ) + + data class HttpSettingsBean( + var host: List = ArrayList(), + var path: String? = null + ) + + 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, + var domainStrategy: String? = null + ) + + data class TlsSettingsBean( + var allowInsecure: Boolean = false, + var serverName: String? = null, + val alpn: List? = null, + val minVersion: String? = null, + val maxVersion: String? = null, + val preferServerCipherSuites: Boolean? = null, + val cipherSuites: String? = null, + val fingerprint: String? = null, + val certificates: List? = null, + val disableSystemRoot: 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 = "", + var header: HeaderBean = HeaderBean() + ) { + data class HeaderBean(var type: String = "none") + } + + data class GrpcSettingsBean( + var serviceName: String = "", + var authority: String? = null, + var multiMode: Boolean? = null, + var idle_timeout: Int? = null, + var health_check_timeout: Int? = null + ) + + data class Hy2steriaSettingsBean( + var password: String? = null, + var use_udp_extension: Boolean? = true, + var congestion: Hy2CongestionBean? = null + ) { + data class Hy2CongestionBean( + var type: String? = "bbr", + var up_mbps: Int? = null, + var down_mbps: Int? = null, + ) + } + + } + + data class MuxBean( + var enabled: Boolean, + var concurrency: Int? = null, + var xudpConcurrency: Int? = null, + var xudpProxyUDP443: String? = null, + ) + + fun getServerAddress(): String? { + if (protocol.equals(EConfigType.VMESS.name, true) + || protocol.equals(EConfigType.VLESS.name, true) + ) { + return settings?.vnext?.first()?.address + } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) + || protocol.equals(EConfigType.SOCKS.name, true) + || protocol.equals(EConfigType.HTTP.name, true) + || protocol.equals(EConfigType.TROJAN.name, true) + || protocol.equals(EConfigType.HYSTERIA2.name, true) + ) { + return settings?.servers?.first()?.address + } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { + return settings?.peers?.first()?.endpoint?.substringBeforeLast(":") + } + return null + } + + fun getServerPort(): Int? { + if (protocol.equals(EConfigType.VMESS.name, true) + || protocol.equals(EConfigType.VLESS.name, true) + ) { + return settings?.vnext?.first()?.port + } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) + || protocol.equals(EConfigType.SOCKS.name, true) + || protocol.equals(EConfigType.HTTP.name, true) + || protocol.equals(EConfigType.TROJAN.name, true) + || protocol.equals(EConfigType.HYSTERIA2.name, true) + ) { + return settings?.servers?.first()?.port + } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { + return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt() + } + return null + } + + fun getServerAddressAndPort(): String { + val address = getServerAddress().orEmpty() + val port = getServerPort() + return Utils.getIpv6Address(address) + ":" + port + } + + fun getPassword(): String? { + if (protocol.equals(EConfigType.VMESS.name, true) + || protocol.equals(EConfigType.VLESS.name, true) + ) { + return settings?.vnext?.first()?.users?.first()?.id + } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) + || protocol.equals(EConfigType.TROJAN.name, true) + || protocol.equals(EConfigType.HYSTERIA2.name, true) + ) { + return settings?.servers?.first()?.password + } else if (protocol.equals(EConfigType.SOCKS.name, true) + || protocol.equals(EConfigType.HTTP.name, true) + ) { + return settings?.servers?.first()?.users?.first()?.pass + } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { + return settings?.secretKey + } + return null + } + + fun getSecurityEncryption(): String? { + return when { + protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security + protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption + protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method + else -> null + } + } + + fun getTransportSettingDetails(): List? { + if (protocol.equals(EConfigType.VMESS.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) { + NetworkType.TCP.type -> { + val tcpSetting = streamSettings?.tcpSettings ?: return null + listOf( + tcpSetting.header.type, + tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(), + tcpSetting.header.request?.path?.joinToString(",").orEmpty() + ) + } + + NetworkType.KCP.type -> { + val kcpSetting = streamSettings?.kcpSettings ?: return null + listOf( + kcpSetting.header.type, + "", + kcpSetting.seed.orEmpty() + ) + } + + NetworkType.WS.type -> { + val wsSetting = streamSettings?.wsSettings ?: return null + listOf( + "", + wsSetting.headers.Host, + wsSetting.path + ) + } + + NetworkType.HTTP_UPGRADE.type -> { + val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null + listOf( + "", + httpupgradeSetting.host, + httpupgradeSetting.path + ) + } + + NetworkType.XHTTP.type -> { + val xhttpSettings = streamSettings?.xhttpSettings ?: return null + listOf( + "", + xhttpSettings.host, + xhttpSettings.path + ) + } + + NetworkType.H2.type -> { + val h2Setting = streamSettings?.httpSettings ?: return null + listOf( + "", + h2Setting.host.joinToString(","), + h2Setting.path + ) + } + +// "quic" -> { +// val quicSetting = streamSettings?.quicSettings ?: return null +// listOf( +// quicSetting.header.type, +// quicSetting.security, +// quicSetting.key +// ) +// } + + NetworkType.GRPC.type -> { + val grpcSetting = streamSettings?.grpcSettings ?: return null + listOf( + if (grpcSetting.multiMode == true) "multi" else "gun", + grpcSetting.authority.orEmpty(), + grpcSetting.serviceName + ) + } + + else -> null + } + } + return null + } + + fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean { + val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also { + streamSettings = it + } + + val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also { + stream.sockopt = it + } + + return sockopt + } + } + + data class DnsBean( + var servers: ArrayList? = null, + var hosts: Map? = null, + val clientIp: String? = null, + val disableCache: Boolean? = null, + val queryStrategy: String? = null, + val tag: String? = null + ) { + data class ServersBean( + var address: String = "", + var port: Int? = null, + var domains: List? = null, + var expectIPs: List? = null, + val clientIp: String? = null, + val skipFallback: Boolean? = null, + ) + } + + data class RoutingBean( + var domainStrategy: String, + var domainMatcher: String? = null, + var rules: ArrayList, + val balancers: List? = null + ) { + + data class RulesBean( + var type: String = "field", + var ip: ArrayList? = null, + var domain: ArrayList? = null, + var outboundTag: String = "", + var balancerTag: String? = null, + var port: String? = null, + val sourcePort: String? = null, + val network: String? = null, + val source: List? = null, + val user: List? = null, + var inboundTag: List? = null, + val protocol: List? = null, + val attrs: String? = null, + val domainMatcher: String? = null + ) + } + + data class PolicyBean( + var levels: Map, + var system: Any? = null + ) { + data class LevelBean( + var handshake: Int? = null, + var connIdle: Int? = null, + var uplinkOnly: Int? = null, + var downlinkOnly: Int? = null, + val statsUserUplink: Boolean? = null, + val statsUserDownlink: Boolean? = null, + var bufferSize: Int? = null + ) + } + + data class FakednsBean( + var ipPool: String = "198.18.0.0/15", + var poolSize: Int = 10000 + ) // roughly 10 times smaller than total ip pool + + fun getProxyOutbound(): OutboundBean? { + outbounds.forEach { outbound -> + EConfigType.entries.forEach { + if (outbound.protocol.equals(it.name, true)) { + return outbound + } + } + } + return null + } + + fun getAllProxyOutbound(): List { + return outbounds.filter { outbound -> + EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) } + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt new file mode 100644 index 00000000..779c7e3c --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt @@ -0,0 +1,19 @@ +package com.v2ray.ang.dto + +data class VmessQRCode( + var v: String = "", + var ps: String = "", + var add: 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 alpn: String = "", + var fp: String = "" +) \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt new file mode 100644 index 00000000..6b7bc379 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt @@ -0,0 +1,39 @@ +package com.v2ray.ang.dto + +/** + * VPN interface address configuration enum class + * Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration. + * Each option provides client and router addresses to establish point-to-point VPN tunnels. + */ +enum class VpnInterfaceAddressConfig( + val displayName: String, + val ipv4Client: String, + val ipv4Router: String, + val ipv6Client: String, + val ipv6Router: String +) { + OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"), + OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"), + OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"), + OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"), + OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"), + OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"), + OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2"); + + companion object { + /** + * Retrieves the VPN interface address configuration based on the specified index. + * + * @param index The configuration index (0-based) corresponding to user selection + * @return The VpnInterfaceAddressConfig instance at the specified index, + * or OPTION_1 (default) if the index is out of bounds + */ + fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig { + return if (index in values().indices) { + values()[index] + } else { + OPTION_1 // Default to the first configuration + } + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt new file mode 100644 index 00000000..6e5c6bb4 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt @@ -0,0 +1,212 @@ +package com.v2ray.ang.extension + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import com.v2ray.ang.AngApplication +import es.dmoral.toasty.Toasty +import org.json.JSONObject +import java.io.Serializable +import java.net.URI +import java.net.URLConnection + +val Context.v2RayApplication: AngApplication? + get() = applicationContext as? AngApplication + +/** + * Shows a toast message with the given resource ID. + * + * @param message The resource ID of the message to show. + */ +fun Context.toast(message: Int) { + Toasty.normal(this, message).show() +} + +/** + * Shows a toast message with the given text. + * + * @param message The text of the message to show. + */ +fun Context.toast(message: CharSequence) { + Toasty.normal(this, message).show() +} + +/** + * Shows a toast message with the given resource ID. + * + * @param message The resource ID of the message to show. + */ +fun Context.toastSuccess(message: Int) { + Toasty.success(this, message, Toast.LENGTH_SHORT, true).show() +} + +/** + * Shows a toast message with the given text. + * + * @param message The text of the message to show. + */ +fun Context.toastSuccess(message: CharSequence) { + Toasty.success(this, message, Toast.LENGTH_SHORT, true).show() +} + +/** + * Shows a toast message with the given resource ID. + * + * @param message The resource ID of the message to show. + */ +fun Context.toastError(message: Int) { + Toasty.error(this, message, Toast.LENGTH_SHORT, true).show() +} + +/** + * Shows a toast message with the given text. + * + * @param message The text of the message to show. + */ +fun Context.toastError(message: CharSequence) { + Toasty.error(this, message, Toast.LENGTH_SHORT, true).show() +} + + +/** + * Puts a key-value pair into the JSONObject. + * + * @param pair The key-value pair to put. + */ +fun JSONObject.putOpt(pair: Pair) { + put(pair.first, pair.second) +} + +/** + * Puts multiple key-value pairs into the JSONObject. + * + * @param pairs The map of key-value pairs to put. + */ +fun JSONObject.putOpt(pairs: Map) { + pairs.forEach { put(it.key, it.value) } +} + +const val THRESHOLD = 1000L +const val DIVISOR = 1024.0 + +/** + * Converts a Long value to a speed string. + * + * @return The speed string. + */ +fun Long.toSpeedString(): String = this.toTrafficString() + "/s" + +/** + * Converts a Long value to a traffic string. + * + * @return The traffic string. + */ +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 >= Build.VERSION_CODES.N) { + contentLengthLong + } else { + contentLength.toLong() + } + +val URI.idnHost: String + get() = host?.replace("[", "")?.replace("]", "").orEmpty() + +/** + * Removes all whitespace from the string. + * + * @return The string without whitespace. + */ +fun String?.removeWhiteSpace(): String? = this?.replace(" ", "") + +/** + * Converts the string to a Long value, or returns 0 if the conversion fails. + * + * @return The Long value. + */ +fun String.toLongEx(): Long = toLongOrNull() ?: 0 + +/** + * Listens for package changes and executes a callback when a change occurs. + * + * @param onetime Whether to unregister the receiver after the first callback. + * @param callback The callback to execute when a package change occurs. + * @return The BroadcastReceiver that was registered. + */ +fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + callback() + if (onetime) context.unregisterReceiver(this) + } + }.apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(this, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + }, Context.RECEIVER_EXPORTED) + } else { + registerReceiver(this, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + }) + } + } + +/** + * Retrieves a serializable object from the Bundle. + * + * @param key The key of the serializable object. + * @return The serializable object, or null if not found. + */ +inline fun Bundle.serializable(key: String): T? = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java) + else -> @Suppress("DEPRECATION") getSerializable(key) as? T +} + +/** + * Retrieves a serializable object from the Intent. + * + * @param key The key of the serializable object. + * @return The serializable object, or null if not found. + */ +inline fun Intent.serializable(key: String): T? = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java) + else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T +} + +/** + * Checks if the CharSequence is not null and not empty. + * + * @return True if the CharSequence is not null and not empty, false otherwise. + */ +fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty() + +fun String.concatUrl(vararg paths: String): String { + val builder = StringBuilder(this.trimEnd('/')) + + paths.forEach { path -> + val trimmedPath = path.trim('/') + if (trimmedPath.isNotEmpty()) { + builder.append('/').append(trimmedPath) + } + } + + return builder.toString() +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt new file mode 100644 index 00000000..3bc20927 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt @@ -0,0 +1,27 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.util.JsonUtil + +object CustomFmt : FmtBase() { + /** + * Parses a JSON string into a ProfileItem object. + * + * @param str the JSON string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.CUSTOM) + + val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java) + val outbound = fullConfig.getProxyOutbound() + + config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString() + config.server = outbound?.getServerAddress() + config.serverPort = outbound?.getServerPort().toString() + + return config + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt new file mode 100644 index 00000000..73cdf958 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt @@ -0,0 +1,170 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.NetworkType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.Utils +import java.net.URI + +open class FmtBase { + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @param userInfo the user information to include in the URI + * @param dicQuery the query parameters to include in the URI + * @return the converted URI string + */ + fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap?): String { + val query = if (dicQuery != null) + "?" + dicQuery.toList().joinToString( + separator = "&", + transform = { it.first + "=" + Utils.urlEncode(it.second) }) + else "" + + val url = String.format( + "%s@%s:%s", + Utils.urlEncode(userInfo ?: ""), + Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())), + config.serverPort + ) + + return "${url}${query}#${Utils.urlEncode(config.remarks)}" + } + + /** + * Extracts query parameters from a URI. + * + * @param uri the URI to extract query parameters from + * @return a map of query parameters + */ + fun getQueryParam(uri: URI): Map { + return uri.rawQuery.split("&") + .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } + } + + /** + * Populates a ProfileItem object with values from query parameters. + * + * @param config the ProfileItem object to populate + * @param queryParam the query parameters to use for populating the ProfileItem + * @param allowInsecure whether to allow insecure connections + */ + fun getItemFormQuery(config: ProfileItem, queryParam: Map, allowInsecure: Boolean) { + config.network = queryParam["type"] ?: NetworkType.TCP.type + config.headerType = queryParam["headerType"] + config.host = queryParam["host"] + config.path = queryParam["path"] + + config.seed = queryParam["seed"] + config.quicSecurity = queryParam["quicSecurity"] + config.quicKey = queryParam["key"] + config.mode = queryParam["mode"] + config.serviceName = queryParam["serviceName"] + config.authority = queryParam["authority"] + config.xhttpMode = queryParam["mode"] + config.xhttpExtra = queryParam["extra"] + + config.security = queryParam["security"] + if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) { + config.security = null + } + config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) { + allowInsecure + } else { + queryParam["allowInsecure"].orEmpty() == "1" + } + config.sni = queryParam["sni"] + config.fingerPrint = queryParam["fp"] + config.alpn = queryParam["alpn"] + config.publicKey = queryParam["pbk"] + config.shortId = queryParam["sid"] + config.spiderX = queryParam["spx"] + config.flow = queryParam["flow"] + } + + /** + * Creates a map of query parameters from a ProfileItem object. + * + * @param config the ProfileItem object to create query parameters from + * @return a map of query parameters + */ + fun getQueryDic(config: ProfileItem): HashMap { + val dicQuery = HashMap() + dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty() + config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() } + config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() } + config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() } + config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() } + config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() } + config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() } + config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() } + + val networkType = NetworkType.fromString(config.network) + dicQuery["type"] = networkType.type + + when (networkType) { + NetworkType.TCP -> { + dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty() + config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() } + } + + NetworkType.KCP -> { + dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty() + config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() } + } + + NetworkType.WS, NetworkType.HTTP_UPGRADE -> { + config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() } + config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() } + } + + NetworkType.XHTTP -> { + config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() } + config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() } + config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() } + config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() } + } + + NetworkType.HTTP, NetworkType.H2 -> { + dicQuery["type"] = "http" + config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() } + config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() } + } + +// NetworkType.QUIC -> { +// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty() +// config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() } +// config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() } +// } + + NetworkType.GRPC -> { + config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() } + config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() } + config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() } + } + } + + return dicQuery + } + + fun getServerAddress(profileItem: ProfileItem): String { + if (Utils.isPureIpAddress(profileItem.server.orEmpty())) { + return profileItem.server.orEmpty() + } + + val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty()) + if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") { + return domain + } + //Resolve and replace domain + val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) + if (resolvedIps.isNullOrEmpty()) { + return domain + } + return resolvedIps.first() + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt new file mode 100644 index 00000000..8c641f24 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt @@ -0,0 +1,32 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.V2rayConfigManager + +object HttpFmt : FmtBase() { + /** + * Converts a ProfileItem object to an OutboundBean object. + * + * @param profileItem the ProfileItem object to convert + * @return the converted OutboundBean object, or null if conversion fails + */ + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP) + + outboundBean?.settings?.servers?.first()?.let { server -> + server.address = getServerAddress(profileItem) + server.port = profileItem.serverPort.orEmpty().toInt() + if (profileItem.username.isNotNullEmpty()) { + val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() + socksUsersBean.user = profileItem.username.orEmpty() + socksUsersBean.pass = profileItem.password.orEmpty() + server.users = listOf(socksUsersBean) + } + } + + return outboundBean + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt new file mode 100644 index 00000000..3b3dc88c --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt @@ -0,0 +1,151 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.Hysteria2Bean +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object Hysteria2Fmt : FmtBase() { + /** + * Parses a Hysteria2 URI string into a ProfileItem object. + * + * @param str the Hysteria2 URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.HYSTERIA2) + + val uri = URI(Utils.fixIllegalUrl(str)) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } + config.server = uri.idnHost + config.serverPort = uri.port.toString() + config.password = uri.userInfo + config.security = AppConfig.TLS + + if (!uri.rawQuery.isNullOrEmpty()) { + val queryParam = getQueryParam(uri) + + config.security = queryParam["security"] ?: AppConfig.TLS + config.insecure = if (queryParam["insecure"].isNullOrEmpty()) { + allowInsecure + } else { + queryParam["insecure"].orEmpty() == "1" + } + config.sni = queryParam["sni"] + config.alpn = queryParam["alpn"] + + config.obfsPassword = queryParam["obfs-password"] + config.portHopping = queryParam["mport"] + config.pinSHA256 = queryParam["pinSHA256"] + + } + + return config + } + + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @return the converted URI string + */ + fun toUri(config: ProfileItem): String { + val dicQuery = HashMap() + + config.security.let { if (it != null) dicQuery["security"] = it } + config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() } + config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() } + config.insecure.let { dicQuery["insecure"] = if (it == true) "1" else "0" } + + if (config.obfsPassword.isNotNullEmpty()) { + dicQuery["obfs"] = "salamander" + dicQuery["obfs-password"] = config.obfsPassword.orEmpty() + } + if (config.portHopping.isNotNullEmpty()) { + dicQuery["mport"] = config.portHopping.orEmpty() + } + if (config.pinSHA256.isNotNullEmpty()) { + dicQuery["pinSHA256"] = config.pinSHA256.orEmpty() + } + + return toUri(config, config.password, dicQuery) + } + + /** + * Converts a ProfileItem object to a Hysteria2Bean object. + * + * @param config the ProfileItem object to convert + * @param socksPort the port number for the socks5 proxy + * @return the converted Hysteria2Bean object, or null if conversion fails + */ + fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? { + + val obfs = if (config.obfsPassword.isNullOrEmpty()) null else + Hysteria2Bean.ObfsBean( + type = "salamander", + salamander = Hysteria2Bean.ObfsBean.SalamanderBean( + password = config.obfsPassword + ) + ) + + val transport = if (config.portHopping.isNullOrEmpty()) null else + Hysteria2Bean.TransportBean( + type = "udp", + udp = Hysteria2Bean.TransportBean.TransportUdpBean( + hopInterval = (config.portHoppingInterval ?: "30") + "s" + ) + ) + + val bandwidth = if (config.bandwidthDown.isNullOrEmpty() || config.bandwidthUp.isNullOrEmpty()) null else + Hysteria2Bean.BandwidthBean( + down = config.bandwidthDown, + up = config.bandwidthUp, + ) + + val server = + if (config.portHopping.isNullOrEmpty()) + config.getServerAddressAndPort() + else + Utils.getIpv6Address(config.server) + ":" + config.portHopping + + val bean = Hysteria2Bean( + server = server, + auth = config.password, + obfs = obfs, + transport = transport, + bandwidth = bandwidth, + socks5 = Hysteria2Bean.Socks5Bean( + listen = "$LOOPBACK:${socksPort}", + ), + http = Hysteria2Bean.Socks5Bean( + listen = "$LOOPBACK:${socksPort}", + ), + tls = Hysteria2Bean.TlsBean( + sni = config.sni ?: config.server, + insecure = config.insecure, + pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256 + ) + ) + return bean + } + + /** + * Converts a ProfileItem object to an OutboundBean object. + * + * @param profileItem the ProfileItem object to convert + * @return the converted OutboundBean object, or null if conversion fails + */ + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2) + return outboundBean + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt new file mode 100644 index 00000000..87ba74f8 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt @@ -0,0 +1,154 @@ +package com.v2ray.ang.fmt + +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.NetworkType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object ShadowsocksFmt : FmtBase() { + /** + * Parses a Shadowsocks URI string into a ProfileItem object. + * + * @param str the Shadowsocks URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + return parseSip002(str) ?: parseLegacy(str) + } + + /** + * Parses a SIP002 Shadowsocks URI string into a ProfileItem object. + * + * @param str the SIP002 Shadowsocks URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parseSip002(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.SHADOWSOCKS) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.idnHost.isEmpty()) return null + if (uri.port <= 0) return null + if (uri.userInfo.isNullOrEmpty()) return null + + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } + config.server = uri.idnHost + config.serverPort = uri.port.toString() + + val result = if (uri.userInfo.contains(":")) { + uri.userInfo.split(":", limit = 2) + } else { + Utils.decode(uri.userInfo).split(":", limit = 2) + } + if (result.count() == 2) { + config.method = result.first() + config.password = result.last() + } + + if (!uri.rawQuery.isNullOrEmpty()) { + val queryParam = getQueryParam(uri) + if (queryParam["plugin"]?.contains("obfs=http") == true) { + val queryPairs = HashMap() + for (pair in queryParam["plugin"]?.split(";") ?: listOf()) { + val idx = pair.split("=") + if (idx.count() == 2) { + queryPairs.put(idx.first(), idx.last()) + } + } + config.network = NetworkType.TCP.type + config.headerType = "http" + config.host = queryPairs["obfs-host"] + config.path = queryPairs["path"] + } + } + + return config + } + + /** + * Parses a legacy Shadowsocks URI string into a ProfileItem object. + * + * @param str the legacy Shadowsocks URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parseLegacy(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.SHADOWSOCKS) + 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) { + Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e) + } + + 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.server = match.groupValues[3].removeSurrounding("[", "]") + config.serverPort = match.groupValues[4] + config.password = match.groupValues[2] + config.method = match.groupValues[1].lowercase() + + return config + } + + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @return the converted URI string + */ + fun toUri(config: ProfileItem): String { + val pw = "${config.method}:${config.password}" + + return toUri(config, Utils.encode(pw), null) + } + + /** + * Converts a ProfileItem object to an OutboundBean object. + * + * @param profileItem the ProfileItem object to convert + * @return the converted OutboundBean object, or null if conversion fails + */ + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS) + + outboundBean?.settings?.servers?.first()?.let { server -> + server.address = getServerAddress(profileItem) + server.port = profileItem.serverPort.orEmpty().toInt() + server.password = profileItem.password + server.method = profileItem.method + } + + val sni = outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTransportSettings(it, profileItem) + } + + outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTlsSettings(it, profileItem, sni) + } + + return outboundBean + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt new file mode 100644 index 00000000..30bc08e4 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt @@ -0,0 +1,79 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object SocksFmt : FmtBase() { + /** + * Parses a Socks URI string into a ProfileItem object. + * + * @param str the Socks URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.SOCKS) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.idnHost.isEmpty()) return null + if (uri.port <= 0) return null + + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } + config.server = uri.idnHost + config.serverPort = uri.port.toString() + + if (uri.userInfo?.isEmpty() == false) { + val result = Utils.decode(uri.userInfo).split(":", limit = 2) + if (result.count() == 2) { + config.username = result.first() + config.password = result.last() + } + } + + return config + } + + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @return the converted URI string + */ + fun toUri(config: ProfileItem): String { + val pw = + if (config.username.isNotNullEmpty()) + "${config.username}:${config.password}" + else + ":" + + return toUri(config, Utils.encode(pw), null) + } + + /** + * Converts a ProfileItem object to an OutboundBean object. + * + * @param profileItem the ProfileItem object to convert + * @return the converted OutboundBean object, or null if conversion fails + */ + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS) + + outboundBean?.settings?.servers?.first()?.let { server -> + server.address = getServerAddress(profileItem) + server.port = profileItem.serverPort.orEmpty().toInt() + if (profileItem.username.isNotNullEmpty()) { + val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() + socksUsersBean.user = profileItem.username.orEmpty() + socksUsersBean.pass = profileItem.password.orEmpty() + server.users = listOf(socksUsersBean) + } + } + + return outboundBean + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt new file mode 100644 index 00000000..446ef99c --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt @@ -0,0 +1,83 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.NetworkType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object TrojanFmt : FmtBase() { + /** + * Parses a Trojan URI string into a ProfileItem object. + * + * @param str the Trojan URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.TROJAN) + + val uri = URI(Utils.fixIllegalUrl(str)) + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } + config.server = uri.idnHost + config.serverPort = uri.port.toString() + config.password = uri.userInfo + + if (uri.rawQuery.isNullOrEmpty()) { + config.network = NetworkType.TCP.type + config.security = AppConfig.TLS + config.insecure = allowInsecure + } else { + val queryParam = getQueryParam(uri) + + getItemFormQuery(config, queryParam, allowInsecure) + config.security = queryParam["security"] ?: AppConfig.TLS + } + + return config + } + + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @return the converted URI string + */ + fun toUri(config: ProfileItem): String { + val dicQuery = getQueryDic(config) + + return toUri(config, config.password, dicQuery) + } + + /** + * Converts a ProfileItem object to an OutboundBean object. + * + * @param profileItem the ProfileItem object to convert + * @return the converted OutboundBean object, or null if conversion fails + */ + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN) + + outboundBean?.settings?.servers?.first()?.let { server -> + server.address = getServerAddress(profileItem) + server.port = profileItem.serverPort.orEmpty().toInt() + server.password = profileItem.password + server.flow = profileItem.flow + } + + val sni = outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTransportSettings(it, profileItem) + } + + outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTlsSettings(it, profileItem, sni) + } + + return outboundBean + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt new file mode 100644 index 00000000..9242f0ec --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt @@ -0,0 +1,80 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object VlessFmt : FmtBase() { + + /** + * Parses a Vless URI string into a ProfileItem object. + * + * @param str the Vless URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.VLESS) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.rawQuery.isNullOrEmpty()) return null + val queryParam = getQueryParam(uri) + + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } + config.server = uri.idnHost + config.serverPort = uri.port.toString() + config.password = uri.userInfo + config.method = queryParam["encryption"] ?: "none" + + getItemFormQuery(config, queryParam, allowInsecure) + + return config + } + + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @return the converted URI string + */ + fun toUri(config: ProfileItem): String { + val dicQuery = getQueryDic(config) + dicQuery["encryption"] = config.method ?: "none" + + return toUri(config, config.password, dicQuery) + } + + /** + * Converts a ProfileItem object to an OutboundBean object. + * + * @param profileItem the ProfileItem object to convert + * @return the converted OutboundBean object, or null if conversion fails + */ + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS) + + outboundBean?.settings?.vnext?.first()?.let { vnext -> + vnext.address = getServerAddress(profileItem) + vnext.port = profileItem.serverPort.orEmpty().toInt() + vnext.users[0].id = profileItem.password.orEmpty() + vnext.users[0].encryption = profileItem.method + vnext.users[0].flow = profileItem.flow + } + + val sni = outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTransportSettings(it, profileItem) + } + + outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTlsSettings(it, profileItem, sni) + } + + return outboundBean + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt new file mode 100644 index 00000000..4201f4dc --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt @@ -0,0 +1,192 @@ +package com.v2ray.ang.fmt + +import android.text.TextUtils +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.NetworkType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.dto.VmessQRCode +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils +import java.net.URI + +object VmessFmt : FmtBase() { + /** + * Parses a Vmess string into a ProfileItem object. + * + * @param str the Vmess string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + if (str.indexOf('?') > 0 && str.indexOf('&') > 0) { + return parseVmessStd(str) + } + + var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.VMESS) + + var result = str.replace(EConfigType.VMESS.protocolScheme, "") + result = Utils.decode(result) + if (TextUtils.isEmpty(result)) { + Log.w(AppConfig.TAG, "Toast decoding failed") + return null + } + val vmessQRCode = JsonUtil.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.w(AppConfig.TAG, "Toast incorrect protocol") + return null + } + + config.remarks = vmessQRCode.ps + config.server = vmessQRCode.add + config.serverPort = vmessQRCode.port + config.password = vmessQRCode.id + config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy + + config.network = vmessQRCode.net ?: NetworkType.TCP.type + config.headerType = vmessQRCode.type + config.host = vmessQRCode.host + config.path = vmessQRCode.path + + when (NetworkType.fromString(config.network)) { + NetworkType.KCP -> { + config.seed = vmessQRCode.path + } + +// NetworkType.QUIC -> { +// config.quicSecurity = vmessQRCode.host +// config.quicKey = vmessQRCode.path +// } + + NetworkType.GRPC -> { + config.mode = vmessQRCode.type + config.serviceName = vmessQRCode.path + config.authority = vmessQRCode.host + } + + else -> {} + } + + config.security = vmessQRCode.tls + config.insecure = allowInsecure + config.sni = vmessQRCode.sni + config.fingerPrint = vmessQRCode.fp + config.alpn = vmessQRCode.alpn + + return config + } + + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @return the converted URI string + */ + fun toUri(config: ProfileItem): String { + val vmessQRCode = VmessQRCode() + + vmessQRCode.v = "2" + vmessQRCode.ps = config.remarks + vmessQRCode.add = config.server.orEmpty() + vmessQRCode.port = config.serverPort.orEmpty() + vmessQRCode.id = config.password.orEmpty() + vmessQRCode.scy = config.method.orEmpty() + vmessQRCode.aid = "0" + + vmessQRCode.net = config.network.orEmpty() + vmessQRCode.type = config.headerType.orEmpty() + when (NetworkType.fromString(config.network)) { + NetworkType.KCP -> { + vmessQRCode.path = config.seed.orEmpty() + } + +// NetworkType.QUIC -> { +// vmessQRCode.host = config.quicSecurity.orEmpty() +// vmessQRCode.path = config.quicKey.orEmpty() +// } + + NetworkType.GRPC -> { + vmessQRCode.type = config.mode.orEmpty() + vmessQRCode.path = config.serviceName.orEmpty() + vmessQRCode.host = config.authority.orEmpty() + } + + else -> {} + } + + config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() } + config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() } + + vmessQRCode.tls = config.security.orEmpty() + vmessQRCode.sni = config.sni.orEmpty() + vmessQRCode.fp = config.fingerPrint.orEmpty() + vmessQRCode.alpn = config.alpn.orEmpty() + + val json = JsonUtil.toJson(vmessQRCode) + return Utils.encode(json) + } + + /** + * Parses a standard Vmess URI string into a ProfileItem object. + * + * @param str the standard Vmess URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parseVmessStd(str: String): ProfileItem? { + val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) + val config = ProfileItem.create(EConfigType.VMESS) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.rawQuery.isNullOrEmpty()) return null + val queryParam = getQueryParam(uri) + + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } + config.server = uri.idnHost + config.serverPort = uri.port.toString() + config.password = uri.userInfo + config.method = AppConfig.DEFAULT_SECURITY + + getItemFormQuery(config, queryParam, allowInsecure) + + return config + } + + /** + * Converts a ProfileItem object to an OutboundBean object. + * + * @param profileItem the ProfileItem object to convert + * @return the converted OutboundBean object, or null if conversion fails + */ + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS) + + outboundBean?.settings?.vnext?.first()?.let { vnext -> + vnext.address = getServerAddress(profileItem) + vnext.port = profileItem.serverPort.orEmpty().toInt() + vnext.users[0].id = profileItem.password.orEmpty() + vnext.users[0].security = profileItem.method + } + + val sni = outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTransportSettings(it, profileItem) + } + + outboundBean?.streamSettings?.let { + V2rayConfigManager.populateTlsSettings(it, profileItem, sni) + } + + return outboundBean + } + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt new file mode 100644 index 00000000..8f1cec84 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt @@ -0,0 +1,149 @@ +package com.v2ray.ang.fmt + +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.extension.idnHost +import com.v2ray.ang.extension.removeWhiteSpace +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.Utils +import java.net.URI + +object WireguardFmt : FmtBase() { + /** + * Parses a URI string into a ProfileItem object. + * + * @param str the URI string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parse(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.WIREGUARD) + + val uri = URI(Utils.fixIllegalUrl(str)) + if (uri.rawQuery.isNullOrEmpty()) return null + val queryParam = getQueryParam(uri) + + config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it } + config.server = uri.idnHost + config.serverPort = uri.port.toString() + + config.secretKey = uri.userInfo.orEmpty() + config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4 + config.publicKey = queryParam["publickey"].orEmpty() + config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() } + config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) + config.reserved = queryParam["reserved"] ?: "0,0,0" + + return config + } + + /** + * Parses a Wireguard configuration file string into a ProfileItem object. + * + * @param str the Wireguard configuration file string to parse + * @return the parsed ProfileItem object, or null if parsing fails + */ + fun parseWireguardConfFile(str: String): ProfileItem? { + val config = ProfileItem.create(EConfigType.WIREGUARD) + + val interfaceParams: MutableMap = mutableMapOf() + val peerParams: MutableMap = mutableMapOf() + + var currentSection: String? = null + + str.lines().forEach { line -> + val trimmedLine = line.trim() + + if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) { + return@forEach + } + + when { + trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface" + trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer" + else -> { + if (currentSection != null) { + val parts = trimmedLine.split("=", limit = 2).map { it.trim() } + if (parts.size == 2) { + val key = parts[0].lowercase() + val value = parts[1] + when (currentSection) { + "Interface" -> interfaceParams[key] = value + "Peer" -> peerParams[key] = value + } + } + } + } + } + } + + config.secretKey = interfaceParams["privatekey"].orEmpty() + config.remarks = System.currentTimeMillis().toString() + config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4 + config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) + config.publicKey = peerParams["publickey"].orEmpty() + config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() } + val endpoint = peerParams["endpoint"].orEmpty() + val endpointParts = endpoint.split(":", limit = 2) + if (endpointParts.size == 2) { + config.server = endpointParts[0] + config.serverPort = endpointParts[1] + } else { + config.server = endpoint + config.serverPort = "" + } + config.reserved = peerParams["reserved"] ?: "0,0,0" + + return config + } + + /** + * Converts a ProfileItem object to an OutboundBean object. + * + * @param profileItem the ProfileItem object to convert + * @return the converted OutboundBean object, or null if conversion fails + */ + fun toOutbound(profileItem: ProfileItem): OutboundBean? { + val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD) + + outboundBean?.settings?.let { wireguard -> + wireguard.secretKey = profileItem.secretKey + wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",") + wireguard.peers?.firstOrNull()?.let { peer -> + peer.publicKey = profileItem.publicKey.orEmpty() + peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() } + peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}" + } + wireguard.mtu = profileItem.mtu + wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() } + } + + return outboundBean + } + + /** + * Converts a ProfileItem object to a URI string. + * + * @param config the ProfileItem object to convert + * @return the converted URI string + */ + fun toUri(config: ProfileItem): String { + val dicQuery = HashMap() + + dicQuery["publickey"] = config.publicKey.orEmpty() + if (config.reserved != null) { + dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty() + } + dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty() + if (config.mtu != null) { + dicQuery["mtu"] = config.mtu.toString() + } + if (config.preSharedKey != null) { + dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty() + } + + return toUri(config, config.secretKey, dicQuery) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt new file mode 100644 index 00000000..d24ae0c2 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt @@ -0,0 +1,493 @@ +package com.v2ray.ang.handler + +import android.content.Context +import android.graphics.Bitmap +import android.text.TextUtils +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.HY2 +import com.v2ray.ang.R +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.SubscriptionItem +import com.v2ray.ang.fmt.CustomFmt +import com.v2ray.ang.fmt.Hysteria2Fmt +import com.v2ray.ang.fmt.ShadowsocksFmt +import com.v2ray.ang.fmt.SocksFmt +import com.v2ray.ang.fmt.TrojanFmt +import com.v2ray.ang.fmt.VlessFmt +import com.v2ray.ang.fmt.VmessFmt +import com.v2ray.ang.fmt.WireguardFmt +import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.QRCodeDecoder +import com.v2ray.ang.util.Utils +import java.net.URI + +object AngConfigManager { + + + /** + * Shares the configuration to the clipboard. + * + * @param context The context. + * @param guid The GUID of the configuration. + * @return The result code. + */ + fun share2Clipboard(context: Context, guid: String): Int { + try { + val conf = shareConfig(guid) + if (TextUtils.isEmpty(conf)) { + return -1 + } + + Utils.setClipboard(context, conf) + + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to share config to clipboard", e) + return -1 + } + return 0 + } + + /** + * Shares non-custom configurations to the clipboard. + * + * @param context The context. + * @param serverList The list of server GUIDs. + * @return The number of configurations shared. + */ + fun shareNonCustomConfigsToClipboard(context: Context, serverList: List): Int { + try { + val sb = StringBuilder() + for (guid in serverList) { + val url = shareConfig(guid) + if (TextUtils.isEmpty(url)) { + continue + } + sb.append(url) + sb.appendLine() + } + if (sb.count() > 0) { + Utils.setClipboard(context, sb.toString()) + } + return sb.lines().count() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e) + return -1 + } + } + + /** + * Shares the configuration as a QR code. + * + * @param guid The GUID of the configuration. + * @return The QR code bitmap. + */ + fun share2QRCode(guid: String): Bitmap? { + try { + val conf = shareConfig(guid) + if (TextUtils.isEmpty(conf)) { + return null + } + return QRCodeDecoder.createQRCode(conf) + + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to share config as QR code", e) + return null + } + } + + /** + * Shares the full content of the configuration to the clipboard. + * + * @param context The context. + * @param guid The GUID of the configuration. + * @return The result code. + */ + fun shareFullContent2Clipboard(context: Context, guid: String?): Int { + try { + if (guid == null) return -1 + val result = V2rayConfigManager.getV2rayConfig(context, guid) + if (result.status) { + val config = MmkvManager.decodeServerConfig(guid) + if (config?.configType == EConfigType.HYSTERIA2) { + val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0)) + val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) + Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content) + return 0 + } + Utils.setClipboard(context, result.content) + } else { + return -1 + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e) + return -1 + } + return 0 + } + + /** + * Shares the configuration. + * + * @param guid The GUID of the configuration. + * @return The configuration string. + */ + private fun shareConfig(guid: String): String { + try { + val config = MmkvManager.decodeServerConfig(guid) ?: return "" + + return config.configType.protocolScheme + when (config.configType) { + EConfigType.VMESS -> VmessFmt.toUri(config) + EConfigType.CUSTOM -> "" + EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config) + EConfigType.SOCKS -> SocksFmt.toUri(config) + EConfigType.HTTP -> "" + EConfigType.VLESS -> VlessFmt.toUri(config) + EConfigType.TROJAN -> TrojanFmt.toUri(config) + EConfigType.WIREGUARD -> WireguardFmt.toUri(config) + EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e) + return "" + } + } + + /** + * Imports a batch of configurations. + * + * @param server The server string. + * @param subid The subscription ID. + * @param append Whether to append the configurations. + * @return A pair containing the number of configurations and subscriptions imported. + */ + fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair { + var count = parseBatchConfig(Utils.decode(server), subid, append) + if (count <= 0) { + count = parseBatchConfig(server, subid, append) + } + if (count <= 0) { + count = parseCustomConfigServer(server, subid) + } + + var countSub = parseBatchSubscription(server) + if (countSub <= 0) { + countSub = parseBatchSubscription(Utils.decode(server)) + } + if (countSub > 0) { + updateConfigViaSubAll() + } + + return count to countSub + } + + /** + * Parses a batch of subscriptions. + * + * @param servers The servers string. + * @return The number of subscriptions parsed. + */ + private fun parseBatchSubscription(servers: String?): Int { + try { + if (servers == null) { + return 0 + } + + var count = 0 + servers.lines() + .distinct() + .forEach { str -> + if (Utils.isValidSubUrl(str)) { + count += importUrlAsSubscription(str) + } + } + return count + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to parse batch subscription", e) + } + return 0 + } + + /** + * Parses a batch of configurations. + * + * @param servers The servers string. + * @param subid The subscription ID. + * @param append Whether to append the configurations. + * @return The number of configurations parsed. + */ + private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int { + try { + if (servers == null) { + return 0 + } + val removedSelectedServer = + if (!TextUtils.isEmpty(subid) && !append) { + MmkvManager.decodeServerConfig( + MmkvManager.getSelectServer().orEmpty() + )?.let { + if (it.subscriptionId == subid) { + return@let it + } + return@let null + } + } else { + null + } + if (!append) { + MmkvManager.removeServerViaSubid(subid) + } + + val subItem = MmkvManager.decodeSubscription(subid) + var count = 0 + servers.lines() + .distinct() + .reversed() + .forEach { + val resId = parseConfig(it, subid, subItem, removedSelectedServer) + if (resId == 0) { + count++ + } + } + return count + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to parse batch config", e) + } + return 0 + } + + /** + * Parses a custom configuration server. + * + * @param server The server string. + * @param subid The subscription ID. + * @return The number of configurations parsed. + */ + private fun parseCustomConfigServer(server: String?, subid: String): Int { + if (server == null) { + return 0 + } + if (server.contains("inbounds") + && server.contains("outbounds") + && server.contains("routing") + ) { + try { + val serverList: Array = + JsonUtil.fromJson(server, Array::class.java) + + if (serverList.isNotEmpty()) { + var count = 0 + for (srv in serverList.reversed()) { + val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue + config.subscriptionId = subid + val key = MmkvManager.encodeServerConfig("", config) + MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "") + count += 1 + } + return count + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e) + } + + try { + // For compatibility + val config = CustomFmt.parse(server) ?: return 0 + config.subscriptionId = subid + val key = MmkvManager.encodeServerConfig("", config) + MmkvManager.encodeServerRaw(key, server) + return 1 + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e) + } + return 0 + } else if (server.startsWith("[Interface]") && server.contains("[Peer]")) { + try { + val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol + val key = MmkvManager.encodeServerConfig("", config) + MmkvManager.encodeServerRaw(key, server) + return 1 + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e) + } + return 0 + } else { + return 0 + } + } + + /** + * Parses the configuration from a QR code or string. + * + * @param str The configuration string. + * @param subid The subscription ID. + * @param subItem The subscription item. + * @param removedSelectedServer The removed selected server. + * @return The result code. + */ + private fun parseConfig( + str: String?, + subid: String, + subItem: SubscriptionItem?, + removedSelectedServer: ProfileItem? + ): Int { + try { + if (str == null || TextUtils.isEmpty(str)) { + return R.string.toast_none_data + } + + val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) { + VmessFmt.parse(str) + } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { + ShadowsocksFmt.parse(str) + } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) { + SocksFmt.parse(str) + } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) { + TrojanFmt.parse(str) + } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) { + VlessFmt.parse(str) + } else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) { + WireguardFmt.parse(str) + } else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) { + Hysteria2Fmt.parse(str) + } else { + null + } + + if (config == null) { + return R.string.toast_incorrect_protocol + } + //filter + if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) { + val matched = Regex(pattern = subItem.filter ?: "") + .containsMatchIn(input = config.remarks) + if (!matched) return -1 + } + + config.subscriptionId = subid + val guid = MmkvManager.encodeServerConfig("", config) + if (removedSelectedServer != null && + config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort + ) { + MmkvManager.setSelectServer(guid) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to parse config", e) + return -1 + } + return 0 + } + + /** + * Updates the configuration via all subscriptions. + * + * @return The number of configurations updated. + */ + fun updateConfigViaSubAll(): Int { + var count = 0 + try { + MmkvManager.decodeSubscriptions().forEach { + count += updateConfigViaSub(it) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e) + return 0 + } + return count + } + + /** + * Updates the configuration via a subscription. + * + * @param it The subscription item. + * @return The number of configurations updated. + */ + fun updateConfigViaSub(it: Pair): Int { + try { + if (TextUtils.isEmpty(it.first) + || TextUtils.isEmpty(it.second.remarks) + || TextUtils.isEmpty(it.second.url) + ) { + return 0 + } + if (!it.second.enabled) { + return 0 + } + val url = HttpUtil.toIdnUrl(it.second.url) + if (!Utils.isValidUrl(url)) { + return 0 + } + if (!it.second.allowInsecureUrl) { + if (!Utils.isValidSubUrl(url)) { + return 0 + } + } + Log.i(AppConfig.TAG, url) + + var configText = try { + val httpPort = SettingsManager.getHttpPort() + HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort) + } catch (e: Exception) { + Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e) + "" + } + if (configText.isEmpty()) { + configText = try { + HttpUtil.getUrlContentWithUserAgent(url) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e) + "" + } + } + if (configText.isEmpty()) { + return 0 + } + return parseConfigViaSub(configText, it.first, false) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to update config via subscription", e) + return 0 + } + } + + /** + * Parses the configuration via a subscription. + * + * @param server The server string. + * @param subid The subscription ID. + * @param append Whether to append the configurations. + * @return The number of configurations parsed. + */ + private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int { + var count = parseBatchConfig(Utils.decode(server), subid, append) + if (count <= 0) { + count = parseBatchConfig(server, subid, append) + } + if (count <= 0) { + count = parseCustomConfigServer(server, subid) + } + return count + } + + /** + * Imports a URL as a subscription. + * + * @param url The URL. + * @return The number of subscriptions imported. + */ + private fun importUrlAsSubscription(url: String): Int { + val subscriptions = MmkvManager.decodeSubscriptions() + subscriptions.forEach { + if (it.second.url == url) { + return 0 + } + } + val uri = URI(Utils.fixIllegalUrl(url)) + val subItem = SubscriptionItem() + subItem.remarks = uri.fragment ?: "import sub" + subItem.url = url + MmkvManager.encodeSubscription("", subItem) + return 1 + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt new file mode 100644 index 00000000..9dd90c76 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MigrateManager.kt @@ -0,0 +1,242 @@ +package com.v2ray.ang.handler + +import android.util.Log +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.NetworkType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.extension.removeWhiteSpace +import com.v2ray.ang.handler.MmkvManager.decodeServerConfig +import com.v2ray.ang.util.JsonUtil + +object MigrateManager { + private const val ID_SERVER_CONFIG = "SERVER_CONFIG" + private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } + + /** + * Migrates server configurations to profile items. + * + * @return True if migration was successful, false otherwise. + */ + fun migrateServerConfig2Profile(): Boolean { + if (serverStorage.count().toInt() == 0) { + return false + } + val serverList = serverStorage.allKeys() ?: return false + Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + serverList.count()) + + for (guid in serverList) { + var configOld = decodeServerConfigOld(guid) ?: continue + var config = decodeServerConfig(guid) + if (config != null) { + serverStorage.remove(guid) + continue + } + config = migrateServerConfig2ProfileSub(configOld) ?: continue + config.subscriptionId = configOld.subscriptionId + + MmkvManager.encodeServerConfig(guid, config) + + //check and remove old + decodeServerConfig(guid) ?: continue + serverStorage.remove(guid) + Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + config.remarks) + } + Log.i(AppConfig.TAG, "migrateServerConfig2Profile-end") + return true + } + + /** + * Migrates a server configuration to a profile item. + * + * @param configOld The old server configuration. + * @return The profile item. + */ + private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? { + return when (configOld.getProxyOutbound()?.protocol) { + EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld) + EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld) + EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld) + EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld) + + EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld) + EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld) + EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld) + EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld) + + EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld) + + else -> null + } + } + + /** + * Migrates a common server configuration to a profile item. + * + * @param configOld The old server configuration. + * @return The profile item. + */ + private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(configOld.configType) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + config.method = outbound.getSecurityEncryption() + config.password = outbound.getPassword() + config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow + + config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type + outbound.getTransportSettingDetails()?.let { transportDetails -> + config.headerType = transportDetails[0].orEmpty() + config.host = transportDetails[1].orEmpty() + config.path = transportDetails[2].orEmpty() + } + + config.seed = outbound?.streamSettings?.kcpSettings?.seed + config.quicSecurity = outbound?.streamSettings?.quicSettings?.security + config.quicKey = outbound?.streamSettings?.quicSettings?.key + config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun" + config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName + config.authority = outbound?.streamSettings?.grpcSettings?.authority + + config.security = outbound.streamSettings?.security + val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings + config.insecure = tlsSettings?.allowInsecure + config.sni = tlsSettings?.serverName + config.fingerPrint = tlsSettings?.fingerprint + config.alpn = tlsSettings?.alpn?.joinToString(",").removeWhiteSpace().toString() + + config.publicKey = tlsSettings?.publicKey + config.shortId = tlsSettings?.shortId + config.spiderX = tlsSettings?.spiderX + + return config + } + + /** + * Migrates a SOCKS server configuration to a profile item. + * + * @param configOld The old server configuration. + * @return The profile item. + */ + private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.SOCKS) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + config.username = outbound.settings?.servers?.first()?.users?.first()?.user + config.password = outbound.getPassword() + + return config + } + + /** + * Migrates an HTTP server configuration to a profile item. + * + * @param configOld The old server configuration. + * @return The profile item. + */ + private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.HTTP) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + config.username = outbound.settings?.servers?.first()?.users?.first()?.user + config.password = outbound.getPassword() + + return config + } + + /** + * Migrates a WireGuard server configuration to a profile item. + * + * @param configOld The old server configuration. + * @return The profile item. + */ + private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.WIREGUARD) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + + outbound.settings?.let { wireguard -> + config.secretKey = wireguard.secretKey + config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString() + config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey + config.mtu = wireguard.mtu + config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString() + } + return config + } + + /** + * Migrates a Hysteria2 server configuration to a profile item. + * + * @param configOld The old server configuration. + * @return The profile item. + */ + private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.HYSTERIA2) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + config.password = outbound.getPassword() + + config.security = AppConfig.TLS + outbound.streamSettings?.tlsSettings?.let { tlsSetting -> + config.insecure = tlsSetting.allowInsecure + config.sni = tlsSetting.serverName + config.alpn = tlsSetting.alpn?.joinToString(",").removeWhiteSpace().orEmpty() + + } + config.obfsPassword = outbound.settings?.obfsPassword + + return config + } + + /** + * Migrates a custom server configuration to a profile item. + * + * @param configOld The old server configuration. + * @return The profile item. + */ + private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? { + val config = ProfileItem.create(EConfigType.CUSTOM) + + val outbound = configOld.getProxyOutbound() ?: return null + config.remarks = configOld.remarks + config.server = outbound.getServerAddress() + config.serverPort = outbound.getServerPort().toString() + + return config + } + + /** + * Decodes the old server configuration. + * + * @param guid The server GUID. + * @return The old server configuration. + */ + private fun decodeServerConfigOld(guid: String): ServerConfig? { + if (guid.isBlank()) { + return null + } + val json = serverStorage.decodeString(guid) + if (json.isNullOrBlank()) { + return null + } + return JsonUtil.fromJson(json, ServerConfig::class.java) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt new file mode 100644 index 00000000..9b589dd0 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt @@ -0,0 +1,588 @@ +package com.v2ray.ang.handler + +import com.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig.PREF_IS_BOOTED +import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET +import com.v2ray.ang.dto.AssetUrlItem +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.dto.ServerAffiliationInfo +import com.v2ray.ang.dto.SubscriptionItem +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils + +object MmkvManager { + + //region private + + //private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG" + private const val ID_MAIN = "MAIN" + private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG" + private const val ID_SERVER_RAW = "SERVER_RAW" + private const val ID_SERVER_AFF = "SERVER_AFF" + private const val ID_SUB = "SUB" + private const val ID_ASSET = "ASSET" + private const val ID_SETTING = "SETTING" + private const val KEY_SELECTED_SERVER = "SELECTED_SERVER" + private const val KEY_ANG_CONFIGS = "ANG_CONFIGS" + private const val KEY_SUB_IDS = "SUB_IDS" + + //private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) } + private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } + private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) } + private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, 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) } + private val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) } + + //endregion + + //region Server + + /** + * Gets the selected server GUID. + * + * @return The selected server GUID. + */ + fun getSelectServer(): String? { + return mainStorage.decodeString(KEY_SELECTED_SERVER) + } + + /** + * Sets the selected server GUID. + * + * @param guid The server GUID. + */ + fun setSelectServer(guid: String) { + mainStorage.encode(KEY_SELECTED_SERVER, guid) + } + + /** + * Encodes the server list. + * + * @param serverList The list of server GUIDs. + */ + fun encodeServerList(serverList: MutableList) { + mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList)) + } + + /** + * Decodes the server list. + * + * @return The list of server GUIDs. + */ + fun decodeServerList(): MutableList { + val json = mainStorage.decodeString(KEY_ANG_CONFIGS) + return if (json.isNullOrBlank()) { + mutableListOf() + } else { + JsonUtil.fromJson(json, Array::class.java).toMutableList() + } + } + + /** + * Decodes the server configuration. + * + * @param guid The server GUID. + * @return The server configuration. + */ + fun decodeServerConfig(guid: String): ProfileItem? { + if (guid.isBlank()) { + return null + } + val json = profileFullStorage.decodeString(guid) + if (json.isNullOrBlank()) { + return null + } + return JsonUtil.fromJson(json, ProfileItem::class.java) + } + +// fun decodeProfileConfig(guid: String): ProfileLiteItem? { +// if (guid.isBlank()) { +// return null +// } +// val json = profileStorage.decodeString(guid) +// if (json.isNullOrBlank()) { +// return null +// } +// return JsonUtil.fromJson(json, ProfileLiteItem::class.java) +// } + + /** + * Encodes the server configuration. + * + * @param guid The server GUID. + * @param config The server configuration. + * @return The server GUID. + */ + fun encodeServerConfig(guid: String, config: ProfileItem): String { + val key = guid.ifBlank { Utils.getUuid() } + profileFullStorage.encode(key, JsonUtil.toJson(config)) + val serverList = decodeServerList() + if (!serverList.contains(key)) { + serverList.add(0, key) + encodeServerList(serverList) + if (getSelectServer().isNullOrBlank()) { + mainStorage.encode(KEY_SELECTED_SERVER, key) + } + } +// val profile = ProfileLiteItem( +// configType = config.configType, +// subscriptionId = config.subscriptionId, +// remarks = config.remarks, +// server = config.getProxyOutbound()?.getServerAddress(), +// serverPort = config.getProxyOutbound()?.getServerPort(), +// ) +// profileStorage.encode(key, JsonUtil.toJson(profile)) + return key + } + + /** + * Removes the server configuration. + * + * @param guid The server GUID. + */ + fun removeServer(guid: String) { + if (guid.isBlank()) { + return + } + if (getSelectServer() == guid) { + mainStorage.remove(KEY_SELECTED_SERVER) + } + val serverList = decodeServerList() + serverList.remove(guid) + encodeServerList(serverList) + profileFullStorage.remove(guid) + //profileStorage.remove(guid) + serverAffStorage.remove(guid) + } + + /** + * Removes the server configurations via subscription ID. + * + * @param subid The subscription ID. + */ + fun removeServerViaSubid(subid: String) { + if (subid.isBlank()) { + return + } + profileFullStorage.allKeys()?.forEach { key -> + decodeServerConfig(key)?.let { config -> + if (config.subscriptionId == subid) { + removeServer(key) + } + } + } + } + + /** + * Decodes the server affiliation information. + * + * @param guid The server GUID. + * @return The server affiliation information. + */ + fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? { + if (guid.isBlank()) { + return null + } + val json = serverAffStorage.decodeString(guid) + if (json.isNullOrBlank()) { + return null + } + return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java) + } + + /** + * Encodes the server test delay in milliseconds. + * + * @param guid The server GUID. + * @param testResult The test delay in milliseconds. + */ + fun encodeServerTestDelayMillis(guid: String, testResult: Long) { + if (guid.isBlank()) { + return + } + val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo() + aff.testDelayMillis = testResult + serverAffStorage.encode(guid, JsonUtil.toJson(aff)) + } + + /** + * Clears all test delay results. + * + * @param keys The list of server GUIDs. + */ + fun clearAllTestDelayResults(keys: List?) { + keys?.forEach { key -> + decodeServerAffiliationInfo(key)?.let { aff -> + aff.testDelayMillis = 0 + serverAffStorage.encode(key, JsonUtil.toJson(aff)) + } + } + } + + /** + * Removes all server configurations. + * + * @return The number of server configurations removed. + */ + fun removeAllServer(): Int { + val count = profileFullStorage.allKeys()?.count() ?: 0 + mainStorage.clearAll() + profileFullStorage.clearAll() + //profileStorage.clearAll() + serverAffStorage.clearAll() + return count + } + + /** + * Removes invalid server configurations. + * + * @param guid The server GUID. + * @return The number of server configurations removed. + */ + fun removeInvalidServer(guid: String): Int { + var count = 0 + if (guid.isNotEmpty()) { + decodeServerAffiliationInfo(guid)?.let { aff -> + if (aff.testDelayMillis < 0L) { + removeServer(guid) + count++ + } + } + } else { + serverAffStorage.allKeys()?.forEach { key -> + decodeServerAffiliationInfo(key)?.let { aff -> + if (aff.testDelayMillis < 0L) { + removeServer(key) + count++ + } + } + } + } + return count + } + + /** + * Encodes the raw server configuration. + * + * @param guid The server GUID. + * @param config The raw server configuration. + */ + fun encodeServerRaw(guid: String, config: String) { + serverRawStorage.encode(guid, config) + } + + /** + * Decodes the raw server configuration. + * + * @param guid The server GUID. + * @return The raw server configuration. + */ + fun decodeServerRaw(guid: String): String? { + return serverRawStorage.decodeString(guid) + } + + //endregion + + //region Subscriptions + + /** + * Initializes the subscription list. + */ + private fun initSubsList() { + val subsList = decodeSubsList() + if (subsList.isNotEmpty()) { + return + } + subStorage.allKeys()?.forEach { key -> + subsList.add(key) + } + encodeSubsList(subsList) + } + + /** + * Decodes the subscriptions. + * + * @return The list of subscriptions. + */ + fun decodeSubscriptions(): List> { + initSubsList() + + val subscriptions = mutableListOf>() + decodeSubsList().forEach { key -> + val json = subStorage.decodeString(key) + if (!json.isNullOrBlank()) { + subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java))) + } + } + return subscriptions + } + + /** + * Removes the subscription. + * + * @param subid The subscription ID. + */ + fun removeSubscription(subid: String) { + subStorage.remove(subid) + val subsList = decodeSubsList() + subsList.remove(subid) + encodeSubsList(subsList) + + removeServerViaSubid(subid) + } + + /** + * Encodes the subscription. + * + * @param guid The subscription GUID. + * @param subItem The subscription item. + */ + fun encodeSubscription(guid: String, subItem: SubscriptionItem) { + val key = guid.ifBlank { Utils.getUuid() } + subStorage.encode(key, JsonUtil.toJson(subItem)) + + val subsList = decodeSubsList() + if (!subsList.contains(key)) { + subsList.add(key) + encodeSubsList(subsList) + } + } + + /** + * Decodes the subscription. + * + * @param subscriptionId The subscription ID. + * @return The subscription item. + */ + fun decodeSubscription(subscriptionId: String): SubscriptionItem? { + val json = subStorage.decodeString(subscriptionId) ?: return null + return JsonUtil.fromJson(json, SubscriptionItem::class.java) + } + + /** + * Encodes the subscription list. + * + * @param subsList The list of subscription IDs. + */ + fun encodeSubsList(subsList: MutableList) { + mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList)) + } + + /** + * Decodes the subscription list. + * + * @return The list of subscription IDs. + */ + fun decodeSubsList(): MutableList { + val json = mainStorage.decodeString(KEY_SUB_IDS) + return if (json.isNullOrBlank()) { + mutableListOf() + } else { + JsonUtil.fromJson(json, Array::class.java).toMutableList() + } + } + + //endregion + + //region Asset + + /** + * Decodes the asset URLs. + * + * @return The list of asset URLs. + */ + fun decodeAssetUrls(): List> { + val assetUrlItems = mutableListOf>() + assetStorage.allKeys()?.forEach { key -> + val json = assetStorage.decodeString(key) + if (!json.isNullOrBlank()) { + assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java))) + } + } + return assetUrlItems.sortedBy { (_, value) -> value.addedTime } + } + + /** + * Removes the asset URL. + * + * @param assetid The asset ID. + */ + fun removeAssetUrl(assetid: String) { + assetStorage.remove(assetid) + } + + /** + * Encodes the asset. + * + * @param assetid The asset ID. + * @param assetItem The asset item. + */ + fun encodeAsset(assetid: String, assetItem: AssetUrlItem) { + val key = assetid.ifBlank { Utils.getUuid() } + assetStorage.encode(key, JsonUtil.toJson(assetItem)) + } + + /** + * Decodes the asset. + * + * @param assetid The asset ID. + * @return The asset item. + */ + fun decodeAsset(assetid: String): AssetUrlItem? { + val json = assetStorage.decodeString(assetid) ?: return null + return JsonUtil.fromJson(json, AssetUrlItem::class.java) + } + + //endregion + + //region Routing + + /** + * Decodes the routing rulesets. + * + * @return The list of routing rulesets. + */ + fun decodeRoutingRulesets(): MutableList? { + val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET) + if (ruleset.isNullOrEmpty()) return null + return JsonUtil.fromJson(ruleset, Array::class.java).toMutableList() + } + + /** + * Encodes the routing rulesets. + * + * @param rulesetList The list of routing rulesets. + */ + fun encodeRoutingRulesets(rulesetList: MutableList?) { + if (rulesetList.isNullOrEmpty()) + encodeSettings(PREF_ROUTING_RULESET, "") + else + encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList)) + } + + //endregion + + /** + * Encodes the settings. + * + * @param key The settings key. + * @param value The settings value. + * @return Whether the encoding was successful. + */ + fun encodeSettings(key: String, value: String?): Boolean { + return settingsStorage.encode(key, value) + } + + /** + * Encodes the settings. + * + * @param key The settings key. + * @param value The settings value. + * @return Whether the encoding was successful. + */ + fun encodeSettings(key: String, value: Int): Boolean { + return settingsStorage.encode(key, value) + } + + /** + * Encodes the settings. + * + * @param key The settings key. + * @param value The settings value. + * @return Whether the encoding was successful. + */ + fun encodeSettings(key: String, value: Boolean): Boolean { + return settingsStorage.encode(key, value) + } + + /** + * Encodes the settings. + * + * @param key The settings key. + * @param value The settings value. + * @return Whether the encoding was successful. + */ + fun encodeSettings(key: String, value: MutableSet): Boolean { + return settingsStorage.encode(key, value) + } + + /** + * Decodes the settings string. + * + * @param key The settings key. + * @return The settings value. + */ + fun decodeSettingsString(key: String): String? { + return settingsStorage.decodeString(key) + } + + /** + * Decodes the settings string. + * + * @param key The settings key. + * @param defaultValue The default value. + * @return The settings value. + */ + fun decodeSettingsString(key: String, defaultValue: String?): String? { + return settingsStorage.decodeString(key, defaultValue) + } + + /** + * Decodes the settings boolean. + * + * @param key The settings key. + * @return The settings value. + */ + fun decodeSettingsBool(key: String): Boolean { + return settingsStorage.decodeBool(key, false) + } + + /** + * Decodes the settings boolean. + * + * @param key The settings key. + * @param defaultValue The default value. + * @return The settings value. + */ + fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean { + return settingsStorage.decodeBool(key, defaultValue) + } + + /** + * Decodes the settings string set. + * + * @param key The settings key. + * @return The settings value. + */ + fun decodeSettingsStringSet(key: String): MutableSet? { + return settingsStorage.decodeStringSet(key) + } + + //endregion + + //region Others + + /** + * Encodes the start on boot setting. + * + * @param startOnBoot Whether to start on boot. + */ + fun encodeStartOnBoot(startOnBoot: Boolean) { + encodeSettings(PREF_IS_BOOTED, startOnBoot) + } + + /** + * Decodes the start on boot setting. + * + * @return Whether to start on boot. + */ + fun decodeStartOnBoot(): Boolean { + return decodeSettingsBool(PREF_IS_BOOTED, false) + } + + //endregion + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt new file mode 100644 index 00000000..b2e23f7f --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt @@ -0,0 +1,373 @@ +package com.v2ray.ang.handler + +import android.content.Context +import android.content.res.AssetManager +import android.text.TextUtils +import android.util.Log +import androidx.appcompat.app.AppCompatDelegate +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.AppConfig.GEOIP_PRIVATE +import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.Language +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.RoutingType +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.dto.VpnInterfaceAddressConfig +import com.v2ray.ang.handler.MmkvManager.decodeServerConfig +import com.v2ray.ang.handler.MmkvManager.decodeServerList +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils +import java.io.File +import java.io.FileOutputStream +import java.util.Collections +import java.util.Locale + +object SettingsManager { + + /** + * Initialize routing rulesets. + * @param context The application context. + */ + fun initRoutingRulesets(context: Context) { + val exist = MmkvManager.decodeRoutingRulesets() + if (exist.isNullOrEmpty()) { + val rulesetList = getPresetRoutingRulesets(context) + MmkvManager.encodeRoutingRulesets(rulesetList) + } + } + + /** + * Get preset routing rulesets. + * @param context The application context. + * @param index The index of the routing type. + * @return A mutable list of RulesetItem. + */ + private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList? { + val fileName = RoutingType.fromIndex(index).fileName + val assets = Utils.readTextFromAssets(context, fileName) + if (TextUtils.isEmpty(assets)) { + return null + } + + return JsonUtil.fromJson(assets, Array::class.java).toMutableList() + } + + /** + * Reset routing rulesets from presets. + * @param context The application context. + * @param index The index of the routing type. + */ + fun resetRoutingRulesetsFromPresets(context: Context, index: Int) { + val rulesetList = getPresetRoutingRulesets(context, index) ?: return + resetRoutingRulesetsCommon(rulesetList) + } + + /** + * Reset routing rulesets. + * @param content The content of the rulesets. + * @return True if successful, false otherwise. + */ + fun resetRoutingRulesets(content: String?): Boolean { + if (content.isNullOrEmpty()) { + return false + } + + try { + val rulesetList = JsonUtil.fromJson(content, Array::class.java).toMutableList() + if (rulesetList.isNullOrEmpty()) { + return false + } + + resetRoutingRulesetsCommon(rulesetList) + return true + } catch (e: Exception) { + Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e) + return false + } + } + + /** + * Common method to reset routing rulesets. + * @param rulesetList The list of rulesets. + */ + private fun resetRoutingRulesetsCommon(rulesetList: MutableList) { + val rulesetNew: MutableList = mutableListOf() + MmkvManager.decodeRoutingRulesets()?.forEach { key -> + if (key.locked == true) { + rulesetNew.add(key) + } + } + + rulesetNew.addAll(rulesetList) + MmkvManager.encodeRoutingRulesets(rulesetNew) + } + + /** + * Get a routing ruleset by index. + * @param index The index of the ruleset. + * @return The RulesetItem. + */ + fun getRoutingRuleset(index: Int): RulesetItem? { + if (index < 0) return null + + val rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) return null + + return rulesetList[index] + } + + /** + * Save a routing ruleset. + * @param index The index of the ruleset. + * @param ruleset The RulesetItem to save. + */ + fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) { + if (ruleset == null) return + + var rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) { + rulesetList = mutableListOf() + } + + if (index < 0 || index >= rulesetList.count()) { + rulesetList.add(0, ruleset) + } else { + rulesetList[index] = ruleset + } + MmkvManager.encodeRoutingRulesets(rulesetList) + } + + /** + * Remove a routing ruleset by index. + * @param index The index of the ruleset. + */ + fun removeRoutingRuleset(index: Int) { + if (index < 0) return + + val rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) return + + rulesetList.removeAt(index) + MmkvManager.encodeRoutingRulesets(rulesetList) + } + + /** + * Check if routing rulesets bypass LAN. + * @return True if bypassing LAN, false otherwise. + */ + fun routingRulesetsBypassLan(): Boolean { + val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1" + if (vpnBypassLan == "1") { + return true + } else if (vpnBypassLan == "2") { + return false + } + + val guid = MmkvManager.getSelectServer() ?: return false + val config = decodeServerConfig(guid) ?: return false + if (config.configType == EConfigType.CUSTOM) { + val raw = MmkvManager.decodeServerRaw(guid) ?: return false + val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java) + val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }.any { + it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true + } + return exist == true + } + + val rulesetItems = MmkvManager.decodeRoutingRulesets() + val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any { + it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true + } + return exist == true + } + + /** + * Swap routing rulesets. + * @param fromPosition The position to swap from. + * @param toPosition The position to swap to. + */ + fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) { + val rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) return + + Collections.swap(rulesetList, fromPosition, toPosition) + MmkvManager.encodeRoutingRulesets(rulesetList) + } + + /** + * Swap subscriptions. + * @param fromPosition The position to swap from. + * @param toPosition The position to swap to. + */ + fun swapSubscriptions(fromPosition: Int, toPosition: Int) { + val subsList = MmkvManager.decodeSubsList() + if (subsList.isNullOrEmpty()) return + + Collections.swap(subsList, fromPosition, toPosition) + MmkvManager.encodeSubsList(subsList) + } + + /** + * Get server via remarks. + * @param remarks The remarks of the server. + * @return The ProfileItem. + */ + fun getServerViaRemarks(remarks: String?): ProfileItem? { + if (remarks.isNullOrEmpty()) { + return null + } + val serverList = decodeServerList() + for (guid in serverList) { + val profile = decodeServerConfig(guid) + if (profile != null && profile.remarks == remarks) { + return profile + } + } + return null + } + + /** + * Get the SOCKS port. + * @return The SOCKS port. + */ + fun getSocksPort(): Int { + return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) + } + + /** + * Get the HTTP port. + * @return The HTTP port. + */ + fun getHttpPort(): Int { + return getSocksPort() + if (Utils.isXray()) 0 else 1 + } + + /** + * Initialize assets. + * @param context The application context. + * @param assets The AssetManager. + */ + fun initAssets(context: Context, assets: AssetManager) { + val extFolder = Utils.userAssetPath(context) + + 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(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}") + } + } catch (e: Exception) { + Log.e(ANG_PACKAGE, "asset copy failed", e) + } + } + + /** + * Get domestic DNS servers from preference. + * @return A list of domestic DNS servers. + */ + fun getDomesticDnsServers(): List { + val domesticDns = + MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT + val ret = domesticDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) } + if (ret.isEmpty()) { + return listOf(AppConfig.DNS_DIRECT) + } + return ret + } + + /** + * Get remote DNS servers from preference. + * @return A list of remote DNS servers. + */ + fun getRemoteDnsServers(): List { + val remoteDns = + MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY + val ret = remoteDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) } + if (ret.isEmpty()) { + return listOf(AppConfig.DNS_PROXY) + } + return ret + } + + /** + * Get VPN DNS servers from preference. + * @return A list of VPN DNS servers. + */ + fun getVpnDnsServers(): List { + val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN + return vpnDns.split(",").filter { Utils.isPureIpAddress(it) } + } + + /** + * Get delay test URL. + * @param second Whether to use the second URL. + * @return The delay test URL. + */ + fun getDelayTestUrl(second: Boolean = false): String { + return if (second) { + AppConfig.DELAY_TEST_URL2 + } else { + MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) + ?: AppConfig.DELAY_TEST_URL + } + } + + /** + * Get the locale. + * @return The locale. + */ + fun getLocale(): Locale { + val langCode = + MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code + val language = Language.fromCode(langCode) + + return when (language) { + Language.AUTO -> Utils.getSysLocale() + Language.ENGLISH -> Locale.ENGLISH + Language.CHINA -> Locale.CHINA + Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE + Language.VIETNAMESE -> Locale("vi") + Language.RUSSIAN -> Locale("ru") + Language.PERSIAN -> Locale("fa") + Language.ARABIC -> Locale("ar") + Language.BANGLA -> Locale("bn") + Language.BAKHTIARI -> Locale("bqi", "IR") + } + } + + /** + * Set night mode. + */ + fun setNightMode() { + when (MmkvManager.decodeSettingsString(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) + } + } + + /** + * Retrieves the currently selected VPN interface address configuration. + * This method reads the user's preference for VPN interface addressing and returns + * the corresponding configuration containing IPv4 and IPv6 addresses. + * + * @return The selected VpnInterfaceAddressConfig instance, or the default configuration + * if no valid selection is found or if the stored index is invalid. + */ + fun getCurrentVpnInterfaceAddressConfig(): VpnInterfaceAddressConfig { + val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt() + return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt new file mode 100644 index 00000000..e547c378 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt @@ -0,0 +1,189 @@ +package com.v2ray.ang.handler + +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.dto.IPAPIInfo +import com.v2ray.ang.extension.responseLength +import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.JsonUtil +import kotlinx.coroutines.isActive +import libv2ray.Libv2ray +import java.io.IOException +import java.net.InetSocketAddress +import java.net.Socket +import java.net.UnknownHostException +import kotlin.coroutines.coroutineContext + +object SpeedtestManager { + + private val tcpTestingSockets = ArrayList() + + /** + * Measures the TCP connection time to a given URL and port. + * + * @param url The URL to connect to. + * @param port The port to connect to. + * @return The connection time in milliseconds, or -1 if the connection failed. + */ + 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 + } + + /** + * Measures the real ping time using the V2Ray library. + * + * @param config The configuration string for the V2Ray library. + * @return The ping time in milliseconds, or -1 if the ping failed. + */ + fun realPing(config: String): Long { + return try { + Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl()) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to measure outbound delay", e) + -1L + } + } + + /** + * Measures the ping time to a given URL using the system ping command. + * + * @param url The URL to ping. + * @return The ping time in milliseconds as a string, or "-1ms" if the ping failed. + */ + 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) { + Log.e(AppConfig.TAG, "Failed to ping URL: $url", e) + } + return "-1ms" + } + + /** + * Measures the time taken to establish a TCP connection to a given URL and port. + * + * @param url The URL to connect to. + * @param port The port to connect to. + * @return The connection time in milliseconds, or -1 if the connection failed. + */ + 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) { + Log.e(AppConfig.TAG, "Unknown host: $url", e) + } catch (e: IOException) { + Log.e(AppConfig.TAG, "socketConnectTime IOException: $e") + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e) + } + return -1 + } + + /** + * Closes all TCP sockets that are currently being tested. + */ + fun closeAllTcpSockets() { + synchronized(this) { + tcpTestingSockets.forEach { + it?.close() + } + tcpTestingSockets.clear() + } + } + + /** + * Tests the connection to a given URL and port. + * + * @param context The Context in which the test is running. + * @param port The port to connect to. + * @return A pair containing the elapsed time in milliseconds and the result message. + */ + fun testConnection(context: Context, port: Int): Pair { + var result: String + var elapsed = -1L + + val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "") + try { + val start = SystemClock.elapsedRealtime() + val code = conn.responseCode + 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) { + Log.e(AppConfig.TAG, "Connection test IOException", e) + result = context.getString(R.string.connection_test_error, e.message) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Connection test Exception", e) + result = context.getString(R.string.connection_test_error, e.message) + } finally { + conn.disconnect() + } + + return Pair(elapsed, result) + } + + fun getRemoteIPInfo(): String? { + val httpPort = SettingsManager.getHttpPort() + var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null + + var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null + var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query + var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode + + return "(${country ?: "unknown"}) $ip" + } + + /** + * Gets the version of the V2Ray library. + * + * @return The version of the V2Ray library. + */ + fun getLibVersion(): String { + return Libv2ray.checkVersionX() + } + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt new file mode 100644 index 00000000..37b55c2e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt @@ -0,0 +1,107 @@ +package com.v2ray.ang.handler + +import android.content.Context +import android.os.Build +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.BuildConfig +import com.v2ray.ang.dto.CheckUpdateResult +import com.v2ray.ang.dto.GitHubRelease +import com.v2ray.ang.extension.concatUrl +import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.JsonUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream + +object UpdateCheckerManager { + suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) { + val url = if (includePreRelease) { + AppConfig.APP_API_URL + } else { + AppConfig.APP_API_URL.concatUrl("latest") + } + + var response = HttpUtil.getUrlContent(url, 5000) + if (response.isNullOrEmpty()) { + val httpPort = SettingsManager.getHttpPort() + response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response") + } + + val latestRelease = if (includePreRelease) { + JsonUtil.fromJson(response, Array::class.java) + .firstOrNull() + ?: throw IllegalStateException("No pre-release found") + } else { + JsonUtil.fromJson(response, GitHubRelease::class.java) + } + + val latestVersion = latestRelease.tagName.removePrefix("v") + Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})") + + return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) { + val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0]) + CheckUpdateResult( + hasUpdate = true, + latestVersion = latestVersion, + releaseNotes = latestRelease.body, + downloadUrl = downloadUrl, + isPreRelease = latestRelease.prerelease + ) + } else { + CheckUpdateResult(hasUpdate = false) + } + } + + suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) { + try { + val httpPort = SettingsManager.getHttpPort() + val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true) + ?: throw IllegalStateException("Failed to create connection") + + try { + val apkFile = File(context.cacheDir, "update.apk") + Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}") + + FileOutputStream(apkFile).use { outputStream -> + connection.inputStream.use { inputStream -> + inputStream.copyTo(outputStream) + } + } + Log.i(AppConfig.TAG, "APK download completed") + return@withContext apkFile + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}") + return@withContext null + } finally { + try { + connection.disconnect() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Error closing connection: ${e.message}") + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}") + return@withContext null + } + } + + private fun compareVersions(version1: String, version2: String): Int { + val v1 = version1.split(".") + val v2 = version2.split(".") + + for (i in 0 until maxOf(v1.size, v2.size)) { + val num1 = if (i < v1.size) v1[i].toInt() else 0 + val num2 = if (i < v2.size) v2[i].toInt() else 0 + if (num1 != num2) return num1 - num2 + } + return 0 + } + + private fun getDownloadUrl(release: GitHubRelease, abi: String): String { + return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl + ?: release.assets.firstOrNull()?.browserDownloadUrl + ?: throw IllegalStateException("No compatible APK found") + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt new file mode 100644 index 00000000..f53697bb --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt @@ -0,0 +1,1091 @@ +package com.v2ray.ang.handler + +import android.content.Context +import android.text.TextUtils +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.ConfigResult +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.NetworkType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.dto.V2rayConfig.OutboundBean +import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean +import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean +import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.fmt.HttpFmt +import com.v2ray.ang.fmt.ShadowsocksFmt +import com.v2ray.ang.fmt.SocksFmt +import com.v2ray.ang.fmt.TrojanFmt +import com.v2ray.ang.fmt.VlessFmt +import com.v2ray.ang.fmt.VmessFmt +import com.v2ray.ang.fmt.WireguardFmt +import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils + +object V2rayConfigManager { + private var initConfigCache: String? = null + + //region get config function + + /** + * Retrieves the V2ray configuration for the given GUID. + * + * @param context The context of the caller. + * @param guid The unique identifier for the V2ray configuration. + * @return A ConfigResult object containing the configuration details or indicating failure. + */ + fun getV2rayConfig(context: Context, guid: String): ConfigResult { + try { + val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) + return if (config.configType == EConfigType.CUSTOM) { + getV2rayCustomConfig(guid, config) + } else { + getV2rayNormalConfig(context, guid, config) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to get V2ray config", e) + return ConfigResult(false) + } + } + + /** + * Retrieves the speedtest V2ray configuration for the given GUID. + * + * @param context The context of the caller. + * @param guid The unique identifier for the V2ray configuration. + * @return A ConfigResult object containing the configuration details or indicating failure. + */ + fun getV2rayConfig4Speedtest(context: Context, guid: String): ConfigResult { + try { + val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) + return if (config.configType == EConfigType.CUSTOM) { + getV2rayCustomConfig(guid, config) + } else { + getV2rayNormalConfig4Speedtest(context, guid, config) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e) + return ConfigResult(false) + } + } + + /** + * Retrieves the custom V2ray configuration. + * + * @param guid The unique identifier for the V2ray configuration. + * @param config The profile item containing the configuration details. + * @return A ConfigResult object containing the result of the configuration retrieval. + */ + private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult { + val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false) + return ConfigResult(true, guid, raw) + } + + /** + * Retrieves the normal V2ray configuration. + * + * @param context The context in which the function is called. + * @param guid The unique identifier for the V2ray configuration. + * @param config The profile item containing the configuration details. + * @return A ConfigResult object containing the result of the configuration retrieval. + */ + private fun getV2rayNormalConfig(context: Context, guid: String, config: ProfileItem): ConfigResult { + val result = ConfigResult(false) + + val address = config.server ?: return result + if (!Utils.isPureIpAddress(address)) { + if (!Utils.isValidUrl(address)) { + Log.w(AppConfig.TAG, "$address is an invalid ip or domain") + return result + } + } + + val v2rayConfig = initV2rayConfig(context) ?: return result + v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" + v2rayConfig.remarks = config.remarks + + getInbounds(v2rayConfig) + + if (config.configType == EConfigType.HYSTERIA2) { + result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result + } else { + getOutbounds(v2rayConfig, config) ?: return result + getMoreOutbounds(v2rayConfig, config.subscriptionId) + } + + getRouting(v2rayConfig) + + getFakeDns(v2rayConfig) + + getDns(v2rayConfig) + + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { + getCustomLocalDns(v2rayConfig) + } + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) { + v2rayConfig.stats = null + v2rayConfig.policy = null + } + + //Resolve and add to DNS Hosts + if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") { + resolveOutboundDomainsToHosts(v2rayConfig) + } + + result.status = true + result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: "" + result.guid = guid + return result + } + + /** + * Retrieves the normal V2ray configuration for speedtest. + * + * @param context The context in which the function is called. + * @param guid The unique identifier for the V2ray configuration. + * @param config The profile item containing the configuration details. + * @return A ConfigResult object containing the result of the configuration retrieval. + */ + private fun getV2rayNormalConfig4Speedtest(context: Context, guid: String, config: ProfileItem): ConfigResult { + val result = ConfigResult(false) + + val address = config.server ?: return result + if (!Utils.isPureIpAddress(address)) { + if (!Utils.isValidUrl(address)) { + Log.w(AppConfig.TAG, "$address is an invalid ip or domain") + return result + } + } + + val v2rayConfig = initV2rayConfig(context) ?: return result + + if (config.configType == EConfigType.HYSTERIA2) { + result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result + } else { + getOutbounds(v2rayConfig, config) ?: return result + getMoreOutbounds(v2rayConfig, config.subscriptionId) + } + + v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning" + v2rayConfig.inbounds.clear() + v2rayConfig.routing.rules.clear() + v2rayConfig.dns = null + v2rayConfig.fakedns = null + v2rayConfig.stats = null + v2rayConfig.policy = null + + v2rayConfig.outbounds.forEach { key -> + key.mux = null + } + + result.status = true + result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: "" + result.guid = guid + return result + } + + /** + * Initializes V2ray configuration. + * + * This function loads the V2ray configuration from assets or from a cached value. + * It first attempts to use the cached configuration if available, otherwise reads + * the configuration from the "v2ray_config.json" asset file. + * + * @param context Android context used to access application assets + * @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty + */ + private fun initV2rayConfig(context: Context): V2rayConfig? { + val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json") + if (TextUtils.isEmpty(assets)) { + return null + } + initConfigCache = assets + val config = JsonUtil.fromJson(assets, V2rayConfig::class.java) + return config + } + + + //endregion + + + //region some sub function + + /** + * Configures the inbound settings for V2ray. + * + * This function sets up the listening ports, sniffing options, and other inbound-related configurations. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if inbound configuration was successful, false otherwise + */ + private fun getInbounds(v2rayConfig: V2rayConfig): Boolean { + try { + val socksPort = SettingsManager.getSocksPort() + + v2rayConfig.inbounds.forEach { curInbound -> + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) { + //bind all inbounds to localhost if the user requests + curInbound.listen = AppConfig.LOOPBACK + } + } + v2rayConfig.inbounds[0].port = socksPort + val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true + val sniffAllTlsAndHttp = + MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false + v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp + v2rayConfig.inbounds[0].sniffing?.routeOnly = + MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false) + if (!sniffAllTlsAndHttp) { + v2rayConfig.inbounds[0].sniffing?.destOverride?.clear() + } + if (fakedns) { + v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns") + } + + if (Utils.isXray()) { + v2rayConfig.inbounds.removeAt(1) + } else { + val httpPort = SettingsManager.getHttpPort() + v2rayConfig.inbounds[1].port = httpPort + } + + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure inbounds", e) + return false + } + return true + } + + /** + * Configures the fake DNS settings if enabled. + * + * Adds FakeDNS configuration to v2rayConfig if both local DNS and fake DNS are enabled. + * + * @param v2rayConfig The V2ray configuration object to be modified + */ + private fun getFakeDns(v2rayConfig: V2rayConfig) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true + && MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true + ) { + v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean()) + } + } + + /** + * Configures routing settings for V2ray. + * + * Sets up the domain strategy and adds routing rules from saved rulesets. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if routing configuration was successful, false otherwise + */ + private fun getRouting(v2rayConfig: V2rayConfig): Boolean { + try { + + v2rayConfig.routing.domainStrategy = + MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) + ?: "AsIs" + + val rulesetItems = MmkvManager.decodeRoutingRulesets() + rulesetItems?.forEach { key -> + getRoutingUserRule(key, v2rayConfig) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure routing", e) + return false + } + return true + } + + /** + * Adds a specific ruleset item to the routing configuration. + * + * @param item The ruleset item to add + * @param v2rayConfig The V2ray configuration object to be modified + */ + private fun getRoutingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) { + try { + if (item == null || !item.enabled) { + return + } + + val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return + + v2rayConfig.routing.rules.add(rule) + + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to apply routing user rule", e) + } + } + + /** + * Retrieves domain rules for a specific outbound tag. + * + * Searches through all rulesets to find domains targeting the specified tag. + * + * @param tag The outbound tag to search for + * @return ArrayList of domain rules matching the tag + */ + private fun getUserRule2Domain(tag: String): ArrayList { + val domain = ArrayList() + + val rulesetItems = MmkvManager.decodeRoutingRulesets() + rulesetItems?.forEach { key -> + if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) { + key.domain?.forEach { + if (it != AppConfig.GEOSITE_PRIVATE + && (it.startsWith("geosite:") || it.startsWith("domain:")) + ) { + domain.add(it) + } + } + } + } + + return domain + } + + /** + * Configures custom local DNS settings. + * + * Sets up DNS inbound, outbound, and routing rules for local DNS resolution. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if custom local DNS configuration was successful, false otherwise + */ + private fun getCustomLocalDns(v2rayConfig: V2rayConfig): Boolean { + try { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) { + val geositeCn = arrayListOf(AppConfig.GEOSITE_CN) + val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY) + val directDomain = getUserRule2Domain(AppConfig.TAG_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) + ) + ) + } + + // DNS inbound + val remoteDns = SettingsManager.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 AppConfig.DNS_PROXY, + port = 53, + network = "tcp,udp" + ) + + val localDnsPort = Utils.parseInt( + MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), + AppConfig.PORT_LOCAL_DNS.toInt() + ) + v2rayConfig.inbounds.add( + V2rayConfig.InboundBean( + tag = "dns-in", + port = localDnsPort, + listen = AppConfig.LOOPBACK, + 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 + ) + ) + } + + // DNS routing tag + v2rayConfig.routing.rules.add( + 0, RulesBean( + inboundTag = arrayListOf("dns-in"), + outboundTag = "dns-out", + domain = null + ) + ) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure custom local DNS", e) + return false + } + return true + } + + /** + * Configures the DNS settings for V2ray. + * + * Sets up DNS servers, hosts, and routing rules for DNS resolution. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if DNS configuration was successful, false otherwise + */ + private fun getDns(v2rayConfig: V2rayConfig): Boolean { + try { + val hosts = mutableMapOf() + val servers = ArrayList() + + //remote Dns + val remoteDns = SettingsManager.getRemoteDnsServers() + val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY) + remoteDns.forEach { + servers.add(it) + } + if (proxyDomain.isNotEmpty()) { + servers.add( + V2rayConfig.DnsBean.ServersBean( + address = remoteDns.first(), + domains = proxyDomain, + ) + ) + } + + // domestic DNS + val domesticDns = SettingsManager.getDomesticDnsServers() + val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT) + val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN) + val geoipCn = arrayListOf(AppConfig.GEOIP_CN) + if (directDomain.isNotEmpty()) { + servers.add( + V2rayConfig.DnsBean.ServersBean( + address = domesticDns.first(), + domains = directDomain, + expectIPs = if (isCnRoutingMode) geoipCn else null, + skipFallback = true + ) + ) + } + + if (Utils.isPureIpAddress(domesticDns.first())) { + v2rayConfig.routing.rules.add( + 0, RulesBean( + outboundTag = AppConfig.TAG_DIRECT, + port = "53", + ip = arrayListOf(domesticDns.first()), + domain = null + ) + ) + } + + //block dns + val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED) + if (blkDomain.isNotEmpty()) { + hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK }) + } + + // hardcode googleapi rule to fix play store problems + hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN + + // hardcode popular Android Private DNS rule to fix localhost DNS problem + hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES + hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES + hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES + hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES + hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES + hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES + + //User DNS hosts + try { + val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS) + if (userHosts.isNotNullEmpty()) { + var userHostsMap = userHosts?.split(",") + ?.filter { it.isNotEmpty() } + ?.filter { it.contains(":") } + ?.associate { it.split(":").let { (k, v) -> k to v } } + if (userHostsMap != null) hosts.putAll(userHostsMap) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e) + } + + // DNS dns + v2rayConfig.dns = V2rayConfig.DnsBean( + servers = servers, + hosts = hosts + ) + + // DNS routing + if (Utils.isPureIpAddress(remoteDns.first())) { + v2rayConfig.routing.rules.add( + 0, RulesBean( + outboundTag = AppConfig.TAG_PROXY, + port = "53", + ip = arrayListOf(remoteDns.first()), + domain = null + ) + ) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure DNS", e) + return false + } + return true + } + + + //endregion + + + //region outbound related functions + + /** + * Configures the primary outbound connection. + * + * Converts the profile to an outbound configuration and applies global settings. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @param config The profile item containing connection details + * @return true if outbound configuration was successful, null if there was an error + */ + private fun getOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Boolean? { + val outbound = convertProfile2Outbound(config) ?: return null + val ret = updateOutboundWithGlobalSettings(outbound) + if (!ret) return null + + if (v2rayConfig.outbounds.isNotEmpty()) { + v2rayConfig.outbounds[0] = outbound + } else { + v2rayConfig.outbounds.add(outbound) + } + + updateOutboundFragment(v2rayConfig) + return true + } + + /** + * Configures special outbound settings for Hysteria2 protocol. + * + * Creates a SOCKS outbound connection on a free port for protocols requiring special handling. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @param config The profile item containing connection details + * @return The port number for the SOCKS connection, or null if there was an error + */ + private fun getPlusOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Int? { + try { + val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0)) + + val outboundNew = OutboundBean( + mux = null, + protocol = EConfigType.SOCKS.name.lowercase(), + settings = OutSettingsBean( + servers = listOf( + OutSettingsBean.ServersBean( + address = AppConfig.LOOPBACK, + port = socksPort + ) + ) + ) + ) + if (v2rayConfig.outbounds.isNotEmpty()) { + v2rayConfig.outbounds[0] = outboundNew + } else { + v2rayConfig.outbounds.add(outboundNew) + } + + return socksPort + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure plusOutbound", e) + return null + } + } + + /** + * Configures additional outbound connections for proxy chaining. + * + * Sets up previous and next proxies in a subscription for advanced routing capabilities. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @param subscriptionId The subscription ID to look up related proxies + * @return true if additional outbounds were configured successfully, false otherwise + */ + private fun getMoreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String): Boolean { + //fragment proxy + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) { + return false + } + + if (subscriptionId.isEmpty()) { + return false + } + try { + val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return false + + //current proxy + val outbound = v2rayConfig.outbounds[0] + + //Previous proxy + val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile) + if (prevNode != null) { + val prevOutbound = convertProfile2Outbound(prevNode) + if (prevOutbound != null) { + updateOutboundWithGlobalSettings(prevOutbound) + prevOutbound.tag = AppConfig.TAG_PROXY + "2" + v2rayConfig.outbounds.add(prevOutbound) + outbound.ensureSockopt().dialerProxy = prevOutbound.tag + } + } + + //Next proxy + val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile) + if (nextNode != null) { + val nextOutbound = convertProfile2Outbound(nextNode) + if (nextOutbound != null) { + updateOutboundWithGlobalSettings(nextOutbound) + nextOutbound.tag = AppConfig.TAG_PROXY + v2rayConfig.outbounds.add(0, nextOutbound) + outbound.tag = AppConfig.TAG_PROXY + "1" + nextOutbound.ensureSockopt().dialerProxy = outbound.tag + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to configure more outbounds", e) + return false + } + + return true + } + + /** + * Updates outbound settings based on global preferences. + * + * Applies multiplexing and protocol-specific settings to an outbound connection. + * + * @param outbound The outbound connection to update + * @return true if the update was successful, false otherwise + */ + private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean { + try { + var muxEnabled = MmkvManager.decodeSettingsBool(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.HTTP.name, true) + || protocol.equals(EConfigType.TROJAN.name, true) + || protocol.equals(EConfigType.WIREGUARD.name, true) + || protocol.equals(EConfigType.HYSTERIA2.name, true) + ) { + muxEnabled = false + } else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) { + muxEnabled = false + } + + if (muxEnabled == true) { + outbound.mux?.enabled = true + outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt() + outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt() + outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC, "reject") + if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true) { + outbound.mux?.concurrency = -1 + } + } else { + outbound.mux?.enabled = false + outbound.mux?.concurrency = -1 + } + + if (protocol.equals(EConfigType.WIREGUARD.name, true)) { + var localTunAddr = if (outbound.settings?.address == null) { + listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4) + } else { + outbound.settings?.address as List<*> + } + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) != true) { + localTunAddr = listOf(localTunAddr.first()) + } + outbound.settings?.address = localTunAddr + } + + if (outbound.streamSettings?.network == AppConfig.DEFAULT_NETWORK + && outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.HEADER_TYPE_HTTP + ) { + val path = outbound.streamSettings?.tcpSettings?.header?.request?.path + val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host + + val requestString: String by lazy { + """{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""" + } + outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson( + requestString, + StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java + ) + outbound.streamSettings?.tcpSettings?.header?.request?.path = + if (path.isNullOrEmpty()) { + listOf("/") + } else { + path + } + outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host + } + + + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to update outbound with global settings", e) + return false + } + return true + } + + /** + * Updates the outbound with fragment settings for traffic optimization. + * + * Configures packet fragmentation for TLS and REALITY protocols if enabled. + * + * @param v2rayConfig The V2ray configuration object to be modified + * @return true if fragment configuration was successful, false otherwise + */ + private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean { + try { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) { + return true + } + if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS + && v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY + ) { + return true + } + + val fragmentOutbound = + V2rayConfig.OutboundBean( + protocol = AppConfig.PROTOCOL_FREEDOM, + tag = AppConfig.TAG_FRAGMENT, + mux = null + ) + + var packets = + MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello" + if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY + && packets == "tlshello" + ) { + packets = "1-3" + } else if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.TLS + && packets != "tlshello" + ) { + packets = "tlshello" + } + + fragmentOutbound.settings = OutboundBean.OutSettingsBean( + fragment = OutboundBean.OutSettingsBean.FragmentBean( + packets = packets, + length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH) + ?: "50-100", + interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL) + ?: "10-20" + ), + noises = listOf( + OutboundBean.OutSettingsBean.NoiseBean( + type = "rand", + packet = "10-20", + delay = "10-16", + ) + ), + ) + fragmentOutbound.streamSettings = StreamSettingsBean( + sockopt = StreamSettingsBean.SockoptBean( + TcpNoDelay = true, + mark = 255 + ) + ) + v2rayConfig.outbounds.add(fragmentOutbound) + + //proxy chain + v2rayConfig.outbounds[0].streamSettings?.sockopt = + StreamSettingsBean.SockoptBean( + dialerProxy = AppConfig.TAG_FRAGMENT + ) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to update outbound fragment", e) + return false + } + return true + } + + /** + * Resolves domain names to IP addresses in outbound connections. + * + * Pre-resolves domains to improve connection speed and reliability. + * + * @param v2rayConfig The V2ray configuration object to be modified + */ + private fun resolveOutboundDomainsToHosts(v2rayConfig: V2rayConfig) { + val proxyOutboundList = v2rayConfig.getAllProxyOutbound() + val dns = v2rayConfig.dns ?: return + val newHosts = dns.hosts?.toMutableMap() ?: mutableMapOf() + val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true + + for (item in proxyOutboundList) { + val domain = item.getServerAddress() + if (domain.isNullOrEmpty()) continue + + if (newHosts.containsKey(domain)) { + item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6" + continue + } + + val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6) + if (resolvedIps.isNullOrEmpty()) continue + + item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6" + newHosts[domain] = if (resolvedIps.size == 1) { + resolvedIps[0] + } else { + resolvedIps + } + } + + dns.hosts = newHosts + } + + /** + * Converts a profile item to an outbound configuration. + * + * Creates appropriate outbound settings based on the protocol type. + * + * @param profileItem The profile item to convert + * @return OutboundBean configuration for the profile, or null if not supported + */ + private fun convertProfile2Outbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? { + return when (profileItem.configType) { + EConfigType.VMESS -> VmessFmt.toOutbound(profileItem) + EConfigType.CUSTOM -> null + EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toOutbound(profileItem) + EConfigType.SOCKS -> SocksFmt.toOutbound(profileItem) + EConfigType.VLESS -> VlessFmt.toOutbound(profileItem) + EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem) + EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem) + EConfigType.HYSTERIA2 -> null + EConfigType.HTTP -> HttpFmt.toOutbound(profileItem) + } + } + + /** + * Creates an initial outbound configuration for a specific protocol type. + * + * Provides a template configuration for different protocol types. + * + * @param configType The type of configuration to create + * @return An initial OutboundBean for the specified configuration type, or null for custom types + */ + fun createInitOutbound(configType: EConfigType): OutboundBean? { + return when (configType) { + EConfigType.VMESS, + EConfigType.VLESS -> + return OutboundBean( + protocol = configType.name.lowercase(), + settings = OutSettingsBean( + vnext = listOf( + OutSettingsBean.VnextBean( + users = listOf(OutSettingsBean.VnextBean.UsersBean()) + ) + ) + ), + streamSettings = StreamSettingsBean() + ) + + EConfigType.SHADOWSOCKS, + EConfigType.SOCKS, + EConfigType.HTTP, + EConfigType.TROJAN, + EConfigType.HYSTERIA2 -> + return OutboundBean( + protocol = configType.name.lowercase(), + settings = OutSettingsBean( + servers = listOf(OutSettingsBean.ServersBean()) + ), + streamSettings = StreamSettingsBean() + ) + + EConfigType.WIREGUARD -> + return OutboundBean( + protocol = configType.name.lowercase(), + settings = OutSettingsBean( + secretKey = "", + peers = listOf(OutSettingsBean.WireGuardBean()) + ) + ) + + EConfigType.CUSTOM -> null + } + } + + /** + * Configures transport settings for an outbound connection. + * + * Sets up protocol-specific transport options based on the profile settings. + * + * @param streamSettings The stream settings to configure + * @param profileItem The profile containing transport configuration + * @return The Server Name Indication (SNI) value to use, or null if not applicable + */ + fun populateTransportSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem): String? { + val transport = profileItem.network.orEmpty() + val headerType = profileItem.headerType + val host = profileItem.host + val path = profileItem.path + val seed = profileItem.seed +// val quicSecurity = profileItem.quicSecurity +// val key = profileItem.quicKey + val mode = profileItem.mode + val serviceName = profileItem.serviceName + val authority = profileItem.authority + val xhttpMode = profileItem.xhttpMode + val xhttpExtra = profileItem.xhttpExtra + + var sni: String? = null + streamSettings.network = if (transport.isEmpty()) NetworkType.TCP.type else transport + when (streamSettings.network) { + NetworkType.TCP.type -> { + val tcpSetting = StreamSettingsBean.TcpSettingsBean() + if (headerType == AppConfig.HEADER_TYPE_HTTP) { + tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP + if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { + val requestObj = StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean() + requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } + requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } + tcpSetting.header.request = requestObj + sni = requestObj.headers.Host?.getOrNull(0) + } + } else { + tcpSetting.header.type = "none" + sni = host + } + streamSettings.tcpSettings = tcpSetting + } + + NetworkType.KCP.type -> { + val kcpsetting = StreamSettingsBean.KcpSettingsBean() + kcpsetting.header.type = headerType ?: "none" + if (seed.isNullOrEmpty()) { + kcpsetting.seed = null + } else { + kcpsetting.seed = seed + } + if (host.isNullOrEmpty()) { + kcpsetting.header.domain = null + } else { + kcpsetting.header.domain = host + } + streamSettings.kcpSettings = kcpsetting + } + + NetworkType.WS.type -> { + val wssetting = StreamSettingsBean.WsSettingsBean() + wssetting.headers.Host = host.orEmpty() + sni = host + wssetting.path = path ?: "/" + streamSettings.wsSettings = wssetting + } + + NetworkType.HTTP_UPGRADE.type -> { + val httpupgradeSetting = StreamSettingsBean.HttpupgradeSettingsBean() + httpupgradeSetting.host = host.orEmpty() + sni = host + httpupgradeSetting.path = path ?: "/" + streamSettings.httpupgradeSettings = httpupgradeSetting + } + + NetworkType.XHTTP.type -> { + val xhttpSetting = StreamSettingsBean.XhttpSettingsBean() + xhttpSetting.host = host.orEmpty() + sni = host + xhttpSetting.path = path ?: "/" + xhttpSetting.mode = xhttpMode + xhttpSetting.extra = JsonUtil.parseString(xhttpExtra) + streamSettings.xhttpSettings = xhttpSetting + } + + NetworkType.H2.type, NetworkType.HTTP.type -> { + streamSettings.network = NetworkType.H2.type + val h2Setting = StreamSettingsBean.HttpSettingsBean() + h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } + sni = h2Setting.host.getOrNull(0) + h2Setting.path = path ?: "/" + streamSettings.httpSettings = h2Setting + } + +// "quic" -> { +// val quicsetting = QuicSettingBean() +// quicsetting.security = quicSecurity ?: "none" +// quicsetting.key = key.orEmpty() +// quicsetting.header.type = headerType ?: "none" +// quicSettings = quicsetting +// } + + NetworkType.GRPC.type -> { + val grpcSetting = StreamSettingsBean.GrpcSettingsBean() + grpcSetting.multiMode = mode == "multi" + grpcSetting.serviceName = serviceName.orEmpty() + grpcSetting.authority = authority.orEmpty() + grpcSetting.idle_timeout = 60 + grpcSetting.health_check_timeout = 20 + sni = authority + streamSettings.grpcSettings = grpcSetting + } + } + return sni + } + + /** + * Configures TLS or REALITY security settings for an outbound connection. + * + * Sets up security-related parameters like certificates, fingerprints, and SNI. + * + * @param streamSettings The stream settings to configure + * @param profileItem The profile containing security configuration + * @param sniExt An external SNI value to use if the profile doesn't specify one + */ + fun populateTlsSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) { + val streamSecurity = profileItem.security.orEmpty() + val allowInsecure = profileItem.insecure == true + val sni = if (profileItem.sni.isNullOrEmpty()) { + when { + sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt + profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server + else -> sniExt + } + } else { + profileItem.sni + } + val fingerprint = profileItem.fingerPrint + val alpns = profileItem.alpn + val publicKey = profileItem.publicKey + val shortId = profileItem.shortId + val spiderX = profileItem.spiderX + + streamSettings.security = if (streamSecurity.isEmpty()) null else streamSecurity + if (streamSettings.security == null) return + val tlsSetting = StreamSettingsBean.TlsSettingsBean( + allowInsecure = allowInsecure, + serverName = if (sni.isNullOrEmpty()) null else sni, + fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint, + alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }, + publicKey = if (publicKey.isNullOrEmpty()) null else publicKey, + shortId = if (shortId.isNullOrEmpty()) null else shortId, + spiderX = if (spiderX.isNullOrEmpty()) null else spiderX, + ) + if (streamSettings.security == AppConfig.TLS) { + streamSettings.tlsSettings = tlsSetting + streamSettings.realitySettings = null + } else if (streamSettings.security == AppConfig.REALITY) { + streamSettings.tlsSettings = null + streamSettings.realitySettings = tlsSetting + } + } + + //endregion +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt new file mode 100644 index 00000000..0f6d37c0 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/CustomDividerItemDecoration.kt @@ -0,0 +1,68 @@ +package com.v2ray.ang.helper + +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class CustomDividerItemDecoration( + private val divider: Drawable, + private val orientation: Int +) : RecyclerView.ItemDecoration() { + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (orientation == RecyclerView.VERTICAL) { + drawVerticalDividers(canvas, parent) + } else { + drawHorizontalDividers(canvas, parent) + } + } + + private fun drawVerticalDividers(canvas: Canvas, parent: RecyclerView) { + val left = parent.paddingLeft + val right = parent.width - parent.paddingRight + + val childCount = parent.childCount + for (i in 0 until childCount - 1) { + val child = parent.getChildAt(i) + val params = child.layoutParams as RecyclerView.LayoutParams + + val top = child.bottom + params.bottomMargin + val bottom = top + divider.intrinsicHeight + + divider.setBounds(left, top, right, bottom) + divider.draw(canvas) + } + } + + private fun drawHorizontalDividers(canvas: Canvas, parent: RecyclerView) { + val top = parent.paddingTop + val bottom = parent.height - parent.paddingBottom + + val childCount = parent.childCount + for (i in 0 until childCount - 1) { + val child = parent.getChildAt(i) + val params = child.layoutParams as RecyclerView.LayoutParams + + val left = child.right + params.rightMargin + val right = left + divider.intrinsicWidth + + divider.setBounds(left, top, right, bottom) + divider.draw(canvas) + } + } + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + if (orientation == RecyclerView.VERTICAL) { + outRect.set(0, 0, 0, divider.intrinsicHeight) + } else { + outRect.set(0, 0, divider.intrinsicWidth, 0) + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt similarity index 53% rename from V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java rename to V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt index 2de0c74a..8707d16f 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.java +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperAdapter.kt @@ -13,48 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package com.v2ray.ang.helper; - -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.ItemTouchHelper; +package com.v2ray.ang.helper /** - * Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}. + * Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback]. * * @author Paul Burke (ipaulpro) */ -public interface ItemTouchHelperAdapter { - +interface ItemTouchHelperAdapter { /** * Called when an item has been dragged far enough to trigger a move. This is called every time - * an item is shifted, and not at the end of a "drop" event.
- *
- * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after + * an item is shifted, and **not** at the end of a "drop" event.

+ *

+ * Implementations should call [RecyclerView.Adapter.notifyItemMoved] after * adjusting the underlying data to reflect this move. * * @param fromPosition The start position of the moved item. * @param toPosition Then resolved position of the moved item. * @return True if the item was moved to the new adapter position. - * - * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) - * @see RecyclerView.ViewHolder#getAdapterPosition() + * @see RecyclerView.getAdapterPositionFor + * @see RecyclerView.ViewHolder.getAdapterPosition */ - boolean onItemMove(int fromPosition, int toPosition); + fun onItemMove(fromPosition: Int, toPosition: Int): Boolean - void onItemMoveCompleted(); + fun onItemMoveCompleted() /** - * Called when an item has been dismissed by a swipe.
- *
- * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after + * Called when an item has been dismissed by a swipe.

+ *

+ * Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after * adjusting the underlying data to reflect this removal. * * @param position The position of the item dismissed. - * - * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) - * @see RecyclerView.ViewHolder#getAdapterPosition() + * @see RecyclerView.getAdapterPositionFor + * @see RecyclerView.ViewHolder.getAdapterPosition */ - void onItemDismiss(int position); + fun onItemDismiss(position: Int) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt similarity index 67% rename from V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java rename to V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt index 149768fc..75655b0c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.java +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/ItemTouchHelperViewHolder.kt @@ -13,29 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.v2ray.ang.helper -package com.v2ray.ang.helper; - -import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.ItemTouchHelper /** - * Interface to notify an item ViewHolder of relevant callbacks from {@link - * ItemTouchHelper.Callback}. + * Interface to notify an item ViewHolder of relevant callbacks from [ ]. * * @author Paul Burke (ipaulpro) */ -public interface ItemTouchHelperViewHolder { - +interface ItemTouchHelperViewHolder { /** - * Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped. + * Called when the [ItemTouchHelper] first registers an item as being moved or swiped. * Implementations should update the item view to indicate it's active state. */ - void onItemSelected(); + fun onItemSelected() /** - * Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item + * Called when the [ItemTouchHelper] has completed the move or swipe, and the active item * state should be cleared. */ - void onItemClear(); + fun onItemClear() } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java deleted file mode 100644 index a6407b6f..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/OnStartDragListener.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2015 Paul Burke - * - * 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.v2ray.ang.helper; - -import androidx.recyclerview.widget.RecyclerView; - -/** - * Listener for manual initiation of a drag. - */ -public interface OnStartDragListener { - - /** - * Called when a view is requesting a start of a drag. - * - * @param viewHolder The holder of the view to drag. - */ - void onStartDrag(RecyclerView.ViewHolder viewHolder); - -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java deleted file mode 100644 index 97fceab7..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2015 Paul Burke - * - * 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.v2ray.ang.helper; - -import android.graphics.Canvas; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.ItemTouchHelper; - -import org.jetbrains.annotations.NotNull; - -/** - * An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and - * swipe-to-dismiss. Drag events are automatically started by an item long-press.
- *
- * Expects the RecyclerView.Adapter to listen for {@link - * ItemTouchHelperAdapter} callbacks and the RecyclerView.ViewHolder to implement - * {@link ItemTouchHelperViewHolder}. - * - * @author Paul Burke (ipaulpro) - */ -public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { - - public static final float ALPHA_FULL = 1.0f; - - private final ItemTouchHelperAdapter mAdapter; - - public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) { - mAdapter = adapter; - } - - @Override - public boolean isLongPressDragEnabled() { - return true; - } - - @Override - public boolean isItemViewSwipeEnabled() { - return false; - } - - @Override - public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) { - // Set movement flags based on the layout manager - if (recyclerView.getLayoutManager() instanceof GridLayoutManager) { - final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; - final int swipeFlags = 0; - return makeMovementFlags(dragFlags, swipeFlags); - } else { - final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; - final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; - return makeMovementFlags(dragFlags, swipeFlags); - } - } - - @Override - public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType()) { - return false; - } - - // Notify the adapter of the move - mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition()); - return true; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { - // Notify the adapter of the dismissal - mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition()); - } - - @Override - public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX, - float dY, int actionState, boolean isCurrentlyActive) { - if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { - // Fade out the view as it is swiped out of the parent's bounds - final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); - viewHolder.itemView.setAlpha(alpha); - viewHolder.itemView.setTranslationX(dX); - } else { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - } - } - - @Override - public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { - // We only want the active item to change - if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { - if (viewHolder instanceof ItemTouchHelperViewHolder) { - // Let the view holder know that this item is being moved or dragged - ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; - itemViewHolder.onItemSelected(); - } - } - - super.onSelectedChanged(viewHolder, actionState); - } - - @Override - public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - - mAdapter.onItemMoveCompleted(); - - viewHolder.itemView.setAlpha(ALPHA_FULL); - - if (viewHolder instanceof ItemTouchHelperViewHolder) { - // Tell the view holder it's time to restore the idle state - ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; - itemViewHolder.onItemClear(); - } - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt new file mode 100644 index 00000000..b98129a7 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2015 Paul Burke + * + * 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.v2ray.ang.helper + +import android.animation.ValueAnimator +import android.graphics.Canvas +import android.view.animation.DecelerateInterpolator +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.sign + +/** + * An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and + * swipe-to-dismiss. Drag events are automatically started by an item long-press.

+ * + * Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement + * [ItemTouchHelperViewHolder]. + * + * @author Paul Burke (ipaulpro) + */ +class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() { + private var mReturnAnimator: ValueAnimator? = null + + override fun isLongPressDragEnabled(): Boolean = true + + override fun isItemViewSwipeEnabled(): Boolean = true + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + val dragFlags: Int + val swipeFlags: Int + if (recyclerView.layoutManager is GridLayoutManager) { + dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END + } else { + dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN + swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END + } + return makeMovementFlags(dragFlags, swipeFlags) + } + + override fun onMove( + recyclerView: RecyclerView, + source: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + return if (source.itemViewType != target.itemViewType) { + false + } else { + mAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition) + true + } + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + // Do not delete; simply return item to original position + returnViewToOriginalPosition(viewHolder) + } + + override fun onChildDraw( + c: Canvas, recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean + ) { + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + val maxSwipeDistance = viewHolder.itemView.width * SWIPE_THRESHOLD + val swipeAmount = abs(dX) + val direction = sign(dX) + + // Limit maximum swipe distance + val translationX = min(swipeAmount, maxSwipeDistance) * direction + val alpha = ALPHA_FULL - min(swipeAmount, maxSwipeDistance) / maxSwipeDistance + + viewHolder.itemView.translationX = translationX + viewHolder.itemView.alpha = alpha + + if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) { + returnViewToOriginalPosition(viewHolder) + } + } else { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + } + + private fun returnViewToOriginalPosition(viewHolder: RecyclerView.ViewHolder) { + mReturnAnimator?.takeIf { it.isRunning }?.cancel() + + mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.translationX, 0f).apply { + addUpdateListener { animation -> + val value = animation.animatedValue as Float + viewHolder.itemView.translationX = value + viewHolder.itemView.alpha = 1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD) + } + interpolator = DecelerateInterpolator() + duration = ANIMATION_DURATION + start() + } + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is ItemTouchHelperViewHolder) { + viewHolder.onItemSelected() + } + super.onSelectedChanged(viewHolder, actionState) + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.alpha = ALPHA_FULL + if (viewHolder is ItemTouchHelperViewHolder) { + viewHolder.onItemClear() + } + mAdapter.onItemMoveCompleted() + } + + override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { + return 1.1f // Set a value greater than 1 to prevent default swipe delete + } + + override fun getSwipeEscapeVelocity(defaultValue: Float): Float { + return defaultValue * 10 // Increase swipe escape velocity to make swipe harder to trigger + } + + companion object { + private const val ALPHA_FULL = 1.0f + private const val SWIPE_THRESHOLD = 0.25f + private const val ANIMATION_DURATION: Long = 200 + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt new file mode 100644 index 00000000..7a072cd0 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/NativePlugin.kt @@ -0,0 +1,32 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.content.pm.ResolveInfo + +class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) { + init { + check(resolveInfo.providerInfo != null) + } + + override val componentInfo get() = resolveInfo.providerInfo!! +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt new file mode 100644 index 00000000..04294ac6 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt @@ -0,0 +1,43 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.graphics.drawable.Drawable + +abstract class Plugin { + abstract val id: String + abstract val label: CharSequence + abstract val version: Int + abstract val versionName: String + open val icon: Drawable? get() = null + open val defaultConfig: String? get() = null + open val packageName: String get() = "" + open val directBootAware: Boolean get() = true + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return id == (other as Plugin).id + } + + override fun hashCode() = id.hashCode() +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt new file mode 100644 index 00000000..5aa253f8 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginContract.kt @@ -0,0 +1,33 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +object PluginContract { + + const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN" + const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY" + const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id" + const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path" + const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable" + + const val COLUMN_PATH = "path" + const val COLUMN_MODE = "mode" + const val SCHEME = "plugin" +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt new file mode 100644 index 00000000..2495eb45 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt @@ -0,0 +1,54 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.content.Intent +import android.content.pm.PackageManager +import com.v2ray.ang.AngApplication + +class PluginList : ArrayList() { + init { + addAll( + AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA + ) + .filter { it.providerInfo.exported }.map { NativePlugin(it) }) + } + + val lookup = mutableMapOf().apply { + for (plugin in this@PluginList.toList()) { + fun check(old: Plugin?) { + if (old != null && old != plugin) { + this@PluginList.remove(old) + } + /* if (old != null && old !== plugin) { + val packages = this@PluginList.filter { it.id == plugin.id } + .joinToString { it.packageName } + val message = "Conflicting plugins found from: $packages" + Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show() + throw IllegalStateException(message) + }*/ + } + check(put(plugin.id, plugin)) + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt new file mode 100644 index 00000000..e7e8457a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt @@ -0,0 +1,233 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.ContentResolver +import android.content.Intent +import android.content.pm.ComponentInfo +import android.content.pm.PackageManager +import android.content.pm.ProviderInfo +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.system.Os +import androidx.core.os.bundleOf +import com.v2ray.ang.AngApplication +import com.v2ray.ang.extension.listenForPackageChanges +import com.v2ray.ang.extension.toast +import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID +import java.io.File +import java.io.FileNotFoundException + +object PluginManager { + + class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin) + + private var receiver: BroadcastReceiver? = null + private var cachedPlugins: PluginList? = null + fun fetchPlugins() = synchronized(this) { + if (receiver == null) receiver = AngApplication.application.listenForPackageChanges { + synchronized(this) { + receiver = null + cachedPlugins = null + } + } + if (cachedPlugins == null) cachedPlugins = PluginList() + cachedPlugins!! + } + + private fun buildUri(id: String, authority: String) = Uri.Builder() + .scheme(PluginContract.SCHEME) + .authority(authority) + .path("/$id") + .build() + + data class InitResult( + val path: String, + ) + + @Throws(Throwable::class) + fun init(pluginId: String): InitResult? { + if (pluginId.isEmpty()) return null + var throwable: Throwable? = null + + try { + val result = initNative(pluginId) + if (result != null) return result + } catch (t: Throwable) { + if (throwable == null) throwable = t //Logs.w(t) + } + + throw throwable ?: PluginNotFoundException(pluginId) + } + + private fun initNative(pluginId: String): InitResult? { + var flags = PackageManager.GET_META_DATA + if (Build.VERSION.SDK_INT >= 24) { + flags = + flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE + } + var providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags + ) + .filter { it.providerInfo.exported } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags + ) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags + ) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags + ) + .filter { it.providerInfo.exported } + } + if (providers.isEmpty()) { + providers = AngApplication.application.packageManager.queryIntentContentProviders( + Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA + ).filter { + it.providerInfo.exported && + it.providerInfo.metaData.containsKey(METADATA_KEY_ID) && + it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId + } + if (providers.size > 1) { + providers = listOf(providers[0]) // What if there is more than one? + } + } + if (providers.isEmpty()) return null + if (providers.size > 1) { + val message = + "Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}" + AngApplication.application.toast(message) + throw IllegalStateException(message) + } + val provider = providers.single().providerInfo + var failure: Throwable? = null + try { + initNativeFaster(provider)?.also { return InitResult(it) } + } catch (t: Throwable) { + // Logs.w("Initializing native plugin faster mode failed") + failure = t + } + + val uri = Uri.Builder().apply { + scheme(ContentResolver.SCHEME_CONTENT) + authority(provider.authority) + }.build() + try { + return initNativeFast( + AngApplication.application.contentResolver, + pluginId, + uri + )?.let { InitResult(it) } + } catch (t: Throwable) { + // Logs.w("Initializing native plugin fast mode failed") + failure?.also { t.addSuppressed(it) } + failure = t + } + + try { + return initNativeSlow( + AngApplication.application.contentResolver, + pluginId, + uri + )?.let { InitResult(it) } + } catch (t: Throwable) { + failure?.also { t.addSuppressed(it) } + throw t + } + } + + private fun initNativeFaster(provider: ProviderInfo): String? { + return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH) + ?.let { relativePath -> + File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply { + check(canExecute()) + }.absolutePath + } + } + + private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? { + return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf()) + ?.getString(PluginContract.EXTRA_ENTRY)?.also { + check(File(it).canExecute()) + } + } + + @SuppressLint("Recycle") + private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? { + var initialized = false + fun entryNotFound(): Nothing = + throw IndexOutOfBoundsException("Plugin entry binary not found") + + val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin") + (cr.query( + uri, + arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), + null, + null, + null + ) + ?: return null).use { cursor -> + if (!cursor.moveToFirst()) entryNotFound() + pluginDir.deleteRecursively() + if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory") + val pluginDirPath = pluginDir.absolutePath + '/' + do { + val path = cursor.getString(0) + val file = File(pluginDir, path) + check(file.absolutePath.startsWith(pluginDirPath)) + cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream -> + file.outputStream().use { outStream -> inStream.copyTo(outStream) } + } + Os.chmod( + file.absolutePath, when (cursor.getType(1)) { + Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1) + Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8) + else -> throw IllegalArgumentException("File mode should be of type int") + } + ) + if (path == pluginId) initialized = true + } while (cursor.moveToNext()) + } + if (!initialized) entryNotFound() + return File(pluginDir, pluginId).absolutePath + } + + fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) { + is String -> value +// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo) +// .getString(value) + + null -> null + else -> error("meta-data $key has invalid type ${value.javaClass}") + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt new file mode 100644 index 00000000..6e6861cf --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/plugin/ResolvedPlugin.kt @@ -0,0 +1,51 @@ +/****************************************************************************** + * * + * Copyright (C) 2021 by nekohasekai * + * Copyright (C) 2021 by Max Lv * + * Copyright (C) 2021 by Mygod Studio * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + * * + ******************************************************************************/ + +package com.v2ray.ang.plugin + +import android.content.pm.ComponentInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.graphics.drawable.Drawable +import android.os.Build +import com.v2ray.ang.AngApplication +import com.v2ray.ang.plugin.PluginManager.loadString + +abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() { + protected abstract val componentInfo: ComponentInfo + + override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! } + override val version by lazy { + getPackageInfo(componentInfo.packageName).versionCode + } + override val versionName: String by lazy { + getPackageInfo(componentInfo.packageName).versionName!! + } + override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager) + override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager) + override val packageName: String get() = componentInfo.packageName + override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware + + fun getPackageInfo(packageName: String) = AngApplication.application.packageManager.getPackageInfo( + packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES + else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES + )!! +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt new file mode 100644 index 00000000..ae15e76c --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/BootReceiver.kt @@ -0,0 +1,23 @@ +package com.v2ray.ang.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.service.V2RayServiceManager + +class BootReceiver : BroadcastReceiver() { + /** + * This method is called when the BroadcastReceiver is receiving an Intent broadcast. + * It checks if the context is not null and the action is ACTION_BOOT_COMPLETED. + * If the conditions are met, it starts the V2Ray service. + * + * @param context The Context in which the receiver is running. + * @param intent The Intent being received. + */ + override fun onReceive(context: Context?, intent: Intent?) { + if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return + if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return + V2RayServiceManager.startVService(context) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt similarity index 50% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt index 08a3184a..bdb7bf9a 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt @@ -4,38 +4,38 @@ 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 -import com.v2ray.ang.util.MmkvManager - -import com.v2ray.ang.util.Utils class TaskerReceiver : BroadcastReceiver() { - private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) } + /** + * This method is called when the BroadcastReceiver is receiving an Intent broadcast. + * It retrieves the bundle from the intent and checks the switch and guid values. + * Depending on the switch value, it starts or stops the V2Ray service. + * + * @param context The Context in which the receiver is running. + * @param intent The Intent being received. + */ override fun onReceive(context: Context, intent: Intent?) { - try { val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE) val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false) - val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "") + val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty() - if (switch == null || guid == null || TextUtils.isEmpty(guid)) { + if (switch == null || TextUtils.isEmpty(guid)) { return } else if (switch) { if (guid == AppConfig.TASKER_DEFAULT_GUID) { - Utils.startVServiceFromToggle(context) + V2RayServiceManager.startVServiceFromToggle(context) } else { - mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid) - V2RayServiceManager.startV2Ray(context) + V2RayServiceManager.startVService(context, guid) } } else { - Utils.stopVService(context) + V2RayServiceManager.stopVService(context) } - } catch (e: WriterException) { - e.printStackTrace() + } catch (e: Exception) { + android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e) } } } diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt similarity index 52% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt index 826bf24f..99d9bda2 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt @@ -8,21 +8,32 @@ 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 class WidgetProvider : AppWidgetProvider() { /** - * 每次窗口小部件被更新都调用一次该方法 + * This method is called every time the widget is updated. + * It updates the widget background based on the V2Ray service running state. + * + * @param context The Context in which the receiver is running. + * @param appWidgetManager The AppWidgetManager instance. + * @param appWidgetIds The appWidgetIds for which an update is needed. */ override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds) - updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning) + updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.isRunning()) } - + /** + * Updates the widget background based on whether the V2Ray service is running. + * + * @param context The Context in which the receiver is running. + * @param appWidgetManager The AppWidgetManager instance. + * @param appWidgetIds The appWidgetIds for which an update is needed. + * @param isRunning Boolean indicating if the V2Ray service is running. + */ private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) { val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch) val intent = Intent(context, WidgetProvider::class.java) @@ -35,20 +46,15 @@ class WidgetProvider : AppWidgetProvider() { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { PendingIntent.FLAG_UPDATE_CURRENT - }) + } + ) 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) { @@ -57,26 +63,35 @@ class WidgetProvider : AppWidgetProvider() { } /** - * 接收窗口小部件发送的广播 + * This method is called when the BroadcastReceiver is receiving an Intent broadcast. + * It handles widget click actions and updates the widget background based on the V2Ray service state. + * + * @param context The Context in which the receiver is running. + * @param intent The Intent being received. */ override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) { - if (V2RayServiceManager.v2rayPoint.isRunning) { - Utils.stopVService(context) + if (V2RayServiceManager.isRunning()) { + V2RayServiceManager.stopVService(context) } else { - Utils.startVServiceFromToggle(context) + V2RayServiceManager.startVServiceFromToggle(context) } } else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) { AppWidgetManager.getInstance(context)?.let { manager -> when (intent.getIntExtra("key", 0)) { AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> { - updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), - true) + updateWidgetBackground( + context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), + true + ) } + AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> { - updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), - false) + updateWidgetBackground( + context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)), + false + ) } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt new file mode 100644 index 00000000..92c551a6 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt @@ -0,0 +1,252 @@ +package com.v2ray.ang.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.AppConfig.TAG_DIRECT +import com.v2ray.ang.R +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.extension.toSpeedString +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.ui.MainActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.math.min + +object NotificationService { + private const val NOTIFICATION_ID = 1 + private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0 + private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1 + private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2 + private const val NOTIFICATION_ICON_THRESHOLD = 3000 + + private var lastQueryTime = 0L + private var mBuilder: NotificationCompat.Builder? = null + private var speedNotificationJob: Job? = null + private var mNotificationManager: NotificationManager? = null + + /** + * Starts the speed notification. + * @param currentConfig The current profile configuration. + */ + fun startSpeedNotification(currentConfig: ProfileItem?) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return + if (speedNotificationJob != null || V2RayServiceManager.isRunning() == false) return + + lastQueryTime = System.currentTimeMillis() + var lastZeroSpeed = false + val outboundTags = currentConfig?.getAllOutboundTags() + outboundTags?.remove(TAG_DIRECT) + + speedNotificationJob = CoroutineScope(Dispatchers.IO).launch { + while (isActive) { + val queryTime = System.currentTimeMillis() + val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0 + var proxyTotal = 0L + val text = StringBuilder() + outboundTags?.forEach { + val up = V2RayServiceManager.queryStats(it, AppConfig.UPLINK) + val down = V2RayServiceManager.queryStats(it, AppConfig.DOWNLINK) + if (up + down > 0) { + appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds) + proxyTotal += up + down + } + } + val directUplink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.UPLINK) + val directDownlink = V2RayServiceManager.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) + } + appendSpeedString( + text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds, + directDownlink / sinceLastQueryInSeconds + ) + updateNotification(text.toString(), proxyTotal, directDownlink + directUplink) + } + lastZeroSpeed = zeroSpeed + lastQueryTime = queryTime + delay(3000) + } + } + } + + /** + * Shows the notification. + * @param currentConfig The current profile configuration. + */ + fun showNotification(currentConfig: ProfileItem?) { + val service = getService() ?: return + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + val startMainIntent = Intent(service, MainActivity::class.java) + val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags) + + val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) + stopV2RayIntent.`package` = ANG_PACKAGE + stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP) + val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags) + + val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) + restartV2RayIntent.`package` = ANG_PACKAGE + restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART) + val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags) + + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel() + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + mBuilder = NotificationCompat.Builder(service, channelId) + .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_delete_24dp, + service.getString(R.string.notification_action_stop_v2ray), + stopV2RayPendingIntent + ) + .addAction( + R.drawable.ic_delete_24dp, + service.getString(R.string.title_service_restart), + restartV2RayPendingIntent + ) + + //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) + + service.startForeground(NOTIFICATION_ID, mBuilder?.build()) + } + + /** + * Cancels the notification. + */ + fun cancelNotification() { + val service = getService() ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + service.stopForeground(Service.STOP_FOREGROUND_REMOVE) + } else { + service.stopForeground(true) + } + + mBuilder = null + speedNotificationJob?.cancel() + speedNotificationJob = null + mNotificationManager = null + } + + /** + * Stops the speed notification. + * @param currentConfig The current profile configuration. + */ + fun stopSpeedNotification(currentConfig: ProfileItem?) { + speedNotificationJob?.let { + it.cancel() + speedNotificationJob = null + updateNotification(currentConfig?.remarks, 0, 0) + } + } + + /** + * Creates a notification channel for Android O and above. + * @return The channel ID. + */ + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(): String { + 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 + chan.importance = NotificationManager.IMPORTANCE_NONE + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + getNotificationManager()?.createNotificationChannel(chan) + return channelId + } + + /** + * Updates the notification with the given content text and traffic data. + * @param contentText The content text. + * @param proxyTraffic The proxy traffic. + * @param directTraffic The direct traffic. + */ + 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_stat_name) + } else if (proxyTraffic > directTraffic) { + mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy) + } else { + mBuilder?.setSmallIcon(R.drawable.ic_stat_direct) + } + mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) + mBuilder?.setContentText(contentText) + getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build()) + } + } + + /** + * Gets the notification manager. + * @return The notification manager. + */ + private fun getNotificationManager(): NotificationManager? { + if (mNotificationManager == null) { + val service = getService() ?: return null + mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + return mNotificationManager + } + + /** + * Appends the speed string to the given text. + * @param text The text to append to. + * @param name The name of the tag. + * @param up The uplink speed. + * @param down The downlink speed. + */ + private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) { + var n = name ?: "no tag" + n = n.substring(0, min(n.length, 6)) + text.append(n) + for (i in n.length..6 step 2) { + text.append("\t") + } + text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n") + } + + /** + * Gets the service instance. + * @return The service instance. + */ + private fun getService(): Service? { + return V2RayServiceManager.serviceControl?.get()?.getService() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt new file mode 100644 index 00000000..e81e3615 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ProcessService.kt @@ -0,0 +1,52 @@ +package com.v2ray.ang.service + +import android.content.Context +import android.util.Log +import com.v2ray.ang.AppConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ProcessService { + private var process: Process? = null + + /** + * Runs a process with the given command. + * @param context The context. + * @param cmd The command to run. + */ + fun runProcess(context: Context, cmd: MutableList) { + Log.i(AppConfig.TAG, cmd.toString()) + + try { + val proBuilder = ProcessBuilder(cmd) + proBuilder.redirectErrorStream(true) + process = proBuilder + .directory(context.filesDir) + .start() + + CoroutineScope(Dispatchers.IO).launch { + Thread.sleep(50L) + Log.i(AppConfig.TAG, "runProcess check") + process?.waitFor() + Log.i(AppConfig.TAG, "runProcess exited") + } + Log.i(AppConfig.TAG, process.toString()) + + } catch (e: Exception) { + Log.e(AppConfig.TAG, e.toString(), e) + } + } + + /** + * Stops the running process. + */ + fun stopProcess() { + try { + Log.i(AppConfig.TAG, "runProcess destroy") + process?.destroy() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to destroy process", e) + } + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt similarity index 58% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt index b7e07e91..7aecf634 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/QSTileService.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt @@ -1,6 +1,5 @@ package com.v2ray.ang.service -import android.annotation.TargetApi import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -9,52 +8,80 @@ import android.graphics.drawable.Icon import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.Utils import java.lang.ref.SoftReference -@TargetApi(Build.VERSION_CODES.N) +@RequiresApi(Build.VERSION_CODES.N) class QSTileService : TileService() { + /** + * Sets the state of the tile. + * @param state The state to set. + */ fun setState(state: Int) { + qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) 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) } 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?.label = V2RayServiceManager.getRunningServerName() } qsTile?.updateTile() } + /** + * Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int): + * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`. + */ override fun onStartListening() { super.onStartListening() - setState(Tile.STATE_INACTIVE) + + if (V2RayServiceManager.isRunning()) { + setState(Tile.STATE_ACTIVE) + } else { + setState(Tile.STATE_INACTIVE) + } mMsgReceive = ReceiveMessageHandler(this) - registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) + val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY) + ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags()) MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") } + /** + * Called when the tile stops listening. + */ override fun onStopListening() { super.onStopListening() - unregisterReceiver(mMsgReceive) - mMsgReceive = null + try { + applicationContext.unregisterReceiver(mMsgReceive) + mMsgReceive = null + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to unregister receiver", e) + } + } + /** + * Called when the tile is clicked. + */ override fun onClick() { super.onClick() when (qsTile.state) { Tile.STATE_INACTIVE -> { - Utils.startVServiceFromToggle(this) + V2RayServiceManager.startVServiceFromToggle(this) } + Tile.STATE_ACTIVE -> { - Utils.stopVService(this) + V2RayServiceManager.stopVService(this) } } } @@ -62,22 +89,26 @@ class QSTileService : TileService() { private var mMsgReceive: BroadcastReceiver? = null private class ReceiveMessageHandler(context: QSTileService) : BroadcastReceiver() { - internal var mReference: SoftReference = SoftReference(context) + var mReference: SoftReference = SoftReference(context) override fun onReceive(ctx: Context?, intent: Intent?) { val context = mReference.get() when (intent?.getIntExtra("key", 0)) { AppConfig.MSG_STATE_RUNNING -> { context?.setState(Tile.STATE_ACTIVE) } + AppConfig.MSG_STATE_NOT_RUNNING -> { context?.setState(Tile.STATE_INACTIVE) } + AppConfig.MSG_STATE_START_SUCCESS -> { context?.setState(Tile.STATE_ACTIVE) } + AppConfig.MSG_STATE_START_FAILURE -> { context?.setState(Tile.STATE_INACTIVE) } + AppConfig.MSG_STATE_STOP_SUCCESS -> { context?.setState(Tile.STATE_INACTIVE) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt new file mode 100644 index 00000000..085c72eb --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/ServiceControl.kt @@ -0,0 +1,28 @@ +package com.v2ray.ang.service + +import android.app.Service + +interface ServiceControl { + /** + * Gets the service instance. + * @return The service instance. + */ + fun getService(): Service + + /** + * Starts the service. + */ + fun startService() + + /** + * Stops the service. + */ + fun stopService() + + /** + * Protects the VPN socket. + * @param socket The socket to protect. + * @return True if the socket is protected, false otherwise. + */ + fun vpnProtect(socket: Int): Boolean +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt new file mode 100644 index 00000000..5f3f8172 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/SubscriptionUpdater.kt @@ -0,0 +1,67 @@ +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.handler.AngConfigManager.updateConfigViaSub +import com.v2ray.ang.handler.MmkvManager + +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) + + /** + * Performs the subscription update work. + * @return The result of the work. + */ + @SuppressLint("MissingPermission") + override suspend fun doWork(): Result { + Log.i(AppConfig.TAG, "subscription automatic update starting") + + val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate } + + for (sub in subs) { + val subItem = sub.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.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}") + updateConfigViaSub(Pair(sub.first, subItem)) + notification.setContentText("Updating ${subItem.remarks}") + } + notificationManager.cancel(3) + return Result.success() + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt new file mode 100644 index 00000000..25fcd1a6 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt @@ -0,0 +1,93 @@ +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.handler.SettingsManager +import com.v2ray.ang.util.MyContextWrapper +import java.lang.ref.SoftReference + +class V2RayProxyOnlyService : Service(), ServiceControl { + /** + * Initializes the service. + */ + override fun onCreate() { + super.onCreate() + V2RayServiceManager.serviceControl = SoftReference(this) + } + + /** + * Handles the start command for the service. + * @param intent The intent. + * @param flags The flags. + * @param startId The start ID. + * @return The start mode. + */ + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + V2RayServiceManager.startCoreLoop() + return START_STICKY + } + + /** + * Destroys the service. + */ + override fun onDestroy() { + super.onDestroy() + V2RayServiceManager.stopCoreLoop() + } + + /** + * Gets the service instance. + * @return The service instance. + */ + override fun getService(): Service { + return this + } + + /** + * Starts the service. + */ + override fun startService() { + // do nothing + } + + /** + * Stops the service. + */ + override fun stopService() { + stopSelf() + } + + /** + * Protects the VPN socket. + * @param socket The socket to protect. + * @return True if the socket is protected, false otherwise. + */ + override fun vpnProtect(socket: Int): Boolean { + return true + } + + /** + * Binds the service. + * @param intent The intent. + * @return The binder. + */ + override fun onBind(intent: Intent?): IBinder? { + return null + } + + /** + * Attaches the base context to the service. + * @param newBase The new base context. + */ + @RequiresApi(Build.VERSION_CODES.N) + override fun attachBaseContext(newBase: Context?) { + val context = newBase?.let { + MyContextWrapper.wrap(newBase, SettingsManager.getLocale()) + } + super.attachBaseContext(context) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt new file mode 100644 index 00000000..4f42ca23 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt @@ -0,0 +1,377 @@ +package com.v2ray.ang.service + +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.util.Log +import androidx.core.content.ContextCompat +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.extension.toast +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.handler.SpeedtestManager +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.PluginUtil +import com.v2ray.ang.util.Utils +import go.Seq +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import libv2ray.CoreCallbackHandler +import libv2ray.CoreController +import libv2ray.Libv2ray +import java.lang.ref.SoftReference + +object V2RayServiceManager { + + private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback()) + private val mMsgReceive = ReceiveMessageHandler() + private var currentConfig: ProfileItem? = null + + var serviceControl: SoftReference? = null + set(value) { + field = value + Seq.setContext(value?.get()?.getService()?.applicationContext) + Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) + } + + /** + * Starts the V2Ray service from a toggle action. + * @param context The context from which the service is started. + * @return True if the service was started successfully, false otherwise. + */ + fun startVServiceFromToggle(context: Context): Boolean { + if (MmkvManager.getSelectServer().isNullOrEmpty()) { + context.toast(R.string.app_tile_first_use) + return false + } + startContextService(context) + return true + } + + /** + * Starts the V2Ray service. + * @param context The context from which the service is started. + * @param guid The GUID of the server configuration to use (optional). + */ + fun startVService(context: Context, guid: String? = null) { + if (guid != null) { + MmkvManager.setSelectServer(guid) + } + startContextService(context) + } + + /** + * Stops the V2Ray service. + * @param context The context from which the service is stopped. + */ + fun stopVService(context: Context) { + context.toast(R.string.toast_services_stop) + MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "") + } + + /** + * Checks if the V2Ray service is running. + * @return True if the service is running, false otherwise. + */ + fun isRunning() = coreController.isRunning + + /** + * Gets the name of the currently running server. + * @return The name of the running server. + */ + fun getRunningServerName() = currentConfig?.remarks.orEmpty() + + /** + * Starts the context service for V2Ray. + * Chooses between VPN service or Proxy-only service based on user settings. + * @param context The context from which the service is started. + */ + private fun startContextService(context: Context) { + if (coreController.isRunning) { + return + } + val guid = MmkvManager.getSelectServer() ?: return + val config = MmkvManager.decodeServerConfig(guid) ?: return + if (config.configType != EConfigType.CUSTOM + && !Utils.isValidUrl(config.server) + && !Utils.isPureIpAddress(config.server.orEmpty()) + ) return +// val result = V2rayConfigUtil.getV2rayConfig(context, guid) +// if (!result.status) return + + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) == true) { + context.toast(R.string.toast_warning_pref_proxysharing_short) + } else { + context.toast(R.string.toast_services_start) + } + val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) { + Intent(context.applicationContext, V2RayVpnService::class.java) + } else { + Intent(context.applicationContext, V2RayProxyOnlyService::class.java) + } + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + /** + * Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int): + * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`. + * Starts the V2Ray core service. + */ + fun startCoreLoop(): Boolean { + if (coreController.isRunning) { + return false + } + + val service = getService() ?: return false + val guid = MmkvManager.getSelectServer() ?: return false + val config = MmkvManager.decodeServerConfig(guid) ?: return false + val result = V2rayConfigManager.getV2rayConfig(service, guid) + if (!result.status) + return false + + 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) + ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags()) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e) + return false + } + + currentConfig = config + + try { + coreController.startLoop(result.content) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to start Core loop", e) + return false + } + + if (coreController.isRunning == false) { + MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") + NotificationService.cancelNotification() + return false + } + + try { + MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") + NotificationService.showNotification(currentConfig) + NotificationService.startSpeedNotification(currentConfig) + + PluginUtil.runPlugin(service, config, result.socksPort) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to startup service", e) + return false + } + return true + } + + /** + * Stops the V2Ray core service. + * Unregisters broadcast receivers, stops notifications, and shuts down plugins. + * @return True if the core was stopped successfully, false otherwise. + */ + fun stopCoreLoop(): Boolean { + val service = getService() ?: return false + + if (coreController.isRunning) { + CoroutineScope(Dispatchers.IO).launch { + try { + coreController.stopLoop() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e) + } + } + } + + MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "") + NotificationService.cancelNotification() + + try { + service.unregisterReceiver(mMsgReceive) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e) + } + PluginUtil.stopPlugin() + + return true + } + + /** + * Queries the statistics for a given tag and link. + * @param tag The tag to query. + * @param link The link to query. + * @return The statistics value. + */ + fun queryStats(tag: String, link: String): Long { + return coreController.queryStats(tag, link) + } + + /** + * Measures the connection delay for the current V2Ray configuration. + * Tests with primary URL first, then falls back to alternative URL if needed. + * Also fetches remote IP information if the delay test was successful. + */ + private fun measureV2rayDelay() { + if (coreController.isRunning == false) { + return + } + + CoroutineScope(Dispatchers.IO).launch { + val service = getService() ?: return@launch + var time = -1L + var errorStr = "" + + try { + time = coreController.measureDelay(SettingsManager.getDelayTestUrl()) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e) + errorStr = e.message?.substringAfter("\":") ?: "empty message" + } + if (time == -1L) { + try { + time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true)) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e) + errorStr = e.message?.substringAfter("\":") ?: "empty message" + } + } + + val result = if (time >= 0) { + service.getString(R.string.connection_test_available, time) + } else { + service.getString(R.string.connection_test_error, errorStr) + } + MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result) + + // Only fetch IP info if the delay test was successful + if (time >= 0) { + SpeedtestManager.getRemoteIPInfo()?.let { ip -> + MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip") + } + } + } + } + + /** + * Gets the current service instance. + * @return The current service instance, or null if not available. + */ + private fun getService(): Service? { + return serviceControl?.get()?.getService() + } + + /** + * Core callback handler implementation for handling V2Ray core events. + * Handles startup, shutdown, socket protection, and status emission. + */ + private class CoreCallback : CoreCallbackHandler { + /** + * Called when V2Ray core starts up. + * @return 0 for success, any other value for failure. + */ + override fun startup(): Long { + return 0 + } + + /** + * Called when V2Ray core shuts down. + * @return 0 for success, any other value for failure. + */ + override fun shutdown(): Long { + val serviceControl = serviceControl?.get() ?: return -1 + return try { + serviceControl.stopService() + 0 + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to stop service in callback", e) + -1 + } + } + + /** + * Called when V2Ray core emits status information. + * @param l Status code. + * @param s Status message. + * @return Always returns 0. + */ + override fun onEmitStatus(l: Long, s: String?): Long { + return 0 + } + } + + /** + * Broadcast receiver for handling messages sent to the service. + * Handles registration, service control, and screen events. + */ + private class ReceiveMessageHandler : BroadcastReceiver() { + /** + * Handles received broadcast messages. + * Processes service control messages and screen state changes. + * @param ctx The context in which the receiver is running. + * @param intent The intent being received. + */ + override fun onReceive(ctx: Context?, intent: Intent?) { + val serviceControl = serviceControl?.get() ?: return + when (intent?.getIntExtra("key", 0)) { + AppConfig.MSG_REGISTER_CLIENT -> { + if (coreController.isRunning) { + MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "") + } else { + MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "") + } + } + + AppConfig.MSG_UNREGISTER_CLIENT -> { + // nothing to do + } + + AppConfig.MSG_STATE_START -> { + // nothing to do + } + + AppConfig.MSG_STATE_STOP -> { + Log.i(AppConfig.TAG, "Stop Service") + serviceControl.stopService() + } + + AppConfig.MSG_STATE_RESTART -> { + Log.i(AppConfig.TAG, "Restart Service") + serviceControl.stopService() + Thread.sleep(500L) + startVService(serviceControl.getService()) + } + + AppConfig.MSG_MEASURE_DELAY -> { + measureV2rayDelay() + } + } + + when (intent?.action) { + Intent.ACTION_SCREEN_OFF -> { + Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats") + NotificationService.stopSpeedNotification(currentConfig) + } + + Intent.ACTION_SCREEN_ON -> { + Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats") + NotificationService.startSpeedNotification(currentConfig) + } + } + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt new file mode 100644 index 00000000..3fef1ae1 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt @@ -0,0 +1,91 @@ +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.dto.EConfigType +import com.v2ray.ang.extension.serializable +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SpeedtestManager +import com.v2ray.ang.handler.V2rayConfigManager +import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.PluginUtil +import com.v2ray.ang.util.Utils +import go.Seq +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import libv2ray.Libv2ray +import java.util.concurrent.Executors + +class V2RayTestService : Service() { + private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) } + + /** + * Initializes the V2Ray environment. + */ + override fun onCreate() { + super.onCreate() + Seq.setContext(this) + Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey()) + } + + /** + * Handles the start command for the service. + * @param intent The intent. + * @param flags The flags. + * @param startId The start ID. + * @return The start mode. + */ + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.getIntExtra("key", 0)) { + MSG_MEASURE_CONFIG -> { + val guid = intent.serializable("content") ?: "" + realTestScope.launch { + val result = startRealPing(guid) + MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result)) + } + } + + MSG_MEASURE_CONFIG_CANCEL -> { + realTestScope.coroutineContext[Job]?.cancelChildren() + } + } + return super.onStartCommand(intent, flags, startId) + } + + /** + * Binds the service. + * @param intent The intent. + * @return The binder. + */ + override fun onBind(intent: Intent?): IBinder? { + return null + } + + /** + * Starts the real ping test. + * @param guid The GUID of the configuration. + * @return The ping result. + */ + private fun startRealPing(guid: String): Long { + val retFailure = -1L + + val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure + if (config.configType == EConfigType.HYSTERIA2) { + val delay = PluginUtil.realPingHy2(this, config) + return delay + } else { + val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid) + if (!configResult.status) { + return retFailure + } + return SpeedtestManager.realPing(configResult.content) + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt new file mode 100644 index 00000000..d734c299 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt @@ -0,0 +1,375 @@ +package com.v2ray.ang.service + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.net.LocalSocket +import android.net.LocalSocketAddress +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.net.ProxyInfo +import android.net.VpnService +import android.os.Build +import android.os.ParcelFileDescriptor +import android.os.StrictMode +import android.util.Log +import androidx.annotation.RequiresApi +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.BuildConfig +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.util.MyContextWrapper +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File +import java.lang.ref.SoftReference + +class V2RayVpnService : VpnService(), ServiceControl { + companion object { + private const val VPN_MTU = 1500 + private const val TUN2SOCKS = "libtun2socks.so" + } + + private lateinit var mInterface: ParcelFileDescriptor + private var isRunning = false + 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() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .build() + } + + private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager } + + @delegate:RequiresApi(Build.VERSION_CODES.P) + private val defaultNetworkCallback by lazy { + object : ConnectivityManager.NetworkCallback() { + 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) + } + } + } + + override fun onCreate() { + super.onCreate() + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + V2RayServiceManager.serviceControl = SoftReference(this) + } + + override fun onRevoke() { + stopV2Ray() + } + +// override fun onLowMemory() { +// stopV2Ray() +// super.onLowMemory() +// } + + override fun onDestroy() { + super.onDestroy() + NotificationService.cancelNotification() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (V2RayServiceManager.startCoreLoop()) { + startService() + } + return START_STICKY + //return super.onStartCommand(intent, flags, startId) + } + + override fun getService(): Service { + return this + } + + override fun startService() { + setup() + } + + override fun stopService() { + stopV2Ray(true) + } + + override fun vpnProtect(socket: Int): Boolean { + return protect(socket) + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun attachBaseContext(newBase: Context?) { + val context = newBase?.let { + MyContextWrapper.wrap(newBase, SettingsManager.getLocale()) + } + super.attachBaseContext(context) + } + + /** + * Sets up the VPN service. + * Prepares the VPN and configures it if preparation is successful. + */ + private fun setup() { + val prepare = prepare(this) + if (prepare != null) { + return + } + + if (setupVpnService() != true) { + return + } + + runTun2socks() + } + + /** + * Configures the VPN service. + * @return True if the VPN service was configured successfully, false otherwise. + */ + private fun setupVpnService(): Boolean { + // If the old interface has exactly the same parameters, use it! + // Configure a builder while parsing the parameters. + val builder = Builder() + val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig() + //val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) + + builder.setMtu(VPN_MTU) + builder.addAddress(vpnConfig.ipv4Client, 30) + //builder.addDnsServer(PRIVATE_VLAN4_ROUTER) + val bypassLan = SettingsManager.routingRulesetsBypassLan() + if (bypassLan) { + AppConfig.ROUTED_IP_LIST.forEach { + val addr = it.split('/') + builder.addRoute(addr[0], addr[1].toInt()) + } + } else { + builder.addRoute("0.0.0.0", 0) + } + + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) { + builder.addAddress(vpnConfig.ipv6Client, 126) + if (bypassLan) { + builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use + builder.addRoute("fc00::", 18) //Xray-core default FakeIPv6 Pool + } else { + builder.addRoute("::", 0) + } + } + +// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { +// builder.addDnsServer(PRIVATE_VLAN4_ROUTER) +// } else { + SettingsManager.getVpnDnsServers() + .forEach { + if (Utils.isPureIpAddress(it)) { + builder.addDnsServer(it) + } + } +// } + + builder.setSession(V2RayServiceManager.getRunningServerName()) + + val selfPackageName = BuildConfig.APPLICATION_ID + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) { + val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET) + val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS) + //process self package + if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName) + apps?.forEach { + try { + if (bypassApps) + builder.addDisallowedApplication(it) + else + builder.addAllowedApplication(it) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e) + } + } + } else { + builder.addDisallowedApplication(selfPackageName) + } + + // Close the old interface since the parameters have been changed. + try { + mInterface.close() + } catch (ignored: Exception) { + // ignored + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to request default network", e) + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + builder.setMetered(false) + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY)) { + builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOOPBACK, SettingsManager.getHttpPort())) + } + } + + // Create a new interface using the builder and save the parameters. + try { + mInterface = builder.establish()!! + isRunning = true + return true + } catch (e: Exception) { + // non-nullable lateinit var + Log.e(AppConfig.TAG, "Failed to establish VPN interface", e) + stopV2Ray() + } + return false + } + + /** + * Runs the tun2socks process. + * Starts the tun2socks process with the appropriate parameters. + */ + private fun runTun2socks() { + Log.i(AppConfig.TAG, "Start run $TUN2SOCKS") + val socksPort = SettingsManager.getSocksPort() + val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig() + val cmd = arrayListOf( + File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath, + "--netif-ipaddr", vpnConfig.ipv4Router, + "--netif-netmask", "255.255.255.252", + "--socks-server-addr", "$LOOPBACK:${socksPort}", + "--tunmtu", VPN_MTU.toString(), + "--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath, + "--enable-udprelay", + "--loglevel", "notice" + ) + + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) { + cmd.add("--netif-ip6addr") + cmd.add(vpnConfig.ipv6Router) + } + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) { + val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt()) + cmd.add("--dnsgw") + cmd.add("$LOOPBACK:${localDnsPort}") + } + Log.i(AppConfig.TAG, cmd.toString()) + + try { + val proBuilder = ProcessBuilder(cmd) + proBuilder.redirectErrorStream(true) + process = proBuilder + .directory(applicationContext.filesDir) + .start() + Thread { + Log.i(AppConfig.TAG, "$TUN2SOCKS check") + process.waitFor() + Log.i(AppConfig.TAG, "$TUN2SOCKS exited") + if (isRunning) { + Log.i(AppConfig.TAG, "$TUN2SOCKS restart") + runTun2socks() + } + }.start() + Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}") + + sendFd() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e) + } + } + + /** + * Sends the file descriptor to the tun2socks process. + * Attempts to send the file descriptor multiple times if necessary. + */ + private fun sendFd() { + val fd = mInterface.fileDescriptor + val path = File(applicationContext.filesDir, "sock_path").absolutePath + Log.i(AppConfig.TAG, "LocalSocket path : $path") + + CoroutineScope(Dispatchers.IO).launch { + var tries = 0 + while (true) try { + Thread.sleep(50L shl tries) + Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries") + LocalSocket().use { localSocket -> + localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) + localSocket.setFileDescriptorsForSend(arrayOf(fd)) + localSocket.outputStream.write(42) + } + break + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to send file descriptor, try: $tries", e) + if (tries > 5) break + tries += 1 + } + } + } + + /** + * Stops the V2Ray service. + * @param isForced Whether to force stop the service. + */ + private fun stopV2Ray(isForced: Boolean = true) { +// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "") +// 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) + } catch (ignored: Exception) { + // ignored + } + } + + try { + Log.i(AppConfig.TAG, "$TUN2SOCKS destroy") + process.destroy() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e) + } + + V2RayServiceManager.stopCoreLoop() + + if (isForced) { + //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped + //It's strage but true. + //This can be verified by putting stopself() behind and call stopLoop and startLoop + //in a row for several times. You will find that later created v2ray core report port in use + //which means the first v2ray core somehow failed to stop and release the port. + stopSelf() + + try { + mInterface.close() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to close VPN interface", e) + } + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt new file mode 100644 index 00000000..1931cb45 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt @@ -0,0 +1,201 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +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.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SpeedtestManager +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 val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) } + private val extDir by lazy { File(Utils.backupPath(this)) } + + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + try { + showFileChooser() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to show file chooser", e) + } + } else { + toast(R.string.toast_permission_denied) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + 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) { + toastSuccess(R.string.toast_success) + } else { + toastError(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 { + toastError(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 + } + + if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) { + try { + showFileChooser() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to show file chooser", e) + } + } else { + requestPermissionLauncher.launch(permission) + } + } + + binding.layoutSoureCcode.setOnClickListener { + Utils.openUri(this, AppConfig.APP_URL) + } + + binding.layoutFeedback.setOnClickListener { + Utils.openUri(this, AppConfig.APP_ISSUES_URL) + } + + binding.layoutOssLicenses.setOnClickListener { + val webView = android.webkit.WebView(this) + webView.loadUrl("file:///android_asset/open_source_licenses.html") + android.app.AlertDialog.Builder(this) + .setTitle("Open source licenses") + .setView(webView) + .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() } + .show() + } + + binding.layoutTgChannel.setOnClickListener { + Utils.openUri(this, AppConfig.TG_CHANNEL_URL) + } + + binding.layoutPrivacyPolicy.setOnClickListener { + Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY) + } + + "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also { + binding.tvVersion.text = it + } + } + + private fun backupConfiguration(outputZipFilePos: String): Pair { + 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, "") + } + } + + private 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).apply { + type = "*/*" + addCategory(Intent.CATEGORY_OPENABLE) + } + + try { + chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) + } catch (ex: android.content.ActivityNotFoundException) { + Log.e(AppConfig.TAG, "File chooser activity not found", ex) + toast(R.string.toast_require_file_manager) + } + } + + private val chooseFile = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val uri = result.data?.data + if (result.resultCode == RESULT_OK && uri != null) { + 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)) { + toastSuccess(R.string.toast_success) + } else { + toastError(R.string.toast_failure) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Error during file restore", e) + toastError(R.string.toast_failure) + } + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt new file mode 100644 index 00000000..0cd7f647 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt @@ -0,0 +1,65 @@ +package com.v2ray.ang.ui + +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.content.ContextCompat +import androidx.core.view.WindowCompat +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.helper.CustomDividerItemDecoration +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 -> { + // 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?) { + super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, SettingsManager.getLocale())) + } + + /** + * Adds a custom divider to a RecyclerView. + * + * @param recyclerView The target RecyclerView to which the divider will be added. + * @param context The context used to access resources. + * @param drawableResId The resource ID of the drawable to be used as the divider. + * @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL). + */ + fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, context: Context?, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) { + // Get the drawable from resources + val drawable = ContextCompat.getDrawable(context!!, drawableResId) + requireNotNull(drawable) { "Drawable resource not found" } + + // Create a DividerItemDecoration with the specified orientation + val dividerItemDecoration = CustomDividerItemDecoration(drawable, orientation) + + // Add the divider to the RecyclerView + recyclerView.addItemDecoration(dividerItemDecoration) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt new file mode 100644 index 00000000..a9b698c5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt @@ -0,0 +1,77 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import com.v2ray.ang.AppConfig +import com.v2ray.ang.BuildConfig +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityCheckUpdateBinding +import com.v2ray.ang.dto.CheckUpdateResult +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SpeedtestManager +import com.v2ray.ang.handler.UpdateCheckerManager +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.launch + +class CheckUpdateActivity : BaseActivity() { + + private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + title = getString(R.string.update_check_for_update) + + binding.layoutCheckUpdate.setOnClickListener { + checkForUpdates(binding.checkPreRelease.isChecked) + } + + binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked -> + MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked) + } + binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false) + + "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also { + binding.tvVersion.text = it + } + + checkForUpdates(binding.checkPreRelease.isChecked) + } + + private fun checkForUpdates(includePreRelease: Boolean) { + toast(R.string.update_checking_for_update) + + lifecycleScope.launch { + try { + val result = UpdateCheckerManager.checkForUpdate(includePreRelease) + if (result.hasUpdate) { + showUpdateDialog(result) + } else { + toastSuccess(R.string.update_already_latest_version) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}") + toastError(e.message ?: getString(R.string.toast_failure)) + } + } + } + + private fun showUpdateDialog(result: CheckUpdateResult) { + AlertDialog.Builder(this) + .setTitle(getString(R.string.update_new_version_found, result.latestVersion)) + .setMessage(result.releaseNotes) + .setPositiveButton(R.string.update_now) { _, _ -> + result.downloadUrl?.let { + Utils.openUri(this, it) + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt similarity index 90% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt index d9d12992..deeb43c3 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt @@ -5,7 +5,7 @@ import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List) : - FragmentStateAdapter(fragmentActivity) { + FragmentStateAdapter(fragmentActivity) { override fun createFragment(position: Int): Fragment { return mFragments[position] diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt new file mode 100644 index 00000000..436f5913 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt @@ -0,0 +1,156 @@ +package com.v2ray.ang.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.ANG_PACKAGE +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityLogcatBinding +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.IOException + + +class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { + private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) } + + private var logsetsAll: MutableList = mutableListOf() + var logsets: MutableList = mutableListOf() + private val adapter by lazy { LogcatRecyclerAdapter(this) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + title = getString(R.string.title_logcat) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) + binding.recyclerView.adapter = adapter + + binding.refreshLayout.setOnRefreshListener(this) + + logsets.add(getString(R.string.pull_down_to_refresh)) + } + + private fun getLogcat() { + + try { + binding.refreshLayout.isRefreshing = true + + lifecycleScope.launch(Dispatchers.Default) { + val lst = LinkedHashSet() + lst.add("logcat") + lst.add("-d") + lst.add("-v") + lst.add("time") + lst.add("-s") + lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err") + val process = withContext(Dispatchers.IO) { + Runtime.getRuntime().exec(lst.toTypedArray()) + } + + val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed() + launch(Dispatchers.Main) { + logsetsAll = allText.toMutableList() + logsets = allText.toMutableList() + refreshData() + binding.refreshLayout.isRefreshing = false + } + } + } catch (e: IOException) { + Log.e(AppConfig.TAG, "Failed to get logcat", e) + } + } + + private fun clearLogcat() { + try { + lifecycleScope.launch(Dispatchers.Default) { + val lst = LinkedHashSet() + lst.add("logcat") + lst.add("-c") + withContext(Dispatchers.IO) { + val process = Runtime.getRuntime().exec(lst.toTypedArray()) + process.waitFor() + } + launch(Dispatchers.Main) { + logsetsAll.clear() + logsets.clear() + refreshData() + } + } + } catch (e: IOException) { + Log.e(AppConfig.TAG, "Failed to clear logcat", e) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_logcat, 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 = false + + override fun onQueryTextChange(newText: String?): Boolean { + filterLogs(newText) + return false + } + }) + searchView.setOnCloseListener { + filterLogs("") + false + } + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.copy_all -> { + Utils.setClipboard(this, logsets.joinToString("\n")) + toastSuccess(R.string.toast_success) + true + } + + R.id.clear_all -> { + clearLogcat() + true + } + + else -> super.onOptionsItemSelected(item) + } + + private fun filterLogs(content: String?): Boolean { + val key = content?.trim() + logsets = if (key.isNullOrEmpty()) { + logsetsAll.toMutableList() + } else { + logsetsAll.filter { it.contains(key) }.toMutableList() + } + + refreshData() + return true + } + + override fun onRefresh() { + getLogcat() + } + + @SuppressLint("NotifyDataSetChanged") + fun refreshData() { + adapter.notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt new file mode 100644 index 00000000..e83714d5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatRecyclerAdapter.kt @@ -0,0 +1,44 @@ +package com.v2ray.ang.ui + +import android.util.Log +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.AppConfig +import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding + +class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter() { + private var mActivity: LogcatActivity = activity + + + override fun getItemCount() = mActivity.logsets.size + + override fun onBindViewHolder(holder: MainViewHolder, position: Int) { + try { + val log = mActivity.logsets[position] + if (log.isEmpty()) { + holder.itemSubSettingBinding.logTag.text = "" + holder.itemSubSettingBinding.logContent.text = "" + } else { + val content = log.split("):", limit = 2) + holder.itemSubSettingBinding.logTag.text = content.first().split("(", limit = 2).first().trim() + holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else "" + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Error binding log view data", e) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { + return MainViewHolder( + ItemRecyclerLogcatBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + class MainViewHolder(val itemSubSettingBinding: ItemRecyclerLogcatBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root) + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt new file mode 100644 index 00000000..0c7584d8 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt @@ -0,0 +1,695 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.ColorStateList +import android.net.Uri +import android.net.VpnService +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +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.appcompat.widget.SearchView +import androidx.core.content.ContextCompat +import androidx.core.view.GravityCompat +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import com.google.android.material.navigation.NavigationView +import com.google.android.material.tabs.TabLayout +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.VPN +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityMainBinding +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.MigrateManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.helper.SimpleItemTouchHelperCallback +import com.v2ray.ang.service.V2RayServiceManager +import com.v2ray.ang.util.Utils +import com.v2ray.ang.viewmodel.MainViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { + private val binding by lazy { + ActivityMainBinding.inflate(layoutInflater) + } + + private val adapter by lazy { MainRecyclerAdapter(this) } + private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + 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() + + // register activity result for requesting permission + private val requestPermissionLauncher = + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + when (pendingAction) { + Action.IMPORT_QR_CODE_CONFIG -> + scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) + + Action.READ_CONTENT_FROM_URI -> + chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { + type = "*/*" + addCategory(Intent.CATEGORY_OPENABLE) + }, getString(R.string.title_file_chooser))) + + Action.POST_NOTIFICATIONS -> {} + else -> {} + } + } else { + toast(R.string.toast_permission_denied) + } + pendingAction = Action.NONE + } + + private var pendingAction: Action = Action.NONE + + enum class Action { + NONE, + IMPORT_QR_CODE_CONFIG, + READ_CONTENT_FROM_URI, + POST_NOTIFICATIONS + } + + private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val uri = it.data?.data + if (it.resultCode == RESULT_OK && uri != null) { + readContentFromUri(uri) + } + } + + private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + importBatchConfig(it.data?.getStringExtra("SCAN_RESULT")) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + title = getString(R.string.title_server) + setSupportActionBar(binding.toolbar) + + binding.fab.setOnClickListener { + if (mainViewModel.isRunning.value == true) { + V2RayServiceManager.stopVService(this) + } else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) { + val intent = VpnService.prepare(this) + if (intent == null) { + startV2Ray() + } else { + requestVpnPermission.launch(intent) + } + } else { + startV2Ray() + } + } + binding.layoutTest.setOnClickListener { + if (mainViewModel.isRunning.value == true) { + setTestState(getString(R.string.connection_test_testing)) + mainViewModel.testCurrentServerRealPing() + } else { +// tv_test_state.text = getString(R.string.connection_test_fail) + } + } + + binding.recyclerView.setHasFixedSize(true) + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) { + binding.recyclerView.layoutManager = GridLayoutManager(this, 2) + } else { + binding.recyclerView.layoutManager = GridLayoutManager(this, 1) + } + addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) + binding.recyclerView.adapter = adapter + + mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) + mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) + + val toggle = ActionBarDrawerToggle( + this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close + ) + binding.drawerLayout.addDrawerListener(toggle) + toggle.syncState() + binding.navView.setNavigationItemSelectedListener(this) + + initGroupTab() + setupViewModel() + migrateLegacy() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + pendingAction = Action.POST_NOTIFICATIONS + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START) + } else { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + isEnabled = true + } + } + }) + } + + @SuppressLint("NotifyDataSetChanged") + private fun setupViewModel() { + mainViewModel.updateListAction.observe(this) { index -> + if (index >= 0) { + adapter.notifyItemChanged(index) + } else { + adapter.notifyDataSetChanged() + } + } + mainViewModel.updateTestResultAction.observe(this) { setTestState(it) } + mainViewModel.isRunning.observe(this) { isRunning -> + adapter.isRunning = isRunning + if (isRunning) { + 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_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 + } + } + mainViewModel.startListenBroadcast() + mainViewModel.initAssets(assets) + } + + private fun migrateLegacy() { + lifecycleScope.launch(Dispatchers.IO) { + val result = MigrateManager.migrateServerConfig2Profile() + 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 + } + + private fun startV2Ray() { + if (MmkvManager.getSelectServer().isNullOrEmpty()) { + toast(R.string.title_file_chooser) + return + } + V2RayServiceManager.startVService(this) + } + + private fun restartV2Ray() { + if (mainViewModel.isRunning.value == true) { + V2RayServiceManager.stopVService(this) + } + lifecycleScope.launch { + delay(500) + startV2Ray() + } + } + + public override fun onResume() { + super.onResume() + mainViewModel.reloadServerList() + } + + public override fun onPause() { + super.onPause() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, 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 = false + + override fun onQueryTextChange(newText: String?): Boolean { + mainViewModel.filterConfig(newText.orEmpty()) + return false + } + }) + + searchView.setOnCloseListener { + mainViewModel.filterConfig("") + false + } + } + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.import_qrcode -> { + importQRcode() + true + } + + R.id.import_clipboard -> { + importClipboard() + true + } + + R.id.import_local -> { + importConfigLocal() + true + } + + R.id.import_manually_vmess -> { + importManually(EConfigType.VMESS.value) + true + } + + R.id.import_manually_vless -> { + importManually(EConfigType.VLESS.value) + true + } + + R.id.import_manually_ss -> { + importManually(EConfigType.SHADOWSOCKS.value) + true + } + + R.id.import_manually_socks -> { + importManually(EConfigType.SOCKS.value) + true + } + + R.id.import_manually_http -> { + importManually(EConfigType.HTTP.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_manually_hysteria2 -> { + importManually(EConfigType.HYSTERIA2.value) + true + } + + R.id.export_all -> { + exportAll() + true + } + + R.id.ping_all -> { + toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count())) + mainViewModel.testAllTcping() + true + } + + R.id.real_ping_all -> { + toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count())) + mainViewModel.testAllRealPing() + true + } + + R.id.service_restart -> { + restartV2Ray() + true + } + + R.id.del_all_config -> { + delAllConfig() + true + } + + R.id.del_duplicate_config -> { + delDuplicateConfig() + true + } + + R.id.del_invalid_config -> { + delInvalidConfig() + true + } + + R.id.sort_by_test_results -> { + sortByTestResults() + true + } + + R.id.sub_update -> { + importConfigViaSub() + 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 + */ + private fun importQRcode(): Boolean { + val permission = Manifest.permission.CAMERA + if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) + } else { + pendingAction = Action.IMPORT_QR_CODE_CONFIG + requestPermissionLauncher.launch(permission) + } + return true + } + + /** + * import config from clipboard + */ + private fun importClipboard() + : Boolean { + try { + val clipboard = Utils.getClipboard(this) + importBatchConfig(clipboard) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to import config from clipboard", e) + return false + } + return true + } + + private fun importBatchConfig(server: String?) { + binding.pbWaiting.show() + + lifecycleScope.launch(Dispatchers.IO) { + try { + val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true) + delay(500L) + withContext(Dispatchers.Main) { + when { + count > 0 -> { + toast(getString(R.string.title_import_config_count, count)) + mainViewModel.reloadServerList() + } + + countSub > 0 -> initGroupTab() + else -> toastError(R.string.toast_failure) + } + binding.pbWaiting.hide() + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + toastError(R.string.toast_failure) + binding.pbWaiting.hide() + } + Log.e(AppConfig.TAG, "Failed to import batch config", e) + } + } + } + + /** + * import config from local config file + */ + private fun importConfigLocal(): Boolean { + try { + showFileChooser() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to import config from local file", e) + return false + } + return true + } + + + /** + * import config from sub + */ + private fun importConfigViaSub(): Boolean { + binding.pbWaiting.show() + + lifecycleScope.launch(Dispatchers.IO) { + val count = mainViewModel.updateConfigViaSubAll() + delay(500L) + launch(Dispatchers.Main) { + if (count > 0) { + toast(getString(R.string.title_update_config_count, count)) + mainViewModel.reloadServerList() + } else { + toastError(R.string.toast_failure) + } + binding.pbWaiting.hide() + } + } + return true + } + + private fun exportAll() { + binding.pbWaiting.show() + lifecycleScope.launch(Dispatchers.IO) { + val ret = mainViewModel.exportAllServer() + launch(Dispatchers.Main) { + if (ret > 0) + toast(getString(R.string.title_export_config_count, ret)) + else + toastError(R.string.toast_failure) + binding.pbWaiting.hide() + } + } + } + + private fun delAllConfig() { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + binding.pbWaiting.show() + lifecycleScope.launch(Dispatchers.IO) { + val ret = mainViewModel.removeAllServer() + launch(Dispatchers.Main) { + mainViewModel.reloadServerList() + toast(getString(R.string.title_del_config_count, ret)) + binding.pbWaiting.hide() + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() + } + + private fun delDuplicateConfig() { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + binding.pbWaiting.show() + lifecycleScope.launch(Dispatchers.IO) { + val ret = mainViewModel.removeDuplicateServer() + launch(Dispatchers.Main) { + mainViewModel.reloadServerList() + toast(getString(R.string.title_del_duplicate_config_count, ret)) + binding.pbWaiting.hide() + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() + } + + private fun delInvalidConfig() { + AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + binding.pbWaiting.show() + lifecycleScope.launch(Dispatchers.IO) { + val ret = mainViewModel.removeInvalidServer() + launch(Dispatchers.Main) { + mainViewModel.reloadServerList() + toast(getString(R.string.title_del_config_count, ret)) + binding.pbWaiting.hide() + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() + } + + private fun sortByTestResults() { + binding.pbWaiting.show() + lifecycleScope.launch(Dispatchers.IO) { + mainViewModel.sortByTestResults() + launch(Dispatchers.Main) { + mainViewModel.reloadServerList() + binding.pbWaiting.hide() + } + } + } + + /** + * show file chooser + */ + private fun showFileChooser() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "*/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + + val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + + if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + pendingAction = Action.READ_CONTENT_FROM_URI + chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) + } else { + requestPermissionLauncher.launch(permission) + } + } + + /** + * 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 + } + + if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + try { + contentResolver.openInputStream(uri).use { input -> + importBatchConfig(input?.bufferedReader()?.readText()) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to read content from URI", e) + } + } else { + requestPermissionLauncher.launch(permission) + } + } + + private fun setTestState(content: String?) { + binding.tvTestState.text = content + } + +// val mConnection = object : ServiceConnection { +// override fun onServiceDisconnected(name: ComponentName?) { +// } +// +// override fun onServiceConnected(name: ComponentName?, service: IBinder?) { +// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "") +// } +// } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) { + moveTaskToBack(false) + return true + } + return super.onKeyDown(keyCode, event) + } + + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + // Handle navigation view item clicks here. + when (item.itemId) { + R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java)) + R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java)) + R.id.routing_setting -> requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java)) + R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java)) + R.id.settings -> startActivity( + Intent(this, SettingsActivity::class.java) + .putExtra("isRunning", mainViewModel.isRunning.value == true) + ) + + R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}") + R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java)) + R.id.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java)) + R.id.about -> startActivity(Intent(this, AboutActivity::class.java)) + } + + binding.drawerLayout.closeDrawer(GravityCompat.START) + return true + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt new file mode 100644 index 00000000..e7ea6211 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt @@ -0,0 +1,362 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.graphics.Color +import android.text.TextUtils +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.AngApplication.Companion.application +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ItemQrcodeBinding +import com.v2ray.ang.databinding.ItemRecyclerFooterBinding +import com.v2ray.ang.databinding.ItemRecyclerMainBinding +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.helper.ItemTouchHelperAdapter +import com.v2ray.ang.helper.ItemTouchHelperViewHolder +import com.v2ray.ang.service.V2RayServiceManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { + companion object { + private const val VIEW_TYPE_ITEM = 1 + private const val VIEW_TYPE_FOOTER = 2 + } + + private var mActivity: MainActivity = activity + private val share_method: Array by lazy { + mActivity.resources.getStringArray(R.array.share_method) + } + private val share_method_more: Array by lazy { + mActivity.resources.getStringArray(R.array.share_method_more) + } + var isRunning = false + private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false) + + /** + * Gets the total number of items in the adapter (servers count + footer view) + * @return The total item count + */ + override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1 + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + if (holder is MainViewHolder) { + val guid = mActivity.mainViewModel.serversCache[position].guid + val profile = mActivity.mainViewModel.serversCache[position].profile + val isCustom = profile.configType == EConfigType.CUSTOM + + holder.itemView.setBackgroundColor(Color.TRANSPARENT) + + //Name address + holder.itemMainBinding.tvName.text = profile.remarks + holder.itemMainBinding.tvStatistics.text = getAddress(profile) + holder.itemMainBinding.tvType.text = profile.configType.name + + //TestResult + val aff = MmkvManager.decodeServerAffiliationInfo(guid) + holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty() + 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)) + } + + //layoutIndicator + if (guid == MmkvManager.getSelectServer()) { + holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent) + } else { + holder.itemMainBinding.layoutIndicator.setBackgroundResource(0) + } + + //subscription remarks + val subRemarks = getSubscriptionRemarks(profile) + holder.itemMainBinding.tvSubscription.text = subRemarks + holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE + + //layout + if (doubleColumnDisplay) { + holder.itemMainBinding.layoutShare.visibility = View.GONE + holder.itemMainBinding.layoutEdit.visibility = View.GONE + holder.itemMainBinding.layoutRemove.visibility = View.GONE + holder.itemMainBinding.layoutMore.visibility = View.VISIBLE + + //share method + val shareOptions = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList() + + holder.itemMainBinding.layoutMore.setOnClickListener { + shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0) + } + } else { + holder.itemMainBinding.layoutShare.visibility = View.VISIBLE + holder.itemMainBinding.layoutEdit.visibility = View.VISIBLE + holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE + holder.itemMainBinding.layoutMore.visibility = View.GONE + + //share method + val shareOptions = if (isCustom) share_method.asList().takeLast(1) else share_method.asList() + + holder.itemMainBinding.layoutShare.setOnClickListener { + shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0) + } + + holder.itemMainBinding.layoutEdit.setOnClickListener { + editServer(guid, profile) + } + holder.itemMainBinding.layoutRemove.setOnClickListener { + removeServer(guid, position) + } + } + + holder.itemMainBinding.infoContainer.setOnClickListener { + setSelectServer(guid) + } + } +// if (holder is FooterViewHolder) { +// if (true) { +// holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE +// } else { +// holder.itemFooterBinding.layoutEdit.setOnClickListener { +// Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}") +// } +// } +// } + } + + /** + * Gets the server address information + * Hides part of IP or domain information for privacy protection + * @param profile The server configuration + * @return Formatted address string + */ + private fun getAddress(profile: ProfileItem): String { + // Hide xxx:xxx:***/xxx.xxx.xxx.*** + return "${ + profile.server?.let { + if (it.contains(":")) + it.split(":").take(2).joinToString(":", postfix = ":***") + else + it.split('.').dropLast(1).joinToString(".", postfix = ".***") + } + } : ${profile.serverPort}" + } + + /** + * Gets the subscription remarks information + * @param profile The server configuration + * @return Subscription remarks string, or empty string if none + */ + private fun getSubscriptionRemarks(profile: ProfileItem): String { + val subRemarks = + if (mActivity.mainViewModel.subscriptionId.isEmpty()) + MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull() + else + null + return subRemarks?.toString() ?: "" + } + + /** + * Shares server configuration + * Displays a dialog with sharing options and executes the selected action + * @param guid The server unique identifier + * @param profile The server configuration + * @param position The position in the list + * @param shareOptions The list of share options + * @param skip The number of options to skip + */ + private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List, skip: Int) { + AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i -> + try { + when (i + skip) { + 0 -> showQRCode(guid) + 1 -> share2Clipboard(guid) + 2 -> shareFullContent(guid) + 3 -> editServer(guid, profile) + 4 -> removeServer(guid, position) + else -> mActivity.toast("else") + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Error when sharing server", e) + } + }.show() + } + + /** + * Displays QR code for the server configuration + * @param guid The server unique identifier + */ + private fun showQRCode(guid: String) { + val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity)) + ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid)) + AlertDialog.Builder(mActivity).setView(ivBinding.root).show() + } + + /** + * Shares server configuration to clipboard + * @param guid The server unique identifier + */ + private fun share2Clipboard(guid: String) { + if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) { + mActivity.toastSuccess(R.string.toast_success) + } else { + mActivity.toastError(R.string.toast_failure) + } + } + + /** + * Shares full server configuration content to clipboard + * @param guid The server unique identifier + */ + private fun shareFullContent(guid: String) { + mActivity.lifecycleScope.launch(Dispatchers.IO) { + val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid) + launch(Dispatchers.Main) { + if (result == 0) { + mActivity.toastSuccess(R.string.toast_success) + } else { + mActivity.toastError(R.string.toast_failure) + } + } + } + } + + /** + * Edits server configuration + * Opens appropriate editing interface based on configuration type + * @param guid The server unique identifier + * @param profile The server configuration + */ + private fun editServer(guid: String, profile: ProfileItem) { + val intent = Intent().putExtra("guid", guid) + .putExtra("isRunning", isRunning) + .putExtra("createConfigType", profile.configType.value) + if (profile.configType == EConfigType.CUSTOM) { + mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java)) + } else { + mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java)) + } + } + + /** + * Removes server configuration + * Handles confirmation dialog and related checks + * @param guid The server unique identifier + * @param position The position in the list + */ + private fun removeServer(guid: String, position: Int) { + if (guid != MmkvManager.getSelectServer()) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { + AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + removeServerSub(guid, position) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() + } else { + removeServerSub(guid, position) + } + } else { + application.toast(R.string.toast_action_not_allowed) + } + } + + /** + * Executes the actual server removal process + * @param guid The server unique identifier + * @param position The position in the list + */ + private fun removeServerSub(guid: String, position: Int) { + mActivity.mainViewModel.removeServer(guid) + notifyItemRemoved(position) + notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size) + } + + /** + * Sets the selected server + * Updates UI and restarts service if needed + * @param guid The server unique identifier to select + */ + private fun setSelectServer(guid: String) { + val selected = MmkvManager.getSelectServer() + if (guid != selected) { + MmkvManager.setSelectServer(guid) + if (!TextUtils.isEmpty(selected)) { + notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty())) + } + notifyItemChanged(mActivity.mainViewModel.getPosition(guid)) + if (isRunning) { + V2RayServiceManager.stopVService(mActivity) + mActivity.lifecycleScope.launch { + try { + delay(500) + V2RayServiceManager.startVService(mActivity) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e) + } + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + return when (viewType) { + VIEW_TYPE_ITEM -> + MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + + else -> + FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + } + + override fun getItemViewType(position: Int): Int { + return if (position == mActivity.mainViewModel.serversCache.size) { + VIEW_TYPE_FOOTER + } else { + VIEW_TYPE_ITEM + } + } + + open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun onItemSelected() { + itemView.setBackgroundColor(Color.LTGRAY) + } + + fun onItemClear() { + itemView.setBackgroundColor(0) + } + } + + class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) : + BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder + + class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) : + BaseViewHolder(itemFooterBinding.root) + + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + mActivity.mainViewModel.swapServer(fromPosition, toPosition) + notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun onItemMoveCompleted() { + // do nothing + } + + override fun onItemDismiss(position: Int) { + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt new file mode 100644 index 00000000..b0df22e5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt @@ -0,0 +1,279 @@ +package com.v2ray.ang.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +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.toastSuccess +import com.v2ray.ang.extension.v2RayApplication +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.util.AppManagerUtil +import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.Utils +import es.dmoral.toasty.Toasty +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.text.Collator + +class PerAppProxyActivity : BaseActivity() { + private val binding by lazy { ActivityBypassListBinding.inflate(layoutInflater) } + + private var adapter: PerAppProxyAdapter? = null + private var appsAll: List? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + title = getString(R.string.per_app_proxy_settings) + + addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) + + lifecycleScope.launch { + try { + binding.pbWaiting.show() + val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET) + val apps = withContext(Dispatchers.IO) { + val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity) + + if (blacklist != null) { + appsList.forEach { app -> + app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0 + } + appsList.sortedWith { p1, p2 -> + when { + p1.isSelected > p2.isSelected -> -1 + p1.isSelected == p2.isSelected -> 0 + else -> 1 + } + } + } else { + val collator = Collator.getInstance() + appsList.sortedWith(compareBy(collator) { it.appName }) + } + } + + appsAll = apps + adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist) + binding.recyclerView.adapter = adapter + binding.pbWaiting.hide() + } catch (e: Exception) { + binding.pbWaiting.hide() + Log.e(ANG_PACKAGE, "Error loading apps", e) + } + } + + binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked -> + MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked) + } + binding.switchPerAppProxy.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false) + + binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked -> + MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked) + } + binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false) + + binding.layoutSwitchBypassAppsTips.setOnClickListener { + Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show() + } + } + + override fun onPause() { + super.onPause() + adapter?.let { + MmkvManager.encodeSettings(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 = false + + override fun onQueryTextChange(newText: String?): Boolean { + filterProxyApp(newText.orEmpty()) + return false + } + }) + } + + + return super.onCreateOptionsMenu(menu) + } + + + @SuppressLint("NotifyDataSetChanged") + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.select_all -> adapter?.let { it -> + val pkgNames = it.apps.map { it.packageName } + if (it.blacklist.containsAll(pkgNames)) { + it.apps.forEach { + val packageName = it.packageName + adapter?.blacklist?.remove(packageName) + } + } else { + it.apps.forEach { + val packageName = it.packageName + adapter?.blacklist?.add(packageName) + } + } + it.notifyDataSetChanged() + true + } == true + + 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) + } + + private fun selectProxyApp() { + toast(R.string.msg_downloading_content) + binding.pbWaiting.show() + + val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL + lifecycleScope.launch(Dispatchers.IO) { + var content = HttpUtil.getUrlContent(url, 5000) + if (content.isNullOrEmpty()) { + val httpPort = SettingsManager.getHttpPort() + content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: "" + } + launch(Dispatchers.Main) { + Log.i(AppConfig.TAG, content) + selectProxyApp(content, true) + toastSuccess(R.string.toast_success) + binding.pbWaiting.hide() + } + } + } + + private fun importProxyApp() { + val content = Utils.getClipboard(applicationContext) + if (TextUtils.isEmpty(content)) return + selectProxyApp(content, false) + toastSuccess(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) + toastSuccess(R.string.toast_success) + } + + @SuppressLint("NotifyDataSetChanged") + private fun selectProxyApp(content: String, force: Boolean): Boolean { + try { + val proxyApps = if (TextUtils.isEmpty(content)) { + Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt") + } else { + content + } + if (TextUtils.isEmpty(proxyApps)) return false + + adapter?.blacklist?.clear() + + if (binding.switchBypassApps.isChecked) { + adapter?.let { it -> + it.apps.forEach block@{ + val packageName = it.packageName + Log.i(AppConfig.TAG, packageName) + if (!inProxyApps(proxyApps, packageName, force)) { + adapter?.blacklist?.add(packageName) + println(packageName) + return@block + } + } + it.notifyDataSetChanged() + } + } else { + adapter?.let { it -> + it.apps.forEach block@{ + val packageName = it.packageName + Log.i(AppConfig.TAG, packageName) + if (inProxyApps(proxyApps, packageName, force)) { + adapter?.blacklist?.add(packageName) + println(packageName) + return@block + } + } + it.notifyDataSetChanged() + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Error selecting proxy app", e) + return false + } + 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() + + 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 + refreshData() + return true + } + + @SuppressLint("NotifyDataSetChanged") + fun refreshData() { + adapter?.notifyDataSetChanged() + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt similarity index 77% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt index ec7e1242..1d02b3b1 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyAdapter.kt @@ -1,17 +1,14 @@ package com.v2ray.ang.ui -import android.graphics.Color -import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.v2ray.ang.R +import androidx.recyclerview.widget.RecyclerView import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding import com.v2ray.ang.dto.AppInfo -import java.util.* class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, blacklist: MutableSet?) : - RecyclerView.Adapter() { + RecyclerView.Adapter() { companion object { private const val VIEW_TYPE_HEADER = 0 @@ -35,8 +32,10 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, bl return when (viewType) { 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) + view.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + 0 + ) BaseViewHolder(view) } // VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater @@ -52,29 +51,30 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, bl open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root), - View.OnClickListener { + View.OnClickListener { private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName) private lateinit var appInfo: AppInfo fun bind(appInfo: AppInfo) { this.appInfo = appInfo + // Set app icon and name itemBypassBinding.icon.setImageDrawable(appInfo.appIcon) -// name.text = appInfo.appName - - itemBypassBinding.checkBox.isChecked = inBlacklist - itemBypassBinding.packageName.text = appInfo.packageName - if (appInfo.isSystemApp) { - itemBypassBinding.name.text = String.format("** %1s", appInfo.appName) - itemBypassBinding.name.setTextColor(Color.RED) + itemBypassBinding.name.text = if (appInfo.isSystemApp) { + String.format("** %s", appInfo.appName) } else { - itemBypassBinding.name.text = appInfo.appName - itemBypassBinding.name.setTextColor(Color.DKGRAY) + appInfo.appName } + // Set package name and checkbox state + itemBypassBinding.packageName.text = appInfo.packageName + itemBypassBinding.checkBox.isChecked = inBlacklist + + // Handle item click to toggle blacklist status itemView.setOnClickListener(this) } + override fun onClick(v: View?) { if (inBlacklist) { blacklist.remove(appInfo.packageName) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt new file mode 100644 index 00000000..bb0ee572 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt @@ -0,0 +1,132 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityRoutingEditBinding +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class RoutingEditActivity : BaseActivity() { + private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) } + private val position by lazy { intent.getIntExtra("position", -1) } + + private val outbound_tag: Array by lazy { + resources.getStringArray(R.array.outbound_tag) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + title = getString(R.string.routing_settings_rule_title) + + val rulesetItem = SettingsManager.getRoutingRuleset(position) + if (rulesetItem != null) { + bindingServer(rulesetItem) + } else { + clearServer() + } + } + + private fun bindingServer(rulesetItem: RulesetItem): Boolean { + binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks) + binding.chkLocked.isChecked = rulesetItem.locked == true + binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(",")) + binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(",")) + binding.etPort.text = Utils.getEditable(rulesetItem.port) + binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(",")) + binding.etNetwork.text = Utils.getEditable(rulesetItem.network) + val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag) + binding.spOutboundTag.setSelection(outbound) + + return true + } + + private fun clearServer(): Boolean { + binding.etRemarks.text = null + binding.spOutboundTag.setSelection(0) + return true + } + + private fun saveServer(): Boolean { + val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem() + + rulesetItem.apply { + remarks = binding.etRemarks.text.toString() + locked = binding.chkLocked.isChecked + domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() } + ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } + ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() } + ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } + protocol = binding.etProtocol.text.toString().takeIf { it.isNotEmpty() } + ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } + port = binding.etPort.text.toString().takeIf { it.isNotEmpty() } + network = binding.etNetwork.text.toString().takeIf { it.isNotEmpty() } + outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition] + } + + if (rulesetItem.remarks.isNullOrEmpty()) { + toast(R.string.sub_setting_remarks) + return false + } + + SettingsManager.saveRoutingRuleset(position, rulesetItem) + toastSuccess(R.string.toast_success) + finish() + return true + } + + + private fun deleteServer(): Boolean { + if (position >= 0) { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + lifecycleScope.launch(Dispatchers.IO) { + SettingsManager.removeRoutingRuleset(position) + launch(Dispatchers.Main) { + finish() + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + // do nothing + } + .show() + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + val del_config = menu.findItem(R.id.del_config) + + if (position < 0) { + 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) + } + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt new file mode 100644 index 00000000..e585cff8 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt @@ -0,0 +1,204 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityRoutingSettingBinding +import com.v2ray.ang.dto.RulesetItem +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.helper.SimpleItemTouchHelperCallback +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class RoutingSettingActivity : BaseActivity() { + private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) } + + var rulesets: MutableList = mutableListOf() + private val adapter by lazy { RoutingSettingRecyclerAdapter(this) } + private var mItemTouchHelper: ItemTouchHelper? = null + private val routing_domain_strategy: Array by lazy { + resources.getStringArray(R.array.routing_domain_strategy) + } + private val preset_rulesets: Array by lazy { + resources.getStringArray(R.array.preset_rulesets) + } + + private val requestCameraPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java)) + } else { + toast(R.string.toast_permission_denied) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + title = getString(R.string.routing_settings_title) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) + binding.recyclerView.adapter = adapter + + mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) + mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) + + binding.tvDomainStrategySummary.text = getDomainStrategy() + binding.layoutDomainStrategy.setOnClickListener { + setDomainStrategy() + } + } + + override fun onResume() { + super.onResume() + refreshData() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_routing_setting, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true } + R.id.import_predefined_rulesets -> importPredefined().let { true } + R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true } + R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true } + R.id.export_rulesets_to_clipboard -> export2Clipboard().let { true } + else -> super.onOptionsItemSelected(item) + } + + private fun getDomainStrategy(): String { + return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first() + } + + private fun setDomainStrategy() { + android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i -> + try { + val value = routing_domain_strategy[i] + MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value) + binding.tvDomainStrategySummary.text = value + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to set domain strategy", e) + } + }.show() + } + + private fun importPredefined() { + AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i -> + AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) + .setPositiveButton(android.R.string.ok) { _, _ -> + try { + lifecycleScope.launch(Dispatchers.IO) { + SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i) + launch(Dispatchers.Main) { + refreshData() + toastSuccess(R.string.toast_success) + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e) + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do nothing + } + .show() + }.show() + } + + private fun importFromClipboard() { + AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) + .setPositiveButton(android.R.string.ok) { _, _ -> + val clipboard = try { + Utils.getClipboard(this) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to get clipboard content", e) + toastError(R.string.toast_failure) + return@setPositiveButton + } + lifecycleScope.launch(Dispatchers.IO) { + val result = SettingsManager.resetRoutingRulesets(clipboard) + withContext(Dispatchers.Main) { + if (result) { + refreshData() + toastSuccess(R.string.toast_success) + } else { + toastError(R.string.toast_failure) + } + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do nothing + } + .show() + } + + private fun export2Clipboard() { + val rulesetList = MmkvManager.decodeRoutingRulesets() + if (rulesetList.isNullOrEmpty()) { + toastError(R.string.toast_failure) + } else { + Utils.setClipboard(this, JsonUtil.toJson(rulesetList)) + toastSuccess(R.string.toast_success) + } + } + + private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + importRulesetsFromQRcode(it.data?.getStringExtra("SCAN_RESULT")) + } + } + + private fun importRulesetsFromQRcode(qrcode: String?): Boolean { + AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) + .setPositiveButton(android.R.string.ok) { _, _ -> + lifecycleScope.launch(Dispatchers.IO) { + val result = SettingsManager.resetRoutingRulesets(qrcode) + withContext(Dispatchers.Main) { + if (result) { + refreshData() + toastSuccess(R.string.toast_success) + } else { + toastError(R.string.toast_failure) + } + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do nothing + } + .show() + return true + } + + @SuppressLint("NotifyDataSetChanged") + fun refreshData() { + rulesets.clear() + rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf()) + adapter.notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt new file mode 100644 index 00000000..bd48ea59 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingRecyclerAdapter.kt @@ -0,0 +1,80 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.databinding.ItemRecyclerRoutingSettingBinding +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.helper.ItemTouchHelperAdapter +import com.v2ray.ang.helper.ItemTouchHelperViewHolder + +class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : RecyclerView.Adapter(), + ItemTouchHelperAdapter { + + private var mActivity: RoutingSettingActivity = activity + override fun getItemCount() = mActivity.rulesets.size + + override fun onBindViewHolder(holder: MainViewHolder, position: Int) { + val ruleset = mActivity.rulesets[position] + + holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks + holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString() + holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag + holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled + holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true + holder.itemView.setBackgroundColor(Color.TRANSPARENT) + + holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener { + mActivity.startActivity( + Intent(mActivity, RoutingEditActivity::class.java) + .putExtra("position", position) + ) + } + + holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> + if (!it.isPressed) return@setOnCheckedChangeListener + ruleset.enabled = isChecked + SettingsManager.saveRoutingRuleset(position, ruleset) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { + return MainViewHolder( + ItemRecyclerRoutingSettingBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + class MainViewHolder(val itemRoutingSettingBinding: ItemRecyclerRoutingSettingBinding) : + BaseViewHolder(itemRoutingSettingBinding.root), ItemTouchHelperViewHolder + + open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun onItemSelected() { + itemView.setBackgroundColor(Color.LTGRAY) + } + + fun onItemClear() { + itemView.setBackgroundColor(0) + } + } + + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + SettingsManager.swapRoutingRuleset(fromPosition, toPosition) + notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun onItemMoveCompleted() { + mActivity.refreshData() + } + + override fun onItemDismiss(position: Int) { + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt new file mode 100644 index 00000000..9a972f93 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt @@ -0,0 +1,52 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.content.Intent +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts +import com.v2ray.ang.R +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.AngConfigManager + +class ScScannerActivity : BaseActivity() { + + private val requestCameraPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + scanQRCode.launch(Intent(this, ScannerActivity::class.java)) + } else { + toast(R.string.toast_permission_denied) + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_none) + importQRcode() + } + + private fun importQRcode(): Boolean { + requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) + return true + } + + private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty() + val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false) + + if (count + countSub > 0) { + toastSuccess(R.string.toast_success) + } else { + toastError(R.string.toast_failure) + } + + startActivity(Intent(this, MainActivity::class.java)) + } + finish() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt similarity index 69% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt index 3d3101ef..0495318a 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt @@ -1,8 +1,7 @@ package com.v2ray.ang.ui -import com.v2ray.ang.R -import com.v2ray.ang.util.Utils import android.os.Bundle +import com.v2ray.ang.R import com.v2ray.ang.service.V2RayServiceManager class ScSwitchActivity : BaseActivity() { @@ -12,10 +11,10 @@ class ScSwitchActivity : BaseActivity() { setContentView(R.layout.activity_none) - if (V2RayServiceManager.v2rayPoint.isRunning) { - Utils.stopVService(this) + if (V2RayServiceManager.isRunning()) { + V2RayServiceManager.stopVService(this) } else { - Utils.startVServiceFromToggle(this) + V2RayServiceManager.startVServiceFromToggle(this) } finish() } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt new file mode 100644 index 00000000..2a971ac0 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt @@ -0,0 +1,134 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.BitmapFactory +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.extension.toast +import com.v2ray.ang.handler.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() { + + + private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult) + private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val uri = it.data?.data + if (it.resultCode == RESULT_OK && uri != null) { + try { + val inputStream = contentResolver.openInputStream(uri) + val bitmap = BitmapFactory.decodeStream(inputStream) + inputStream?.close() + + val text = QRCodeDecoder.syncDecodeQRCode(bitmap) + if (text.isNullOrEmpty()) { + toast(R.string.toast_decoding_failed) + } else { + finished(text) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to decode QR code from file", e) + toast(R.string.toast_decoding_failed) + } + } + } + + private val requestPermissionLauncher = + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + showFileChooser() + } else { + toast(R.string.toast_permission_denied) + } + } + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) { + launchScan() + } + } + + 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 + } + ) + } + + private fun handleResult(result: QRResult) { + if (result is QRResult.QRSuccess) { + finished(result.content.rawValue.orEmpty()) + } else { + finish() + } + } + + private fun finished(text: String) { + val intent = Intent() + intent.putExtra("SCAN_RESULT", text) + setResult(RESULT_OK, intent) + finish() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_scanner, menu) + return true + } + + 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 + } + + if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + showFileChooser() + } else { + requestPermissionLauncher.launch(permission) + } + true + } + + + else -> super.onOptionsItemSelected(item) + } + + private fun showFileChooser() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + + try { + chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) + } catch (ex: android.content.ActivityNotFoundException) { + toast(R.string.toast_require_file_manager) + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt new file mode 100644 index 00000000..e9bdad66 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt @@ -0,0 +1,652 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +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.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.DEFAULT_PORT +import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE +import com.v2ray.ang.AppConfig.REALITY +import com.v2ray.ang.AppConfig.TLS +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 +import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU +import com.v2ray.ang.R +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.NetworkType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.extension.isNotNullEmpty +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.util.Utils + +class ServerActivity : BaseActivity() { + + private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() } + private val isRunning by lazy { + intent.getBooleanExtra("isRunning", false) + && editGuid.isNotEmpty() + && editGuid == MmkvManager.getSelectServer() + } + private val createConfigType by lazy { + EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) + ?: EConfigType.VMESS + } + private val subscriptionId by lazy { + intent.getStringExtra("subscriptionId") + } + + private val securitys: Array by lazy { + resources.getStringArray(R.array.securitys) + } + private val shadowsocksSecuritys: Array by lazy { + resources.getStringArray(R.array.ss_securitys) + } + private val flows: Array by lazy { + resources.getStringArray(R.array.flows) + } + private val networks: Array by lazy { + resources.getStringArray(R.array.networks) + } + private val tcpTypes: Array by lazy { + resources.getStringArray(R.array.header_type_tcp) + } + private val kcpAndQuicTypes: Array by lazy { + resources.getStringArray(R.array.header_type_kcp_and_quic) + } + private val grpcModes: Array by lazy { + resources.getStringArray(R.array.mode_type_grpc) + } + private val streamSecuritys: Array by lazy { + resources.getStringArray(R.array.streamsecurityxs) + } + private val allowinsecures: Array by lazy { + resources.getStringArray(R.array.allowinsecures) + } + private val uTlsItems: Array by lazy { + resources.getStringArray(R.array.streamsecurity_utls) + } + private val alpns: Array by lazy { + resources.getStringArray(R.array.streamsecurity_alpn) + } + private val xhttpMode: Array by lazy { + resources.getStringArray(R.array.xhttp_mode) + } + + + // 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_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.lay_allow_insecure) } + private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) } + private val container_sni: LinearLayout? by lazy { findViewById(R.id.lay_sni) } + 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.lay_stream_fingerprint) } + 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.lay_stream_alpn) } + private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) } + private val et_preshared_key: EditText? by lazy { findViewById(R.id.et_preshared_key) } + private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) } + 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.lay_short_id) } + 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.lay_spider_x) } + private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) } + 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) } + private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) } + private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) } + private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) } + private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) } + private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) } + private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) } + private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) } + private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + title = getString(R.string.title_server) + + val config = MmkvManager.decodeServerConfig(editGuid) + 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.HTTP -> setContentView(R.layout.activity_server_socks) + EConfigType.VLESS -> setContentView(R.layout.activity_server_vless) + EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan) + EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard) + EConfigType.HYSTERIA2 -> setContentView(R.layout.activity_server_hysteria2) + } + sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + 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) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + sp_header_type?.adapter = adapter + sp_header_type_title?.text = + when (networks[position]) { + NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type) + NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode) + else -> getString(R.string.server_lab_head_type) + }.orEmpty() + sp_header_type?.setSelection( + Utils.arrayFind( + types, + when (networks[position]) { + NetworkType.GRPC.type -> config?.mode + NetworkType.XHTTP.type -> config?.xhttpMode + else -> config?.headerType + }.orEmpty() + ) + ) + + et_request_host?.text = Utils.getEditable( + when (networks[position]) { + //"quic" -> config?.quicSecurity + NetworkType.GRPC.type -> config?.authority + else -> config?.host + }.orEmpty() + ) + et_path?.text = Utils.getEditable( + when (networks[position]) { + NetworkType.KCP.type -> config?.seed + //"quic" -> config?.quicKey + NetworkType.GRPC.type -> config?.serviceName + else -> config?.path + }.orEmpty() + ) + + tv_request_host?.text = Utils.getEditable( + getString( + when (networks[position]) { + NetworkType.TCP.type -> R.string.server_lab_request_host_http + NetworkType.WS.type -> R.string.server_lab_request_host_ws + NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade + NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp + NetworkType.H2.type -> R.string.server_lab_request_host_h2 + //"quic" -> R.string.server_lab_request_host_quic + NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc + else -> R.string.server_lab_request_host + } + ) + ) + + tv_path?.text = Utils.getEditable( + getString( + when (networks[position]) { + NetworkType.KCP.type -> R.string.server_lab_path_kcp + NetworkType.WS.type -> R.string.server_lab_path_ws + NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade + NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp + NetworkType.H2.type -> R.string.server_lab_path_h2 + //"quic" -> R.string.server_lab_path_quic + NetworkType.GRPC.type -> R.string.server_lab_path_grpc + else -> R.string.server_lab_path + } + ) + ) + et_extra?.text = Utils.getEditable( + when (networks[position]) { + NetworkType.XHTTP.type -> config?.xhttpExtra + else -> null + }.orEmpty() + ) + + layout_extra?.visibility = + when (networks[position]) { + NetworkType.XHTTP.type -> View.VISIBLE + else -> View.GONE + } + } + + 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, + ) { + val isBlank = streamSecuritys[position].isBlank() + val isTLS = streamSecuritys[position] == TLS + + when { + // Case 1: Null or blank + isBlank -> { + listOf( + container_sni, container_fingerprint, container_alpn, + container_allow_insecure, container_public_key, + container_short_id, container_spider_x + ).forEach { it?.visibility = View.GONE } + } + + // Case 2: TLS value + isTLS -> { + listOf( + container_sni, + container_fingerprint, + container_alpn + ).forEach { it?.visibility = View.VISIBLE } + container_allow_insecure?.visibility = View.VISIBLE + listOf( + container_public_key, + container_short_id, + container_spider_x + ).forEach { it?.visibility = View.GONE } + } + + // Case 3: Other reality values + else -> { + listOf(container_sni, container_fingerprint).forEach { + it?.visibility = View.VISIBLE + } + container_alpn?.visibility = View.GONE + container_allow_insecure?.visibility = View.GONE + listOf( + container_public_key, + container_short_id, + container_spider_x + ).forEach { it?.visibility = View.VISIBLE } + } + } + } + + override fun onNothingSelected(p0: AdapterView<*>?) { + // do nothing + } + } + if (config != null) { + bindingServer(config) + } else { + clearServer() + } + } + + /** + * binding selected server config + */ + private fun bindingServer(config: ProfileItem): Boolean { + + et_remarks.text = Utils.getEditable(config.remarks) + et_address.text = Utils.getEditable(config.server.orEmpty()) + et_port.text = Utils.getEditable(config.serverPort ?: DEFAULT_PORT.toString()) + et_id.text = Utils.getEditable(config.password.orEmpty()) + + if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) { + et_security?.text = Utils.getEditable(config.username.orEmpty()) + } else if (config.configType == EConfigType.VLESS) { + et_security?.text = Utils.getEditable(config.method.orEmpty()) + val flow = Utils.arrayFind(flows, config.flow.orEmpty()) + if (flow >= 0) { + sp_flow?.setSelection(flow) + } + } else if (config.configType == EConfigType.WIREGUARD) { + et_id.text = Utils.getEditable(config.secretKey.orEmpty()) + et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty()) + et_preshared_key?.visibility = View.VISIBLE + et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty()) + et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0") + et_local_address?.text = Utils.getEditable( + config.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4 + ) + et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU) + } else if (config.configType == EConfigType.HYSTERIA2) { + et_obfs_password?.text = Utils.getEditable(config.obfsPassword) + et_port_hop?.text = Utils.getEditable(config.portHopping) + et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval) + et_pinsha256?.text = Utils.getEditable(config.pinSHA256) + et_bandwidth_down?.text = Utils.getEditable(config.bandwidthDown) + et_bandwidth_up?.text = Utils.getEditable(config.bandwidthUp) + } + val securityEncryptions = + if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys + val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty()) + if (security >= 0) { + sp_security?.setSelection(security) + } + + val streamSecurity = Utils.arrayFind(streamSecuritys, config.security.orEmpty()) + if (streamSecurity >= 0) { + sp_stream_security?.setSelection(streamSecurity) + container_sni?.visibility = View.VISIBLE + container_fingerprint?.visibility = View.VISIBLE + container_alpn?.visibility = View.VISIBLE + + et_sni?.text = Utils.getEditable(config.sni) + config.fingerPrint?.let { it -> + val utlsIndex = Utils.arrayFind(uTlsItems, it) + utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) } + } + config.alpn?.let { it -> + val alpnIndex = Utils.arrayFind(alpns, it) + alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) } + } + if (config.security == TLS) { + container_allow_insecure?.visibility = View.VISIBLE + val allowinsecure = Utils.arrayFind(allowinsecures, config.insecure.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 if (config.security == REALITY) { + container_public_key?.visibility = View.VISIBLE + et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty()) + container_short_id?.visibility = View.VISIBLE + et_short_id?.text = Utils.getEditable(config.shortId.orEmpty()) + container_spider_x?.visibility = View.VISIBLE + et_spider_x?.text = Utils.getEditable(config.spiderX.orEmpty()) + container_allow_insecure?.visibility = View.GONE + } + } + + if (config.security.isNullOrEmpty()) { + 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, config.network.orEmpty()) + if (network >= 0) { + sp_network?.setSelection(network) + } + return true + } + + /** + * clear or init server config + */ + private fun clearServer(): Boolean { + et_remarks.text = null + et_address.text = null + et_port.text = Utils.getEditable(DEFAULT_PORT.toString()) + et_id.text = null + sp_security?.setSelection(0) + sp_network?.setSelection(0) + + sp_header_type?.setSelection(0) + et_request_host?.text = null + 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) + et_public_key?.text = null + et_reserved1?.text = Utils.getEditable("0,0,0") + et_local_address?.text = + Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4) + et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU) + return true + } + + /** + * save server config + */ + private fun saveServer(): Boolean { + if (TextUtils.isEmpty(et_remarks.text.toString())) { + toast(R.string.server_lab_remarks) + return false + } + if (TextUtils.isEmpty(et_address.text.toString())) { + toast(R.string.server_lab_address) + return false + } + if (createConfigType != EConfigType.HYSTERIA2) { + if (Utils.parseInt(et_port.text.toString()) <= 0) { + toast(R.string.server_lab_port) + return false + } + } + val config = + MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType) + if (config.configType != EConfigType.SOCKS + && config.configType != EConfigType.HTTP + && TextUtils.isEmpty(et_id.text.toString()) + ) { + if (config.configType == EConfigType.TROJAN + || config.configType == EConfigType.SHADOWSOCKS + || config.configType == EConfigType.HYSTERIA2 + ) { + 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 + } + } + if (et_extra?.text?.toString().isNotNullEmpty()) { + if (JsonUtil.parseString(et_extra?.text?.toString()) == null) { + toast(R.string.server_lab_xhttp_extra) + return false + } + } + + saveCommon(config) + saveStreamSettings(config) + saveTls(config) + + if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { + config.subscriptionId = subscriptionId.orEmpty() + } + //Log.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "") + MmkvManager.encodeServerConfig(editGuid, config) + toastSuccess(R.string.toast_success) + finish() + return true + } + + private fun saveCommon(config: ProfileItem) { + config.remarks = et_remarks.text.toString().trim() + config.server = et_address.text.toString().trim() + config.serverPort = et_port.text.toString().trim() + config.password = et_id.text.toString().trim() + + if (config.configType == EConfigType.VMESS) { + config.method = securitys[sp_security?.selectedItemPosition ?: 0] + } else if (config.configType == EConfigType.VLESS) { + config.method = et_security?.text.toString().trim() + config.flow = flows[sp_flow?.selectedItemPosition ?: 0] + } else if (config.configType == EConfigType.SHADOWSOCKS) { + config.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0] + } else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) { + if (!TextUtils.isEmpty(et_security?.text) || !TextUtils.isEmpty(et_id.text)) { + config.username = et_security?.text.toString().trim() + } + } else if (config.configType == EConfigType.TROJAN) { + } else if (config.configType == EConfigType.WIREGUARD) { + config.secretKey = et_id.text.toString().trim() + config.publicKey = et_public_key?.text.toString().trim() + config.preSharedKey = et_preshared_key?.text.toString().trim() + config.reserved = et_reserved1?.text.toString().trim() + config.localAddress = et_local_address?.text.toString().trim() + config.mtu = Utils.parseInt(et_local_mtu?.text.toString()) + } else if (config.configType == EConfigType.HYSTERIA2) { + config.obfsPassword = et_obfs_password?.text?.toString() + config.portHopping = et_port_hop?.text?.toString() + config.portHoppingInterval = et_port_hop_interval?.text?.toString() + config.pinSHA256 = et_pinsha256?.text?.toString() + config.bandwidthDown = et_bandwidth_down?.text?.toString() + config.bandwidthUp = et_bandwidth_up?.text?.toString() + } + } + + + private fun saveStreamSettings(profileItem: ProfileItem) { + 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 + + profileItem.network = networks[network] + profileItem.headerType = transportTypes(networks[network])[type] + profileItem.host = requestHost + profileItem.path = path + profileItem.seed = path + profileItem.quicSecurity = requestHost + profileItem.quicKey = path + profileItem.mode = transportTypes(networks[network])[type] + profileItem.serviceName = path + profileItem.authority = requestHost + profileItem.xhttpMode = transportTypes(networks[network])[type] + profileItem.xhttpExtra = et_extra?.text?.toString()?.trim() + } + + private fun saveTls(config: ProfileItem) { + val streamSecurity = sp_stream_security?.selectedItemPosition ?: return + val sniField = et_sni?.text?.toString()?.trim() + val allowInsecureField = sp_allow_insecure?.selectedItemPosition + val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: 0 + val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: 0 + val publicKey = et_public_key?.text?.toString() + val shortId = et_short_id?.text?.toString() + val spiderX = et_spider_x?.text?.toString() + + val allowInsecure = + if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) { + MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE) + } else { + allowinsecures[allowInsecureField].toBoolean() + } + + config.security = streamSecuritys[streamSecurity] + config.insecure = allowInsecure + config.sni = sniField + config.fingerPrint = uTlsItems[utlsIndex] + config.alpn = alpns[alpnIndex] + config.publicKey = publicKey + config.shortId = shortId + config.spiderX = spiderX + } + + private fun transportTypes(network: String?): Array { + return when (network) { + NetworkType.TCP.type -> { + tcpTypes + } + + NetworkType.KCP.type -> { + kcpAndQuicTypes + } + + NetworkType.GRPC.type -> { + grpcModes + } + + NetworkType.XHTTP.type -> { + xhttpMode + } + + else -> { + arrayOf("---") + } + } + } + + /** + * delete server config + */ + private fun deleteServer(): Boolean { + if (editGuid.isNotEmpty()) { + if (editGuid != MmkvManager.getSelectServer()) { + if (MmkvManager.decodeSettingsBool(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.cancel) { _, _ -> + // do nothing + } + .show() + } else { + MmkvManager.removeServer(editGuid) + finish() + } + } else { + application.toast(R.string.toast_action_not_allowed) + } + } + return true + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.action_server, menu) + val delButton = menu.findItem(R.id.del_config) + val saveButton = menu.findItem(R.id.save_config) + + if (editGuid.isNotEmpty()) { + if (isRunning) { + delButton?.isVisible = false + saveButton?.isVisible = false + } + } else { + delButton?.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) + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt similarity index 59% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt index a38c1783..44e0b32b 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerCustomConfigActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt @@ -2,42 +2,41 @@ package com.v2ray.ang.ui import android.os.Bundle import android.text.TextUtils +import android.util.Log 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.tencent.mmkv.MMKV +import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.V2rayConfig +import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.fmt.CustomFmt +import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.Utils -import me.drakeet.support.toast.ToastCompat class ServerCustomConfigActivity : BaseActivity() { - private lateinit var binding: ActivityServerCustomConfigBinding + private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) } - 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 editGuid by lazy { intent.getStringExtra("guid").orEmpty() } private val isRunning by lazy { intent.getBooleanExtra("isRunning", false) && editGuid.isNotEmpty() - && editGuid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) + && editGuid == MmkvManager.getSelectServer() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityServerCustomConfigBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) + setContentView(binding.root) 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,20 +44,17 @@ class ServerCustomConfigActivity : BaseActivity() { } else { clearServer() } - supportActionBar?.setDisplayHomeAsUpEnabled(true) } /** - * bingding seleced server config + * Binding selected server config */ - private fun bindingServer(config: ServerConfig): Boolean { + private fun bindingServer(config: ProfileItem): Boolean { binding.etRemarks.text = Utils.getEditable(config.remarks) - val raw = serverRawStorage?.decodeString(editGuid) - if (raw.isNullOrBlank()) { - binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty())) - } else { - binding.editor.setTextContent(Utils.getEditable(raw)) - } + val raw = MmkvManager.decodeServerRaw(editGuid) + val configContent = raw.orEmpty() + + binding.editor.setTextContent(Utils.getEditable(configContent)) return true } @@ -79,21 +75,24 @@ class ServerCustomConfigActivity : BaseActivity() { return false } - val v2rayConfig = try { - Gson().fromJson(binding.editor.text.toString(), V2rayConfig::class.java) + val profileItem = try { + CustomFmt.parse(binding.editor.text.toString()) } catch (e: Exception) { - e.printStackTrace() - ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show() + Log.e(AppConfig.TAG, "Failed to parse custom configuration", e) + toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}") return false } - val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM) - config.remarks = binding.etRemarks.text.toString().trim() - config.fullConfig = v2rayConfig + val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM) + binding.etRemarks.text.let { + config.remarks = if (it.isNullOrEmpty()) profileItem?.remarks.orEmpty() else it.toString() + } + config.server = profileItem?.server + config.serverPort = profileItem?.serverPort MmkvManager.encodeServerConfig(editGuid, config) - serverRawStorage?.encode(editGuid, binding.editor.text.toString()) - toast(R.string.toast_success) + MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) + toastSuccess(R.string.toast_success) finish() return true } @@ -104,11 +103,14 @@ class ServerCustomConfigActivity : 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() + .setPositiveButton(android.R.string.ok) { _, _ -> + MmkvManager.removeServer(editGuid) + finish() + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + // do nothing + } + .show() } return true } @@ -135,10 +137,12 @@ class ServerCustomConfigActivity : BaseActivity() { deleteServer() true } + R.id.save_config -> { saveServer() true } + else -> super.onOptionsItemSelected(item) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt new file mode 100644 index 00000000..6af64e3a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt @@ -0,0 +1,372 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import androidx.activity.viewModels +import androidx.preference.CheckBoxPreference +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.PreferenceFragmentCompat +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.multiprocess.RemoteWorkManager +import com.v2ray.ang.AngApplication +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.VPN +import com.v2ray.ang.R +import com.v2ray.ang.extension.toLongEx +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.service.SubscriptionUpdater +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() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + title = getString(R.string.title_settings) + + settingsViewModel.startListenPreferenceChange() + } + + class SettingsFragment : PreferenceFragmentCompat() { + + private val perAppProxy by lazy { findPreference(AppConfig.PREF_PER_APP_PROXY) } + private val localDns by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_ENABLED) } + private val fakeDns by lazy { findPreference(AppConfig.PREF_FAKE_DNS_ENABLED) } + private val appendHttpProxy by lazy { findPreference(AppConfig.PREF_APPEND_HTTP_PROXY) } + private val localDnsPort by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_PORT) } + private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) } + private val vpnBypassLan by lazy { findPreference(AppConfig.PREF_VPN_BYPASS_LAN) } + private val vpnInterfaceAddress by lazy { findPreference(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) } + + private val mux by lazy { findPreference(AppConfig.PREF_MUX_ENABLED) } + private val muxConcurrency by lazy { findPreference(AppConfig.PREF_MUX_CONCURRENCY) } + private val muxXudpConcurrency by lazy { findPreference(AppConfig.PREF_MUX_XUDP_CONCURRENCY) } + private val muxXudpQuic by lazy { findPreference(AppConfig.PREF_MUX_XUDP_QUIC) } + + private val fragment by lazy { findPreference(AppConfig.PREF_FRAGMENT_ENABLED) } + private val fragmentPackets by lazy { findPreference(AppConfig.PREF_FRAGMENT_PACKETS) } + private val fragmentLength by lazy { findPreference(AppConfig.PREF_FRAGMENT_LENGTH) } + private val fragmentInterval by lazy { findPreference(AppConfig.PREF_FRAGMENT_INTERVAL) } + + private val autoUpdateCheck by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE) } + private val autoUpdateInterval by lazy { findPreference(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) } + + private val socksPort by lazy { findPreference(AppConfig.PREF_SOCKS_PORT) } + private val remoteDns by lazy { findPreference(AppConfig.PREF_REMOTE_DNS) } + private val domesticDns by lazy { findPreference(AppConfig.PREF_DOMESTIC_DNS) } + private val dnsHosts by lazy { findPreference(AppConfig.PREF_DNS_HOSTS) } + private val delayTestUrl by lazy { findPreference(AppConfig.PREF_DELAY_TEST_URL) } + private val mode by lazy { findPreference(AppConfig.PREF_MODE) } + + override fun onCreatePreferences(bundle: Bundle?, s: String?) { + addPreferencesFromResource(R.xml.pref_settings) + + perAppProxy?.setOnPreferenceClickListener { + startActivity(Intent(activity, PerAppProxyActivity::class.java)) + perAppProxy?.isChecked = true + false + } + localDns?.setOnPreferenceChangeListener { _, any -> + updateLocalDns(any as Boolean) + true + } + localDnsPort?.setOnPreferenceChangeListener { _, any -> + val nval = any as String + localDnsPort?.summary = + if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval + true + } + vpnDns?.setOnPreferenceChangeListener { _, any -> + vpnDns?.summary = any as String + true + } + + 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?.toLongEx()?.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.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval + autoUpdateInterval?.summary = nval + configureUpdateTask(nval.toLongEx()) + true + } + + socksPort?.setOnPreferenceChangeListener { _, any -> + val nval = any as String + socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS 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 + } + dnsHosts?.setOnPreferenceChangeListener { _, any -> + val nval = any as String + dnsHosts?.summary = nval + true + } + delayTestUrl?.setOnPreferenceChangeListener { _, any -> + val nval = any as String + delayTestUrl?.summary = if (nval == "") AppConfig.DELAY_TEST_URL else nval + true + } + mode?.setOnPreferenceChangeListener { _, newValue -> + updateMode(newValue.toString()) + true + } + mode?.dialogLayoutResource = R.layout.preference_with_help_link + //loglevel.summary = "LogLevel" + + } + + override fun onStart() { + super.onStart() + updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN)) + localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false) + fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false) + appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false) + localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS) + vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN) + + updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)) + mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false) + muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8") + muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8") + + updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)) + fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) + fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello") + fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100") + fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20") + + autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) + autoUpdateInterval?.summary = + MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL) + autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) + + socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS) + remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY) + domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) + dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS) + delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL) + + initSharedPreference() + } + + private fun initSharedPreference() { + listOf( + localDnsPort, + vpnDns, + muxConcurrency, + muxXudpConcurrency, + fragmentLength, + fragmentInterval, + autoUpdateInterval, + socksPort, + remoteDns, + domesticDns, + delayTestUrl + ).forEach { key -> + key?.text = key?.summary.toString() + } + + listOf( + AppConfig.PREF_SNIFFING_ENABLED, + ).forEach { key -> + findPreference(key)?.isChecked = + MmkvManager.decodeSettingsBool(key, true) + } + + listOf( + AppConfig.PREF_ROUTE_ONLY_ENABLED, + AppConfig.PREF_IS_BOOTED, + AppConfig.PREF_BYPASS_APPS, + AppConfig.PREF_SPEED_ENABLED, + AppConfig.PREF_CONFIRM_REMOVE, + AppConfig.PREF_START_SCAN_IMMEDIATE, + AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, + AppConfig.PREF_PREFER_IPV6, + AppConfig.PREF_PROXY_SHARING, + AppConfig.PREF_ALLOW_INSECURE + ).forEach { key -> + findPreference(key)?.isChecked = + MmkvManager.decodeSettingsBool(key, false) + } + + listOf( + AppConfig.PREF_VPN_BYPASS_LAN, + AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, + AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, + AppConfig.PREF_MUX_XUDP_QUIC, + AppConfig.PREF_FRAGMENT_PACKETS, + AppConfig.PREF_LANGUAGE, + AppConfig.PREF_UI_MODE_NIGHT, + AppConfig.PREF_LOGLEVEL, + AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, + AppConfig.PREF_MODE + ).forEach { key -> + if (MmkvManager.decodeSettingsString(key) != null) { + findPreference(key)?.value = MmkvManager.decodeSettingsString(key) + } + } + } + + private fun updateMode(mode: String?) { + val vpn = mode == VPN + perAppProxy?.isEnabled = vpn + perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false) + localDns?.isEnabled = vpn + fakeDns?.isEnabled = vpn + appendHttpProxy?.isEnabled = vpn + localDnsPort?.isEnabled = vpn + vpnDns?.isEnabled = vpn + vpnBypassLan?.isEnabled = vpn + vpnInterfaceAddress?.isEnabled = vpn + if (vpn) { + updateLocalDns( + MmkvManager.decodeSettingsBool( + AppConfig.PREF_LOCAL_DNS_ENABLED, + false + ) + ) + } + } + + private fun updateLocalDns(enabled: Boolean) { + fakeDns?.isEnabled = enabled + 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(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")) + updateMuxXudpConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")) + } + } + + private fun updateMuxConcurrency(value: String?) { + 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(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")) + updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")) + updateFragmentInterval(MmkvManager.decodeSettingsString(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) { + Utils.openUri(this, AppConfig.APP_WIKI_MODE) + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt new file mode 100644 index 00000000..f85382f1 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt @@ -0,0 +1,165 @@ +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 androidx.lifecycle.lifecycleScope +import com.v2ray.ang.AppConfig +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.extension.toastSuccess +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class SubEditActivity : BaseActivity() { + private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) } + + private var del_config: MenuItem? = null + private var save_config: MenuItem? = null + + private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + title = getString(R.string.title_sub_setting) + + val subItem = MmkvManager.decodeSubscription(editSubId) + if (subItem != null) { + bindingServer(subItem) + } else { + clearServer() + } + } + + /** + * binding selected server config + */ + private fun bindingServer(subItem: SubscriptionItem): Boolean { + binding.etRemarks.text = Utils.getEditable(subItem.remarks) + binding.etUrl.text = Utils.getEditable(subItem.url) + binding.etFilter.text = Utils.getEditable(subItem.filter) + binding.chkEnable.isChecked = subItem.enabled + binding.autoUpdateCheck.isChecked = subItem.autoUpdate + binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl + binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile) + binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile) + return true + } + + /** + * clear or init server config + */ + private fun clearServer(): Boolean { + binding.etRemarks.text = null + binding.etUrl.text = null + binding.etFilter.text = null + binding.chkEnable.isChecked = true + binding.etPreProfile.text = null + binding.etNextProfile.text = null + return true + } + + /** + * save server config + */ + private fun saveServer(): Boolean { + val subItem = MmkvManager.decodeSubscription(editSubId) ?: SubscriptionItem() + + subItem.remarks = binding.etRemarks.text.toString() + subItem.url = binding.etUrl.text.toString() + subItem.filter = binding.etFilter.text.toString() + subItem.enabled = binding.chkEnable.isChecked + subItem.autoUpdate = binding.autoUpdateCheck.isChecked + subItem.prevProfile = binding.etPreProfile.text.toString() + subItem.nextProfile = binding.etNextProfile.text.toString() + subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked + + if (TextUtils.isEmpty(subItem.remarks)) { + toast(R.string.sub_setting_remarks) + return false + } + if (subItem.url.isNotEmpty()) { + if (!Utils.isValidUrl(subItem.url)) { + toast(R.string.toast_invalid_url) + return false + } + + if (!Utils.isValidSubUrl(subItem.url)) { + toast(R.string.toast_insecure_url_protocol) + if (!subItem.allowInsecureUrl) { + return false + } + } + } + + MmkvManager.encodeSubscription(editSubId, subItem) + toastSuccess(R.string.toast_success) + finish() + return true + } + + /** + * save server config + */ + private fun deleteServer(): Boolean { + if (editSubId.isNotEmpty()) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { + AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + lifecycleScope.launch(Dispatchers.IO) { + MmkvManager.removeSubscription(editSubId) + launch(Dispatchers.Main) { + finish() + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + // do nothing + } + .show() + } else { + lifecycleScope.launch(Dispatchers.IO) { + MmkvManager.removeSubscription(editSubId) + launch(Dispatchers.Main) { + finish() + } + } + } + } + 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 (editSubId.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) + } + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt new file mode 100644 index 00000000..ef27fcff --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt @@ -0,0 +1,89 @@ +package com.v2ray.ang.ui + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivitySubSettingBinding +import com.v2ray.ang.dto.SubscriptionItem +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.helper.SimpleItemTouchHelperCallback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class SubSettingActivity : BaseActivity() { + private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) } + + var subscriptions: List> = listOf() + private val adapter by lazy { SubSettingRecyclerAdapter(this) } + private var mItemTouchHelper: ItemTouchHelper? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + title = getString(R.string.title_sub_setting) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) + binding.recyclerView.adapter = adapter + + mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) + mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) + } + + override fun onResume() { + super.onResume() + refreshData() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.action_sub_setting, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { + R.id.add_config -> { + startActivity(Intent(this, SubEditActivity::class.java)) + true + } + + R.id.sub_update -> { + binding.pbWaiting.show() + + lifecycleScope.launch(Dispatchers.IO) { + val count = AngConfigManager.updateConfigViaSubAll() + delay(500L) + launch(Dispatchers.Main) { + if (count > 0) { + toastSuccess(R.string.toast_success) + } else { + toastError(R.string.toast_failure) + } + binding.pbWaiting.hide() + } + } + + true + } + + else -> super.onOptionsItemSelected(item) + + } + + @SuppressLint("NotifyDataSetChanged") + fun refreshData() { + subscriptions = MmkvManager.decodeSubscriptions() + adapter.notifyDataSetChanged() + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt new file mode 100644 index 00000000..cc2d5404 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt @@ -0,0 +1,163 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.graphics.Color +import android.text.TextUtils +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.AppConfig +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.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.helper.ItemTouchHelperAdapter +import com.v2ray.ang.helper.ItemTouchHelperViewHolder +import com.v2ray.ang.util.QRCodeDecoder +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { + + private var mActivity: SubSettingActivity = activity + + private val share_method: Array by lazy { + mActivity.resources.getStringArray(R.array.share_sub_method) + } + + override fun getItemCount() = mActivity.subscriptions.size + + override fun onBindViewHolder(holder: MainViewHolder, position: Int) { + val subId = mActivity.subscriptions[position].first + val subItem = mActivity.subscriptions[position].second + holder.itemSubSettingBinding.tvName.text = subItem.remarks + holder.itemSubSettingBinding.tvUrl.text = subItem.url + holder.itemSubSettingBinding.chkEnable.isChecked = subItem.enabled + holder.itemView.setBackgroundColor(Color.TRANSPARENT) + + holder.itemSubSettingBinding.layoutEdit.setOnClickListener { + mActivity.startActivity( + Intent(mActivity, SubEditActivity::class.java) + .putExtra("subId", subId) + ) + } + + holder.itemSubSettingBinding.layoutRemove.setOnClickListener { + removeSubscription(subId, position) + } + + holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked -> + if (!it.isPressed) return@setOnCheckedChangeListener + subItem.enabled = isChecked + MmkvManager.encodeSubscription(subId, subItem) + + } + + if (TextUtils.isEmpty(subItem.url)) { + holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE + holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE + holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE + } else { + holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE + holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE + holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE + 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) { + Log.e(AppConfig.TAG, "Share subscription failed", e) + } + }.show() + } + } + } + + private fun removeSubscription(subId: String, position: Int) { + if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { + AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + removeSubscriptionSub(subId, position) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() + } else { + removeSubscriptionSub(subId, position) + } + } + + private fun removeSubscriptionSub(subId: String, position: Int) { + mActivity.lifecycleScope.launch(Dispatchers.IO) { + MmkvManager.removeSubscription(subId) + launch(Dispatchers.Main) { + notifyItemRemoved(position) + notifyItemRangeChanged(position, mActivity.subscriptions.size) + mActivity.refreshData() + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { + return MainViewHolder( + ItemRecyclerSubSettingBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : + BaseViewHolder(itemSubSettingBinding.root), ItemTouchHelperViewHolder + + open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun onItemSelected() { + itemView.setBackgroundColor(Color.LTGRAY) + } + + fun onItemClear() { + itemView.setBackgroundColor(0) + } + } + + override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { + SettingsManager.swapSubscriptions(fromPosition, toPosition) + notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun onItemMoveCompleted() { + mActivity.refreshData() + } + + override fun onItemDismiss(position: Int) { + } +} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt similarity index 79% rename from V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt rename to V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt index 1fff3fbc..8600a4ee 100644 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/TaskerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt @@ -1,51 +1,46 @@ package com.v2ray.ang.ui -import android.app.Activity +import android.content.Intent import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuItem import android.view.View import android.widget.ArrayAdapter import android.widget.ListView -import java.util.ArrayList -import com.v2ray.ang.R -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.R import com.v2ray.ang.databinding.ActivityTaskerBinding -import com.v2ray.ang.util.MmkvManager +import com.v2ray.ang.handler.MmkvManager class TaskerActivity : BaseActivity() { - private lateinit var binding: ActivityTaskerBinding + private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) } private var listview: ListView? = null private var lstData: ArrayList = ArrayList() private var lstGuid: ArrayList = ArrayList() - private val serverStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityTaskerBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) + setContentView(binding.root) //add def value lstData.add("Default") lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) - serverStorage?.allKeys()?.forEach { key -> + MmkvManager.decodeServerList().forEach { key -> MmkvManager.decodeServerConfig(key)?.let { config -> lstData.add(config.remarks) lstGuid.add(key) } } - val adapter = ArrayAdapter(this, - android.R.layout.simple_list_item_single_choice, lstData) + val adapter = ArrayAdapter( + this, + android.R.layout.simple_list_item_single_choice, lstData + ) listview = findViewById(R.id.listview) as ListView - listview!!.adapter = adapter + listview?.adapter = adapter init() } @@ -65,8 +60,8 @@ class TaskerActivity : BaseActivity() { listview?.setItemChecked(pos, true) } } - } catch (e: WriterException) { - e.printStackTrace() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e) } } @@ -91,7 +86,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(RESULT_OK, intent) finish() } @@ -106,11 +101,14 @@ class TaskerActivity : BaseActivity() { R.id.del_config -> { true } + R.id.save_config -> { confirmFinish() true } + else -> super.onOptionsItemSelected(item) } } + diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt new file mode 100644 index 00000000..364f442a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt @@ -0,0 +1,87 @@ +package com.v2ray.ang.ui + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.lifecycle.lifecycleScope +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityLogcatBinding +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.handler.AngConfigManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.URLDecoder + +class UrlSchemeActivity : BaseActivity() { + private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + try { + intent.apply { + if (action == Intent.ACTION_SEND) { + if ("text/plain" == type) { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + parseUri(it, null) + } + } + } else if (action == Intent.ACTION_VIEW) { + when (data?.host) { + "install-config" -> { + val uri: Uri? = intent.data + val shareUrl = uri?.getQueryParameter("url").orEmpty() + parseUri(shareUrl, uri?.fragment) + } + + "install-sub" -> { + val uri: Uri? = intent.data + val shareUrl = uri?.getQueryParameter("url").orEmpty() + parseUri(shareUrl, uri?.fragment) + } + + else -> { + toastError(R.string.toast_failure) + } + } + } + } + + startActivity(Intent(this, MainActivity::class.java)) + finish() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Error processing URL scheme", e) + } + } + + private fun parseUri(uriString: String?, fragment: String?) { + if (uriString.isNullOrEmpty()) { + return + } + Log.i(AppConfig.TAG, uriString) + + var decodedUrl = URLDecoder.decode(uriString, "UTF-8") + val uri = Uri.parse(decodedUrl) + if (uri != null) { + if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) { + decodedUrl += "#${fragment}" + } + Log.i(AppConfig.TAG, decodedUrl) + lifecycleScope.launch(Dispatchers.IO) { + val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false) + withContext(Dispatchers.Main) { + if (count + countSub > 0) { + toast(R.string.import_subscription_success) + } else { + toast(R.string.import_subscription_failure) + } + } + } + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt new file mode 100644 index 00000000..efc0a39c --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -0,0 +1,376 @@ +package com.v2ray.ang.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.app.AlertDialog +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.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityUserAssetBinding +import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding +import com.v2ray.ang.dto.AssetUrlItem +import com.v2ray.ang.extension.concatUrl +import com.v2ray.ang.extension.toTrafficString +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.util.HttpUtil +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.net.HttpURLConnection +import java.text.DateFormat +import java.util.Date + +class UserAssetActivity : BaseActivity() { + private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) } + + val extDir by lazy { File(Utils.userAssetPath(this)) } + val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat") + + private val requestStoragePermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + 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 requestCameraPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java)) + } else { + toast(R.string.toast_permission_denied) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + title = getString(R.string.title_user_asset_setting) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) + binding.recyclerView.adapter = UserAssetAdapter() + + binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources() + binding.layoutGeoFilesSources.setOnClickListener { + setGeoFilesSources() + } + } + + override fun onResume() { + super.onResume() + refreshData() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_asset, menu) + return super.onCreateOptionsMenu(menu) + } + + // Use when to streamline the option selection + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.add_file -> showFileChooser().let { true } + R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true } + R.id.add_qrcode -> importAssetFromQRcode().let { true } + R.id.download_file -> downloadGeoFiles().let { true } + else -> super.onOptionsItemSelected(item) + } + + private fun getGeoFilesSources(): String { + return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first() + } + + private fun setGeoFilesSources() { + AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i -> + try { + val value = AppConfig.GEO_FILES_SOURCES[i] + MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value) + binding.tvGeoFilesSourcesSummary.text = value + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to set geo files sources", e) + } + }.show() + } + + 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 + } + requestStoragePermissionLauncher.launch(permission) + } + + private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val uri = result.data?.data + if (result.resultCode == RESULT_OK && uri != null) { + val assetId = Utils.getUuid() + runCatching { + val assetItem = AssetUrlItem( + getCursorName(uri) ?: uri.toString(), + "file" + ) + + val assetList = MmkvManager.decodeAssetUrls() + if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) { + toast(R.string.msg_remark_is_duplicate) + } else { + MmkvManager.encodeAsset(assetId, assetItem) + copyFile(uri) + } + }.onFailure { + toastError(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) + toastSuccess(R.string.toast_success) + refreshData() + } + } + 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) { + Log.e(AppConfig.TAG, "Failed to get cursor name", e) + null + } + + private fun importAssetFromQRcode(): Boolean { + requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) + return true + } + + private val scanQRCodeForAssetURL = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + importAsset(it.data?.getStringExtra("SCAN_RESULT")) + } + } + + private fun importAsset(url: String?): Boolean { + try { + if (!Utils.isValidUrl(url)) { + toast(R.string.toast_invalid_url) + return false + } + // Send URL to UserAssetUrlActivity for Processing + startActivity( + Intent(this, UserAssetUrlActivity::class.java) + .putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url) + ) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to import asset from URL", e) + return false + } + return true + } + + private fun downloadGeoFiles() { + binding.pbWaiting.show() + toast(R.string.msg_downloading_content) + + val httpPort = SettingsManager.getHttpPort() + var assets = MmkvManager.decodeAssetUrls() + assets = addBuiltInGeoItems(assets) + + var resultCount = 0 + lifecycleScope.launch(Dispatchers.IO) { + assets.forEach { + try { + var result = downloadGeo(it.second, 15000, httpPort) + if (!result) { + result = downloadGeo(it.second, 15000, 0) + } + if (result) + resultCount++ + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e) + } + } + withContext(Dispatchers.Main) { + if (resultCount > 0) { + toast(getString(R.string.title_update_config_count, resultCount)) + refreshData() + } else { + toast(getString(R.string.toast_failure)) + } + binding.pbWaiting.hide() + } + } + } + + private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean { + val targetTemp = File(extDir, item.remarks + "_temp") + val target = File(extDir, item.remarks) + Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}") + + val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false + try { + 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.TAG, "Failed to download geo file: ${item.remarks}", e) + return false + } finally { + conn.disconnect() + } + } + + private fun addBuiltInGeoItems(assets: List>): List> { + val list = mutableListOf>() + builtInGeoFiles + .filter { geoFile -> assets.none { it.second.remarks == geoFile } } + .forEach { + list.add( + Utils.getUuid() to AssetUrlItem( + it, + String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it), + locked = true + ) + ) + } + + return list + assets + } + + fun initAssets() { + lifecycleScope.launch(Dispatchers.Default) { + SettingsManager.initAssets(this@UserAssetActivity, assets) + withContext(Dispatchers.Main) { + refreshData() + } + } + } + + @SuppressLint("NotifyDataSetChanged") + fun refreshData() { + binding.recyclerView.adapter?.notifyDataSetChanged() + } + + inner class UserAssetAdapter : RecyclerView.Adapter() { + 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.locked == true) { + 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 { + AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm) + .setPositiveButton(android.R.string.ok) { _, _ -> + file?.delete() + MmkvManager.removeAssetUrl(item.first) + initAssets() + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + //do noting + } + .show() + } + } + + override fun getItemCount(): Int { + var assets = MmkvManager.decodeAssetUrls() + assets = addBuiltInGeoItems(assets) + return assets.size + } + } + + class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : + RecyclerView.ViewHolder(itemUserAssetBinding.root) +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt new file mode 100644 index 00000000..3d390967 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt @@ -0,0 +1,160 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import com.v2ray.ang.AppConfig +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.extension.toastSuccess +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.Utils +import java.io.File + +class UserAssetUrlActivity : BaseActivity() { + // Receive QRcode URL from UserAssetActivity + companion object { + const val ASSET_URL_QRCODE = "ASSET_URL_QRCODE" + } + + private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) } + + private var del_config: MenuItem? = null + private var save_config: MenuItem? = null + + private val extDir by lazy { File(Utils.userAssetPath(this)) } + private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + title = getString(R.string.title_user_asset_add_url) + + val assetItem = MmkvManager.decodeAsset(editAssetId) + val assetUrlQrcode = intent.getStringExtra(ASSET_URL_QRCODE) + val assetNameQrcode = File(assetUrlQrcode.toString()).name + when { + assetItem != null -> bindingAsset(assetItem) + assetUrlQrcode != null -> { + binding.etRemarks.setText(assetNameQrcode) + binding.etUrl.setText(assetUrlQrcode) + } + + 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 { + var assetItem = MmkvManager.decodeAsset(editAssetId) + var assetId = editAssetId + if (assetItem != null) { + // remove file associated with the asset + val file = extDir.resolve(assetItem.remarks) + if (file.exists()) { + try { + file.delete() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e) + } + } + } 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 + } + + MmkvManager.encodeAsset(assetId, assetItem) + toastSuccess(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.cancel) { _, _ -> + // 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) + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt new file mode 100644 index 00000000..577698ea --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt @@ -0,0 +1,40 @@ +package com.v2ray.ang.util + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import com.v2ray.ang.dto.AppInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +object AppManagerUtil { + /** + * Load the list of network applications. + * + * @param context The context to use. + * @return A list of AppInfo objects representing the network applications. + */ + suspend fun loadNetworkAppList(context: Context): ArrayList = + withContext(Dispatchers.IO) { + val packageManager = context.packageManager + val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) + val apps = ArrayList() + + for (pkg in packages) { + val applicationInfo = pkg.applicationInfo ?: continue + + val appName = applicationInfo.loadLabel(packageManager).toString() + val appIcon = applicationInfo.loadIcon(packageManager) ?: continue + val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0 + + val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) + apps.add(appInfo) + } + + return@withContext apps + } + + fun getLastUpdateTime(context: Context): Long = + context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime + +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt new file mode 100644 index 00000000..7172728e --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt @@ -0,0 +1,223 @@ +package com.v2ray.ang.util + +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.BuildConfig +import com.v2ray.ang.util.Utils.encode +import com.v2ray.ang.util.Utils.urlDecode +import java.io.IOException +import java.net.HttpURLConnection +import java.net.IDN +import java.net.Inet6Address +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy +import java.net.URL + +object HttpUtil { + + /** + * Converts the domain part of a URL string to its IDN (Punycode, ASCII Compatible Encoding) format. + * + * For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path". + * + * @param str The URL string to convert (can contain non-ASCII characters in the domain). + * @return The URL string with the domain part converted to ASCII-compatible (Punycode) format. + */ + fun toIdnUrl(str: String): String { + val url = URL(str) + val host = url.host + val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED) + if (host != asciiHost) { + return str.replace(host, asciiHost) + } else { + return str + } + } + + /** + * Converts a Unicode domain name to its IDN (Punycode, ASCII Compatible Encoding) format. + * If the input is an IP address or already an ASCII domain, returns the original string. + * + * @param domain The domain string to convert (can include non-ASCII internationalized characters). + * @return The domain in ASCII-compatible (Punycode) format, or the original string if input is an IP or already ASCII. + */ + fun toIdnDomain(domain: String): String { + // Return as is if it's a pure IP address (IPv4 or IPv6) + if (Utils.isPureIpAddress(domain)) { + return domain + } + + // Return as is if already ASCII (English domain or already punycode) + if (domain.all { it.code < 128 }) { + return domain + } + + // Otherwise, convert to ASCII using IDN + return IDN.toASCII(domain, IDN.ALLOW_UNASSIGNED) + } + + /** + * Resolves a hostname to an IP address, returns original input if it's already an IP + * + * @param host The hostname or IP address to resolve + * @param ipv6Preferred Whether to prefer IPv6 addresses, defaults to false + * @return The resolved IP address or the original input (if it's already an IP or resolution fails) + */ + fun resolveHostToIP(host: String, ipv6Preferred: Boolean = false): List? { + try { + // If it's already an IP address, return it as a list + if (Utils.isPureIpAddress(host)) { + return null + } + + // Get all IP addresses + val addresses = InetAddress.getAllByName(host) + if (addresses.isEmpty()) { + return null + } + + // Sort addresses based on preference + val sortedAddresses = if (ipv6Preferred) { + addresses.sortedWith(compareByDescending { it is Inet6Address }) + } else { + addresses.sortedWith(compareBy { it is Inet6Address }) + } + + val ipList = sortedAddresses.mapNotNull { it.hostAddress } + + Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}") + + return ipList + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to resolve host to IP", e) + return null + } + } + + + /** + * Retrieves the content of a URL as a string. + * + * @param url The URL to fetch content from. + * @param timeout The timeout value in milliseconds. + * @param httpPort The HTTP port to use. + * @return The content of the URL as a string. + */ + fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? { + val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null + try { + return conn.inputStream.bufferedReader().readText() + } catch (_: Exception) { + } finally { + conn.disconnect() + } + return null + } + + /** + * Retrieves the content of a URL as a string with a custom User-Agent header. + * + * @param url The URL to fetch content from. + * @param timeout The timeout value in milliseconds. + * @param httpPort The HTTP port to use. + * @return The content of the URL as a string. + * @throws IOException If an I/O error occurs. + */ + @Throws(IOException::class) + fun getUrlContentWithUserAgent(url: String?, timeout: Int = 15000, httpPort: Int = 0): String { + var currentUrl = url + var redirects = 0 + val maxRedirects = 3 + + while (redirects++ < maxRedirects) { + if (currentUrl == null) continue + val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue + conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}") + conn.connect() + + val responseCode = conn.responseCode + when (responseCode) { + in 300..399 -> { + val location = conn.getHeaderField("Location") + conn.disconnect() + if (location.isNullOrEmpty()) { + throw IOException("Redirect location not found") + } + currentUrl = location + continue + } + + else -> try { + return conn.inputStream.use { it.bufferedReader().readText() } + } finally { + conn.disconnect() + } + } + } + throw IOException("Too many redirects") + } + + /** + * Creates an HttpURLConnection object connected through a proxy. + * + * @param urlStr The target URL address. + * @param port The port of the proxy server. + * @param connectTimeout The connection timeout in milliseconds (default is 15000 ms). + * @param readTimeout The read timeout in milliseconds (default is 15000 ms). + * @param needStream Whether the connection needs to support streaming. + * @return Returns a configured HttpURLConnection object, or null if it fails. + */ + fun createProxyConnection( + urlStr: String, + port: Int, + connectTimeout: Int = 15000, + readTimeout: Int = 15000, + needStream: Boolean = false + ): HttpURLConnection? { + + var conn: HttpURLConnection? = null + try { + val url = URL(urlStr) + // Create a connection + conn = if (port == 0) { + url.openConnection() + } else { + url.openConnection( + Proxy( + Proxy.Type.HTTP, + InetSocketAddress(LOOPBACK, port) + ) + ) + } as HttpURLConnection + + // Set connection and read timeouts + conn.connectTimeout = connectTimeout + conn.readTimeout = readTimeout + if (!needStream) { + // Set request headers + conn.setRequestProperty("Connection", "close") + // Disable automatic redirects + conn.instanceFollowRedirects = false + // Disable caching + conn.useCaches = false + } + + //Add Basic Authorization + url.userInfo?.let { + conn.setRequestProperty( + "Authorization", + "Basic ${encode(urlDecode(it))}" + ) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to create proxy connection", e) + // If an exception occurs, close the connection and return null + conn?.disconnect() + return null + } + return conn + } +} + diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt new file mode 100644 index 00000000..80a40fbf --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/JsonUtil.kt @@ -0,0 +1,79 @@ +package com.v2ray.ang.util + +import android.util.Log +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.google.gson.reflect.TypeToken +import com.v2ray.ang.AppConfig +import java.lang.reflect.Type + +object JsonUtil { + private var gson = Gson() + + /** + * Converts an object to its JSON representation. + * + * @param src The object to convert. + * @return The JSON representation of the object. + */ + fun toJson(src: Any?): String { + return gson.toJson(src) + } + + /** + * Parses a JSON string into an object of the specified class. + * + * @param src The JSON string to parse. + * @param cls The class of the object to parse into. + * @return The parsed object. + */ + fun fromJson(src: String, cls: Class): T { + return gson.fromJson(src, cls) + } + + /** + * Converts an object to its pretty-printed JSON representation. + * + * @param src The object to convert. + * @return The pretty-printed JSON representation of the object, or null if the object is null. + */ + fun toJsonPretty(src: Any?): String? { + if (src == null) + return null + val gsonPre = GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .registerTypeAdapter( // custom serializer is needed here since JSON by default parse number as Double, core will fail to start + object : TypeToken() {}.type, + JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> + JsonPrimitive( + src?.toInt() + ) + } + ) + .create() + return gsonPre.toJson(src) + } + + /** + * Parses a JSON string into a JsonObject. + * + * @param src The JSON string to parse. + * @return The parsed JsonObject, or null if parsing fails. + */ + fun parseString(src: String?): JsonObject? { + if (src == null) + return null + try { + return JsonParser.parseString(src).getAsJsonObject() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to parse JSON string", e) + return null + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt new file mode 100644 index 00000000..c84443c7 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MessageUtil.kt @@ -0,0 +1,75 @@ +package com.v2ray.ang.util + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.service.V2RayTestService +import java.io.Serializable + +object MessageUtil { + + + /** + * Sends a message to the service. + * + * @param ctx The context. + * @param what The message identifier. + * @param content The message content. + */ + fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) { + sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content) + } + + /** + * Sends a message to the UI. + * + * @param ctx The context. + * @param what The message identifier. + * @param content The message content. + */ + fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) { + sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content) + } + + /** + * Sends a message to the test service. + * + * @param ctx The context. + * @param what The message identifier. + * @param content The message content. + */ + 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) { + Log.e(AppConfig.TAG, "Failed to send message to test service", e) + } + } + + /** + * Sends a message with the specified action. + * + * @param ctx The context. + * @param action The action string. + * @param what The message identifier. + * @param content The message content. + */ + private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) { + try { + val intent = Intent() + intent.action = action + intent.`package` = AppConfig.ANG_PACKAGE + intent.putExtra("key", what) + intent.putExtra("content", content) + ctx.sendBroadcast(intent) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to send message with action: $action", e) + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt new file mode 100644 index 00000000..a769368f --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/MyContextWrapper.kt @@ -0,0 +1,39 @@ +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 { + /** + * Wraps the context with a new locale. + * + * @param context The original context. + * @param newLocale The new locale to set. + * @return A ContextWrapper with the new locale. + */ + @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 + 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) + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt new file mode 100644 index 00000000..2b9f71aa --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt @@ -0,0 +1,140 @@ +package com.v2ray.ang.util + +import android.content.Context +import android.os.SystemClock +import android.util.Log +import com.v2ray.ang.AppConfig +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.fmt.Hysteria2Fmt +import com.v2ray.ang.handler.SpeedtestManager +import com.v2ray.ang.service.ProcessService +import java.io.File + +object PluginUtil { + private const val HYSTERIA2 = "libhysteria2.so" + + private val procService: ProcessService by lazy { + ProcessService() + } + + /** + * Run the plugin based on the provided configuration. + * + * @param context The context to use. + * @param config The profile configuration. + * @param socksPort The port information. + */ + fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) { + Log.i(AppConfig.TAG, "Starting plugin execution") + + if (config == null) { + Log.w(AppConfig.TAG, "Cannot run plugin: config is null") + return + } + + try { + if (config.configType == EConfigType.HYSTERIA2) { + if (socksPort == null) { + Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null") + return + } + Log.i(AppConfig.TAG, "Running Hysteria2 plugin") + val configFile = genConfigHy2(context, config, socksPort) ?: return + val cmd = genCmdHy2(context, configFile) + + procService.runProcess(context, cmd) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Error running plugin", e) + } + } + + /** + * Stop the running plugin. + */ + fun stopPlugin() { + stopHy2() + } + + /** + * Perform a real ping using Hysteria2. + * + * @param context The context to use. + * @param config The profile configuration. + * @return The ping delay in milliseconds, or -1 if it fails. + */ + fun realPingHy2(context: Context, config: ProfileItem?): Long { + Log.i(AppConfig.TAG, "realPingHy2") + val retFailure = -1L + + if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) { + val socksPort = Utils.findFreePort(listOf(0)) + val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure + val cmd = genCmdHy2(context, configFile) + + val proc = ProcessService() + proc.runProcess(context, cmd) + Thread.sleep(1000L) + val delay = SpeedtestManager.testConnection(context, socksPort) + proc.stopProcess() + + return delay.first + } + return retFailure + } + + /** + * Generate the configuration file for Hysteria2. + * + * @param context The context to use. + * @param config The profile configuration. + * @param socksPort The port information. + * @return The generated configuration file. + */ + private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? { + Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2") + + val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null + + val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json") + Log.i(AppConfig.TAG, "runPlugin ${configFile.absolutePath}") + + configFile.parentFile?.mkdirs() + configFile.writeText(JsonUtil.toJson(hy2Config)) + Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config)) + + return configFile + } + + /** + * Generate the command to run Hysteria2. + * + * @param context The context to use. + * @param configFile The configuration file. + * @return The command to run Hysteria2. + */ + private fun genCmdHy2(context: Context, configFile: File): MutableList { + return mutableListOf( + File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath, + "--disable-update-check", + "--config", + configFile.absolutePath, + "--log-level", + "warn", + "client" + ) + } + + /** + * Stop the Hysteria2 process. + */ + private fun stopHy2() { + try { + Log.i(AppConfig.TAG, "$HYSTERIA2 destroy") + procService?.stopProcess() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to stop Hysteria2 process", e) + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java deleted file mode 100644 index 1a16ac3e..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.java +++ /dev/null @@ -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 HINTS = new EnumMap<>(DecodeHintType.class); - - static { - List 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; - } - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt new file mode 100644 index 00000000..446739b6 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/QRCodeDecoder.kt @@ -0,0 +1,123 @@ +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 + +/** + * QR code decoder utility. + */ +object QRCodeDecoder { + val HINTS: MutableMap = EnumMap(DecodeHintType::class.java) + + /** + * Creates a QR code bitmap from the given text. + * + * @param text The text to encode in the QR code. + * @param size The size of the QR code bitmap. + * @return The generated QR code bitmap, or null if an error occurs. + */ + fun createQRCode(text: String, size: Int = 800): Bitmap? { + return runCatching { + val hints = mapOf(EncodeHintType.CHARACTER_SET to Charsets.UTF_8) + val bitMatrix = QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size, hints) + val pixels = IntArray(size * size) { i -> + if (bitMatrix.get(i % size, i / size)) 0xff000000.toInt() else 0xffffffff.toInt() + } + Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply { + setPixels(pixels, 0, size, 0, 0, size, size) + } + }.getOrNull() + } + + /** + * Decodes a QR code from a local image file. This method is time-consuming and should be called in a background thread. + * + * @param picturePath The local path of the image file to decode. + * @return The content of the QR code, or null if decoding fails. + */ + fun syncDecodeQRCode(picturePath: String): String? { + return syncDecodeQRCode(getDecodeAbleBitmap(picturePath)) + } + + /** + * Decodes a QR code from a bitmap. This method is time-consuming and should be called in a background thread. + * + * @param bitmap The bitmap to decode. + * @return The content of the QR code, or null if decoding fails. + */ + fun syncDecodeQRCode(bitmap: Bitmap?): String? { + return bitmap?.let { + runCatching { + val pixels = IntArray(it.width * it.height).also { array -> + it.getPixels(array, 0, it.width, 0, 0, it.width, it.height) + } + val source = RGBLuminanceSource(it.width, it.height, pixels) + val qrReader = QRCodeReader() + + try { + qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)), mapOf(DecodeHintType.TRY_HARDER to true)).text + } catch (e: NotFoundException) { + qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source.invert())), mapOf(DecodeHintType.TRY_HARDER to true)).text + } + }.getOrNull() + } + } + + /** + * Converts a local image file to a bitmap that can be decoded as a QR code. The image is compressed to avoid being too large. + * + * @param picturePath The local path of the image file. + * @return The decoded bitmap, or null if an error occurs. + */ + 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 = 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] = Charsets.UTF_8 + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt new file mode 100644 index 00000000..148ce4ec --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt @@ -0,0 +1,570 @@ +package com.v2ray.ang.util + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.res.Configuration.UI_MODE_NIGHT_MASK +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.os.Build +import android.os.LocaleList +import android.provider.Settings +import android.text.Editable +import android.util.Base64 +import android.util.Log +import android.util.Patterns +import android.webkit.URLUtil +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import com.v2ray.ang.AppConfig +import com.v2ray.ang.AppConfig.LOOPBACK +import com.v2ray.ang.BuildConfig +import java.io.IOException +import java.net.InetAddress +import java.net.ServerSocket +import java.net.URI +import java.net.URLDecoder +import java.net.URLEncoder +import java.util.Locale +import java.util.UUID + +object Utils { + + private val IPV4_REGEX = + Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") + private val IPV6_REGEX = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$") + + /** + * Convert string to editable for Kotlin. + * + * @param text The string to convert. + * @return An Editable instance containing the text. + */ + fun getEditable(text: String?): Editable { + return Editable.Factory.getInstance().newEditable(text.orEmpty()) + } + + /** + * Find the position of a value in an array. + * + * @param array The array to search. + * @param value The value to find. + * @return The index of the value in the array, or -1 if not found. + */ + fun arrayFind(array: Array, value: String): Int { + return array.indexOf(value) + } + + /** + * Parse a string to an integer with a default value. + * + * @param str The string to parse. + * @param default The default value if parsing fails. + * @return The parsed integer, or the default value if parsing fails. + */ + fun parseInt(str: String?, default: Int = 0): Int { + return str?.toIntOrNull() ?: default + } + + /** + * Get text from the clipboard. + * + * @param context The context to use. + * @return The text from the clipboard, or an empty string if an error occurs. + */ + fun getClipboard(context: Context): String { + return try { + val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + cmb.primaryClip?.getItemAt(0)?.text.toString() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to get clipboard content", e) + "" + } + } + + /** + * Set text to the clipboard. + * + * @param context The context to use. + * @param content The text to set to the clipboard. + */ + fun setClipboard(context: Context, content: String) { + try { + val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText(null, content) + cmb.setPrimaryClip(clipData) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to set clipboard content", e) + } + } + + /** + * Decode a base64 encoded string. + * + * @param text The base64 encoded string. + * @return The decoded string, or an empty string if decoding fails. + */ + fun decode(text: String?): String { + return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty() + } + + /** + * Try to decode a base64 encoded string. + * + * @param text The base64 encoded string. + * @return The decoded string, or null if decoding fails. + */ + fun tryDecodeBase64(text: String?): String? { + if (text.isNullOrEmpty()) return null + + try { + return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to decode standard base64", e) + } + try { + return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e) + } + return null + } + + /** + * Encode a string to base64. + * + * @param text The string to encode. + * @return The base64 encoded string, or an empty string if encoding fails. + */ + fun encode(text: String): String { + return try { + Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to encode text to base64", e) + "" + } + } + + /** + * Check if a string is a valid IP address. + * + * @param value The string to check. + * @return True if the string is a valid IP address, false otherwise. + */ + fun isIpAddress(value: String?): Boolean { + if (value.isNullOrEmpty()) return false + + try { + var addr = value.trim() + if (addr.isEmpty()) return false + + //CIDR + if (addr.contains("/")) { + val arr = addr.split("/") + if (arr.size == 2 && arr[1].toIntOrNull() != null && arr[1].toInt() > -1) { + addr = arr[0] + } + } + + // Handle IPv4-mapped IPv6 addresses + if (addr.startsWith("::ffff:") && '.' in addr) { + addr = addr.drop(7) + } else if (addr.startsWith("[::ffff:") && '.' in addr) { + addr = addr.drop(8).replace("]", "") + } + + val octets = addr.split('.') + if (octets.size == 4) { + if (octets[3].contains(":")) { + addr = addr.substring(0, addr.indexOf(":")) + } + return isIpv4Address(addr) + } + + return isIpv6Address(addr) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to validate IP address", e) + return false + } + } + + /** + * Check if a string is a pure IP address (IPv4 or IPv6). + * + * @param value The string to check. + * @return True if the string is a pure IP address, false otherwise. + */ + fun isPureIpAddress(value: String): Boolean { + return isIpv4Address(value) || isIpv6Address(value) + } + + /** + * Check if a string is a valid domain name. + * + * A valid domain name must not be an IP address and must be a valid URL format. + * + * @param input The string to check. + * @return True if the string is a valid domain name, false otherwise. + */ + fun isDomainName(input: String?): Boolean { + if (input.isNullOrEmpty()) return false + + // Must not be an IP address and must be a valid URL format + return !isPureIpAddress(input) && isValidUrl(input) + } + + /** + * Check if a string is a valid IPv4 address. + * + * @param value The string to check. + * @return True if the string is a valid IPv4 address, false otherwise. + */ + private fun isIpv4Address(value: String): Boolean { + return IPV4_REGEX.matches(value) + } + + /** + * Check if a string is a valid IPv6 address. + * + * @param value The string to check. + * @return True if the string is a valid IPv6 address, false otherwise. + */ + private fun isIpv6Address(value: String): Boolean { + var addr = value + if (addr.startsWith("[") && addr.endsWith("]")) { + addr = addr.drop(1).dropLast(1) + } + return IPV6_REGEX.matches(addr) + } + + /** + * Check if a string is a CoreDNS address. + * + * @param s The string to check. + * @return True if the string is a CoreDNS address, false otherwise. + */ + fun isCoreDNSAddress(s: String): Boolean { + return s.startsWith("https") || + s.startsWith("tcp") || + s.startsWith("quic") || + s == "localhost" + } + + /** + * Check if a string is a valid URL. + * + * @param value The string to check. + * @return True if the string is a valid URL, false otherwise. + */ + fun isValidUrl(value: String?): Boolean { + if (value.isNullOrEmpty()) return false + + return try { + Patterns.WEB_URL.matcher(value).matches() || + Patterns.DOMAIN_NAME.matcher(value).matches() || + URLUtil.isValidUrl(value) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to validate URL", e) + false + } + } + + /** + * Open a URI in a browser. + * + * @param context The context to use. + * @param uriString The URI string to open. + */ + fun openUri(context: Context, uriString: String) { + try { + val uri = uriString.toUri() + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to open URI", e) + } + } + + /** + * Generate a UUID. + * + * @return A UUID string without dashes. + */ + fun getUuid(): String { + return try { + UUID.randomUUID().toString().replace("-", "") + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to generate UUID", e) + "" + } + } + + /** + * Decode a URL-encoded string. + * + * @param url The URL-encoded string. + * @return The decoded string, or the original string if decoding fails. + */ + fun urlDecode(url: String): String { + return try { + URLDecoder.decode(url, Charsets.UTF_8.toString()) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to decode URL", e) + url + } + } + + /** + * Encode a string to URL-encoded format. + * + * @param url The string to encode. + * @return The URL-encoded string, or the original string if encoding fails. + */ + fun urlEncode(url: String): String { + return try { + URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20") + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to encode URL", e) + url + } + } + + /** + * Read text from an asset file. + * + * @param context The context to use. + * @param fileName The name of the asset file. + * @return The content of the asset file as a string. + */ + fun readTextFromAssets(context: Context?, fileName: String): String { + if (context == null) return "" + + return try { + context.assets.open(fileName).use { inputStream -> + inputStream.bufferedReader().use { reader -> + reader.readText() + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e) + "" + } + } + + /** + * Get the path to the user asset directory. + * + * @param context The context to use. + * @return The path to the user asset directory. + */ + fun userAssetPath(context: Context?): String { + if (context == null) return "" + + return try { + context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath + ?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to get user asset path", e) + "" + } + } + + /** + * Get the path to the backup directory. + * + * @param context The context to use. + * @return The path to the backup directory. + */ + fun backupPath(context: Context?): String { + if (context == null) return "" + + return try { + context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath + ?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to get backup path", e) + "" + } + } + + /** + * Get the device ID for XUDP base key. + * + * @return The device ID for XUDP base key. + */ + fun getDeviceIdForXUDPBaseKey(): String { + return try { + val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8) + Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE)) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to generate device ID", e) + "" + } + } + + /** + * Get the dark mode status. + * + * @param context The context to use. + * @return True if dark mode is enabled, false otherwise. + */ + fun getDarkModeStatus(context: Context): Boolean { + return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO + } + + /** + * Get the IPv6 address in a formatted string. + * + * @param address The IPv6 address. + * @return The formatted IPv6 address, or the original address if not valid. + */ + fun getIpv6Address(address: String?): String { + if (address.isNullOrEmpty()) return "" + + return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) { + "[$address]" + } else { + address + } + } + + /** + * Get the system locale. + * + * @return The system locale. + */ + fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + LocaleList.getDefault()[0] + } else { + Locale.getDefault() + } + + /** + * Fix illegal characters in a URL. + * + * @param str The URL string. + * @return The URL string with illegal characters replaced. + */ + fun fixIllegalUrl(str: String): String { + return str.replace(" ", "%20") + .replace("|", "%7C") + } + + /** + * Find a free port from a list of ports. + * + * @param ports The list of ports to check. + * @return The first free port found. + * @throws IOException If no free port is found. + */ + fun findFreePort(ports: List): Int { + for (port in ports) { + try { + return ServerSocket(port).use { it.localPort } + } catch (ex: IOException) { + continue // try next port + } + } + + // if the program gets here, no port in the range was found + throw IOException("no free port found") + } + + /** + * Check if a string is a valid subscription URL. + * + * @param value The string to check. + * @return True if the string is a valid subscription URL, false otherwise. + */ + fun isValidSubUrl(value: String?): Boolean { + if (value.isNullOrEmpty()) return false + + try { + if (URLUtil.isHttpsUrl(value)) return true + if (URLUtil.isHttpUrl(value)) { + if (value.contains(LOOPBACK)) return true + + //Check private ip address + val uri = URI(fixIllegalUrl(value)) + if (isIpAddress(uri.host)) { + AppConfig.PRIVATE_IP_LIST.forEach { + if (isIpInCidr(uri.host, it)) return true + } + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to validate subscription URL", e) + } + return false + } + + /** + * Get the receiver flags based on the Android version. + * + * @return The receiver flags. + */ + fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.RECEIVER_EXPORTED + } else { + ContextCompat.RECEIVER_NOT_EXPORTED + } + + /** + * Check if the package is Xray. + * + * @return True if the package is Xray, false otherwise. + */ + fun isXray(): Boolean = BuildConfig.APPLICATION_ID.startsWith("com.v2ray.ang") + + /** + * Check if it is the Google Play version. + * + * @return True if the package is Google Play, false otherwise. + */ + fun isGoogleFlavor(): Boolean = BuildConfig.FLAVOR == "playstore" + + /** + * Converts an InetAddress to its long representation + * + * @param ip The InetAddress to convert + * @return The long representation of the IP address + */ + private fun inetAddressToLong(ip: InetAddress): Long { + val bytes = ip.address + var result: Long = 0 + for (i in bytes.indices) { + result = result shl 8 or (bytes[i].toInt() and 0xff).toLong() + } + return result + } + + /** + * Check if an IP address is within a CIDR range + * + * @param ip The IP address to check + * @param cidr The CIDR notation range (e.g., "192.168.1.0/24") + * @return True if the IP is within the CIDR range, false otherwise + */ + fun isIpInCidr(ip: String, cidr: String): Boolean { + try { + if (!isIpAddress(ip)) return false + + // Parse CIDR (e.g., "192.168.1.0/24") + val (cidrIp, prefixLen) = cidr.split("/") + val prefixLength = prefixLen.toInt() + + // Convert IP and CIDR's IP portion to Long + val ipLong = inetAddressToLong(InetAddress.getByName(ip)) + val cidrIpLong = inetAddressToLong(InetAddress.getByName(cidrIp)) + + // Calculate subnet mask (e.g., /24 → 0xFFFFFF00) + val mask = if (prefixLength == 0) 0L else (-1L shl (32 - prefixLength)) + + // Check if they're in the same subnet + return (ipLong and mask) == (cidrIpLong and mask) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e) + return false + } + } +} + diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt new file mode 100644 index 00000000..9d9dce62 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/ZipUtil.kt @@ -0,0 +1,125 @@ +package com.v2ray.ang.util + +import android.util.Log +import com.v2ray.ang.AppConfig +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 + + /** + * Zip the contents of a folder. + * + * @param folderPath The path to the folder to zip. + * @param outputZipFilePath The path to the output zip file. + * @return True if the operation is successful, false otherwise. + * @throws IOException If an I/O error occurs. + */ + @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() + 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) { + Log.e(AppConfig.TAG, "Failed to zip folder", e) + return false + } + return true + } + + /** + * Unzip the contents of a zip file to a folder. + * + * @param zipFile The zip file to unzip. + * @param destDirectory The destination directory. + * @return True if the operation is successful, false otherwise. + * @throws IOException If an I/O error occurs. + */ + @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) { + extractFile(input, filePath) + } else { + val dir = File(filePath) + dir.mkdir() + } + } + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to unzip file", e) + return false + } + return true + } + + /** + * Extract a file from an input stream. + * + * @param inputStream The input stream to read from. + * @param destFilePath The destination file path. + * @throws IOException If an I/O error occurs. + */ + @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() + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt new file mode 100644 index 00000000..ec5cb7ee --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt @@ -0,0 +1,447 @@ +package com.v2ray.ang.viewmodel + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.AssetManager +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.v2ray.ang.AngApplication +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.dto.ProfileItem +import com.v2ray.ang.dto.ServersCache +import com.v2ray.ang.extension.serializable +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.handler.SpeedtestManager +import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.Utils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import java.util.Collections + +class MainViewModel(application: Application) : AndroidViewModel(application) { + private var serverList = MmkvManager.decodeServerList() + var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() + + //var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:"" + var keywordFilter = "" + val serversCache = mutableListOf() + val isRunning by lazy { MutableLiveData() } + val updateListAction by lazy { MutableLiveData() } + val updateTestResultAction by lazy { MutableLiveData() } + private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) } + + /** + * Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int): + * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`. + */ + fun startListenBroadcast() { + isRunning.value = false + val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY) + ContextCompat.registerReceiver(getApplication(), mMsgReceiver, mFilter, Utils.receiverFlags()) + MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "") + } + + /** + * Called when the ViewModel is cleared. + */ + override fun onCleared() { + getApplication().unregisterReceiver(mMsgReceiver) + tcpingTestScope.coroutineContext[Job]?.cancelChildren() + SpeedtestManager.closeAllTcpSockets() + Log.i(AppConfig.TAG, "Main ViewModel is cleared") + super.onCleared() + } + + /** + * Reloads the server list. + */ + fun reloadServerList() { + serverList = MmkvManager.decodeServerList() + updateCache() + updateListAction.value = -1 + } + + /** + * Removes a server by its GUID. + * @param guid The GUID of the server to remove. + */ + fun removeServer(guid: String) { + serverList.remove(guid) + MmkvManager.removeServer(guid) + val index = getPosition(guid) + if (index >= 0) { + serversCache.removeAt(index) + } + } + +// /** +// * Appends a custom configuration server. +// * @param server The server configuration to append. +// * @return True if the server was successfully appended, false otherwise. +// */ +// fun appendCustomConfigServer(server: String): Boolean { +// if (server.contains("inbounds") +// && server.contains("outbounds") +// && server.contains("routing") +// ) { +// try { +// val config = CustomFmt.parse(server) ?: return false +// config.subscriptionId = subscriptionId +// val key = MmkvManager.encodeServerConfig("", config) +// MmkvManager.encodeServerRaw(key, server) +// serverList.add(0, key) +//// val profile = ProfileLiteItem( +//// configType = config.configType, +//// subscriptionId = config.subscriptionId, +//// remarks = config.remarks, +//// server = config.getProxyOutbound()?.getServerAddress(), +//// serverPort = config.getProxyOutbound()?.getServerPort(), +//// ) +// serversCache.add(0, ServersCache(key, config)) +// return true +// } catch (e: Exception) { +// e.printStackTrace() +// } +// } +// return false +// } + + /** + * Swaps the positions of two servers. + * @param fromPosition The initial position of the server. + * @param toPosition The target position of the server. + */ + fun swapServer(fromPosition: Int, toPosition: Int) { + if (subscriptionId.isEmpty()) { + Collections.swap(serverList, fromPosition, toPosition) + } else { + val fromPosition2 = serverList.indexOf(serversCache[fromPosition].guid) + val toPosition2 = serverList.indexOf(serversCache[toPosition].guid) + Collections.swap(serverList, fromPosition2, toPosition2) + } + Collections.swap(serversCache, fromPosition, toPosition) + MmkvManager.encodeServerList(serverList) + } + + /** + * Updates the cache of servers. + */ + @Synchronized + fun updateCache() { + serversCache.clear() + for (guid in serverList) { + var profile = MmkvManager.decodeServerConfig(guid) ?: continue +// var profile = MmkvManager.decodeProfileConfig(guid) +// if (profile == null) { +// val config = MmkvManager.decodeServerConfig(guid) ?: continue +// profile = ProfileLiteItem( +// configType = config.configType, +// subscriptionId = config.subscriptionId, +// remarks = config.remarks, +// server = config.getProxyOutbound()?.getServerAddress(), +// serverPort = config.getProxyOutbound()?.getServerPort(), +// ) +// MmkvManager.encodeServerConfig(guid, config) +// } + + if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) { + continue + } + + if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) { + serversCache.add(ServersCache(guid, profile)) + } + } + } + + /** + * Updates the configuration via subscription for all servers. + * @return The number of updated configurations. + */ + fun updateConfigViaSubAll(): Int { + if (subscriptionId.isEmpty()) { + return AngConfigManager.updateConfigViaSubAll() + } else { + val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0 + return AngConfigManager.updateConfigViaSub(Pair(subscriptionId, subItem)) + } + } + + /** + * Exports all servers. + * @return The number of exported servers. + */ + fun exportAllServer(): Int { + val serverListCopy = + if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { + serverList + } else { + serversCache.map { it.guid }.toList() + } + + val ret = AngConfigManager.shareNonCustomConfigsToClipboard( + getApplication(), + serverListCopy + ) + return ret + } + + /** + * Tests the TCP ping for all servers. + */ + fun testAllTcping() { + tcpingTestScope.coroutineContext[Job]?.cancelChildren() + SpeedtestManager.closeAllTcpSockets() + MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) + + val serversCopy = serversCache.toList() + for (item in serversCopy) { + item.profile.let { outbound -> + val serverAddress = outbound.server + val serverPort = outbound.serverPort + if (serverAddress != null && serverPort != null) { + tcpingTestScope.launch { + val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt()) + launch(Dispatchers.Main) { + MmkvManager.encodeServerTestDelayMillis(item.guid, testResult) + updateListAction.value = getPosition(item.guid) + } + } + } + } + } + } + + /** + * Tests the real ping for all servers. + */ + fun testAllRealPing() { + MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "") + MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) + updateListAction.value = -1 + + val serversCopy = serversCache.toList() + viewModelScope.launch(Dispatchers.Default) { + for (item in serversCopy) { + MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid) + } + } + } + + /** + * Tests the real ping for the current server. + */ + fun testCurrentServerRealPing() { + MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "") + } + + /** + * Changes the subscription ID. + * @param id The new subscription ID. + */ + fun subscriptionIdChanged(id: String) { + if (subscriptionId != id) { + subscriptionId = id + MmkvManager.encodeSettings(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId) + reloadServerList() + } + } + + /** + * Gets the subscriptions. + * @param context The context. + * @return A pair of lists containing the subscription IDs and remarks. + */ + fun getSubscriptions(context: Context): Pair?, MutableList?> { + 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 + } + + /** + * Gets the position of a server by its GUID. + * @param guid The GUID of the server. + * @return The position of the server. + */ + fun getPosition(guid: String): Int { + serversCache.forEachIndexed { index, it -> + if (it.guid == guid) + return index + } + return -1 + } + + /** + * Removes duplicate servers. + * @return The number of removed servers. + */ + fun removeDuplicateServer(): Int { + val serversCacheCopy = mutableListOf>() + for (it in serversCache) { + val config = MmkvManager.decodeServerConfig(it.guid) ?: continue + serversCacheCopy.add(Pair(it.guid, config)) + } + + val deleteServer = mutableListOf() + serversCacheCopy.forEachIndexed { index, it -> + val outbound = it.second + serversCacheCopy.forEachIndexed { index2, it2 -> + if (index2 > index) { + val outbound2 = it2.second + if (outbound.equals(outbound2) && !deleteServer.contains(it2.first)) { + deleteServer.add(it2.first) + } + } + } + } + for (it in deleteServer) { + MmkvManager.removeServer(it) + } + + return deleteServer.count() + } + + /** + * Removes all servers. + * @return The number of removed servers. + */ + fun removeAllServer(): Int { + val count = + if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { + MmkvManager.removeAllServer() + } else { + val serversCopy = serversCache.toList() + for (item in serversCopy) { + MmkvManager.removeServer(item.guid) + } + serversCache.toList().count() + } + return count + } + + /** + * Removes invalid servers. + * @return The number of removed servers. + */ + fun removeInvalidServer(): Int { + var count = 0 + if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { + count += MmkvManager.removeInvalidServer("") + } else { + val serversCopy = serversCache.toList() + for (item in serversCopy) { + count += MmkvManager.removeInvalidServer(item.guid) + } + } + return count + } + + /** + * Sorts servers by their test results. + */ + fun sortByTestResults() { + data class ServerDelay(var guid: String, var testDelayMillis: Long) + + val serverDelays = mutableListOf() + val serverList = MmkvManager.decodeServerList() + serverList.forEach { key -> + val delay = MmkvManager.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) + } + + MmkvManager.encodeServerList(serverList) + } + + /** + * Initializes assets. + * @param assets The asset manager. + */ + fun initAssets(assets: AssetManager) { + viewModelScope.launch(Dispatchers.Default) { + SettingsManager.initAssets(getApplication(), assets) + } + } + + /** + * Filters the configuration by a keyword. + * @param keyword The keyword to filter by. + */ + fun filterConfig(keyword: String) { + if (keyword == keywordFilter) { + return + } + keywordFilter = keyword + MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter) + reloadServerList() + } + + private val mMsgReceiver = object : BroadcastReceiver() { + override fun onReceive(ctx: Context?, intent: Intent?) { + when (intent?.getIntExtra("key", 0)) { + AppConfig.MSG_STATE_RUNNING -> { + isRunning.value = true + } + + AppConfig.MSG_STATE_NOT_RUNNING -> { + isRunning.value = false + } + + AppConfig.MSG_STATE_START_SUCCESS -> { + getApplication().toastSuccess(R.string.toast_services_success) + isRunning.value = true + } + + AppConfig.MSG_STATE_START_FAILURE -> { + getApplication().toastError(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.serializable>("content") ?: return + MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second) + updateListAction.value = getPosition(resultPair.first) + } + } + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt new file mode 100644 index 00000000..7ac5d60f --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt @@ -0,0 +1,99 @@ +package com.v2ray.ang.viewmodel + +import android.app.Application +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.preference.PreferenceManager +import com.v2ray.ang.AppConfig +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.handler.SettingsManager + +class SettingsViewModel(application: Application) : AndroidViewModel(application), + SharedPreferences.OnSharedPreferenceChangeListener { + + /** + * Starts listening for preference changes. + */ + fun startListenPreferenceChange() { + PreferenceManager.getDefaultSharedPreferences(getApplication()) + .registerOnSharedPreferenceChangeListener(this) + } + + /** + * Called when the ViewModel is cleared. + */ + override fun onCleared() { + PreferenceManager.getDefaultSharedPreferences(getApplication()) + .unregisterOnSharedPreferenceChangeListener(this) + Log.i(AppConfig.TAG, "Settings ViewModel is cleared") + super.onCleared() + } + + /** + * Called when a shared preference is changed. + * @param sharedPreferences The shared preferences. + * @param key The key of the changed preference. + */ + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { + Log.i(AppConfig.TAG, "Observe settings changed: $key") + when (key) { + AppConfig.PREF_MODE, + AppConfig.PREF_VPN_DNS, + AppConfig.PREF_VPN_BYPASS_LAN, + AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, + AppConfig.PREF_REMOTE_DNS, + AppConfig.PREF_DOMESTIC_DNS, + AppConfig.PREF_DNS_HOSTS, + AppConfig.PREF_DELAY_TEST_URL, + AppConfig.PREF_LOCAL_DNS_PORT, + AppConfig.PREF_SOCKS_PORT, + AppConfig.PREF_LOGLEVEL, + AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, + AppConfig.PREF_LANGUAGE, + AppConfig.PREF_UI_MODE_NIGHT, + AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, + AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, + AppConfig.PREF_FRAGMENT_PACKETS, + AppConfig.PREF_FRAGMENT_LENGTH, + AppConfig.PREF_FRAGMENT_INTERVAL, + AppConfig.PREF_MUX_XUDP_QUIC, + -> { + MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "")) + } + + AppConfig.PREF_ROUTE_ONLY_ENABLED, + AppConfig.PREF_IS_BOOTED, + AppConfig.PREF_SPEED_ENABLED, + AppConfig.PREF_PROXY_SHARING, + AppConfig.PREF_LOCAL_DNS_ENABLED, + AppConfig.PREF_FAKE_DNS_ENABLED, + AppConfig.PREF_APPEND_HTTP_PROXY, + AppConfig.PREF_ALLOW_INSECURE, + AppConfig.PREF_PREFER_IPV6, + AppConfig.PREF_PER_APP_PROXY, + AppConfig.PREF_BYPASS_APPS, + AppConfig.PREF_CONFIRM_REMOVE, + AppConfig.PREF_START_SCAN_IMMEDIATE, + AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, + AppConfig.SUBSCRIPTION_AUTO_UPDATE, + AppConfig.PREF_FRAGMENT_ENABLED, + AppConfig.PREF_MUX_ENABLED, + -> { + MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, false)) + } + + AppConfig.PREF_SNIFFING_ENABLED -> { + MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, true)) + } + + AppConfig.PREF_MUX_CONCURRENCY, + AppConfig.PREF_MUX_XUDP_CONCURRENCY -> { + MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "8")) + } + } + if (key == AppConfig.PREF_UI_MODE_NIGHT) { + SettingsManager.setNightMode() + } + } +} diff --git a/V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so b/V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so deleted file mode 100755 index 0bbc6891..00000000 Binary files a/V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so and /dev/null differ diff --git a/V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so b/V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so deleted file mode 100755 index 4f588c79..00000000 Binary files a/V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so and /dev/null differ diff --git a/V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so b/V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so deleted file mode 100755 index 9ce3e627..00000000 Binary files a/V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so and /dev/null differ diff --git a/V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so b/V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so deleted file mode 100755 index 58199692..00000000 Binary files a/V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so and /dev/null differ diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt deleted file mode 100644 index 6ca82ded..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.v2ray.ang - -import androidx.multidex.MultiDexApplication -import androidx.preference.PreferenceManager -import com.tencent.mmkv.MMKV - -class AngApplication : MultiDexApplication() { - companion object { - const val PREF_LAST_VERSION = "pref_last_version" - } - - var curIndex = -1 //Current proxy that is opened. (Used to implement restart feature) - 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() - - //Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE) - MMKV.initialize(this) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt deleted file mode 100644 index 5f37e276..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.v2ray.ang - -/** - * - * App Config Const - */ -object AppConfig { - const val ANG_PACKAGE = "com.v2ray.ang" - - // legacy - 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" - const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled" - const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled" - 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_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 HTTP_PROTOCOL: String = "http://" - const val HTTPS_PROTOCOL: String = "https://" - - 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" - - 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" - const val TAG_DIRECT = "direct" - const val TAG_BLOCKED = "block" - - 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" - - const val DNS_AGENT = "1.1.1.1" - const val DNS_DIRECT = "223.5.5.5" - - const val MSG_REGISTER_CLIENT = 1 - const val MSG_STATE_RUNNING = 11 - const val MSG_STATE_NOT_RUNNING = 12 - const val MSG_UNREGISTER_CLIENT = 2 - const val MSG_STATE_START = 3 - const val MSG_STATE_START_SUCCESS = 31 - const val MSG_STATE_START_FAILURE = 32 - const val MSG_STATE_STOP = 4 - const val MSG_STATE_STOP_SUCCESS = 41 - const val MSG_STATE_RESTART = 5 -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt deleted file mode 100644 index c51d78b6..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AngConfig.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.v2ray.ang.dto - -data class AngConfig( - var index: Int, - var vmess: ArrayList, - var subItem: ArrayList -) { - 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 = "") -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt deleted file mode 100644 index f99655a8..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/AppInfo.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.v2ray.ang.dto - -import android.graphics.drawable.Drawable - -data class AppInfo(val appName: String, - val packageName: String, - val appIcon: Drawable, - val isSystemApp: Boolean, - var isSelected: Int) \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt deleted file mode 100644 index 3f12160f..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/EConfigType.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.v2ray.ang.dto - -enum class EConfigType(val value: Int, val protocolScheme: String) { - VMESS(1, "vmess://"), - CUSTOM(2, ""), - SHADOWSOCKS(3, "ss://"), - SOCKS(4, "socks://"), - VLESS(5, "vless://"), - TROJAN(6, "trojan://"); - - companion object { - fun fromInt(value: Int) = values().firstOrNull { it.value == value } - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt deleted file mode 100644 index db89c946..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/ServerConfig.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.v2ray.ang.dto - -import com.v2ray.ang.AppConfig.TAG_AGENT -import com.v2ray.ang.AppConfig.TAG_BLOCKED -import com.v2ray.ang.AppConfig.TAG_DIRECT -import com.v2ray.ang.util.Utils - -data class ServerConfig( - val configVersion: Int = 3, - val configType: EConfigType, - var subscriptionId: String = "", - val addedTime: Long = System.currentTimeMillis(), - var remarks: String = "", - val outboundBean: V2rayConfig.OutboundBean? = null, - var fullConfig: V2rayConfig? = null -) { - companion object { - fun create(configType: EConfigType): 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())) - 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())) - } - } - } - - fun getProxyOutbound(): V2rayConfig.OutboundBean? { - if (configType != EConfigType.CUSTOM) { - return outboundBean - } - return fullConfig?.getProxyOutbound() - } - - fun getAllOutboundTags(): MutableList { - if (configType != EConfigType.CUSTOM) { - return mutableListOf(TAG_AGENT, TAG_DIRECT, TAG_BLOCKED) - } - fullConfig?.let { config -> - return config.outbounds.map { it.tag }.toMutableList() - } - return mutableListOf() - } - - 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) - } - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt deleted file mode 100644 index b2195148..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/SubscriptionItem.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.v2ray.ang.dto - -data class SubscriptionItem( - var remarks: String = "", - var url: String = "", - var enabled: Boolean = true, - val addedTime: Long = System.currentTimeMillis()) { -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt deleted file mode 100644 index e3bc1e22..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt +++ /dev/null @@ -1,446 +0,0 @@ -package com.v2ray.ang.dto - -import android.text.TextUtils -import com.google.gson.GsonBuilder -import com.google.gson.JsonPrimitive -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer -import com.google.gson.annotations.SerializedName -import com.google.gson.reflect.TypeToken -import java.lang.reflect.Type - -data class V2rayConfig( - var stats: Any? = null, - val log: LogBean, - var policy: PolicyBean?, - val inbounds: ArrayList, - var outbounds: ArrayList, - var dns: DnsBean, - val routing: RoutingBean, - val api: Any? = null, - val transport: Any? = null, - val reverse: Any? = null, - var fakedns: Any? = null, - val browserForwarder: 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 HTTP = "http" - } - - data class LogBean(val access: String, - val error: String, - var loglevel: String?, - val dnsLog: Boolean? = null) - - data class InboundBean( - var tag: String, - var port: Int, - var protocol: String, - var listen: String? = null, - val settings: Any? = null, - val sniffing: SniffingBean?, - val streamSettings: Any? = null, - val allocate: Any? = null) { - - data class InSettingsBean(val auth: String? = null, - val udp: Boolean? = null, - val userLevel: Int? = null, - val address: String? = null, - val port: Int? = null, - val network: String? = null) - - data class SniffingBean(var enabled: Boolean, - val destOverride: ArrayList, - val metadataOnly: Boolean? = null) - } - - data class OutboundBean(val 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)) { - - data class OutSettingsBean(var vnext: List? = null, - var servers: List? = null, - /*Blackhole*/ - var response: Response? = null, - /*DNS*/ - val network: String? = null, - val address: String? = null, - val port: Int? = null, - /*Freedom*/ - var domainStrategy: String? = null, - val redirect: String? = null, - val userLevel: Int? = null, - /*Loopback*/ - val inboundTag: String? = null) { - - data class VnextBean(var address: String = "", - var port: Int = DEFAULT_PORT, - var users: List) { - - data class UsersBean(var id: String = "", - var security: String = DEFAULT_SECURITY, - var level: Int = DEFAULT_LEVEL, - var encryption: String = "", - var flow: String = "") - } - - data class ServersBean(var address: String = "", - var method: String = "chacha20-poly1305", - var ota: Boolean = false, - var password: String = "", - var port: Int = DEFAULT_PORT, - var level: Int = DEFAULT_LEVEL, - val email: String? = null, - val flow: String? = null, - val ivCheck: Boolean? = null, - var users: List? = null) { - - - data class SocksUsersBean(var user: String = "", - var pass: String = "", - var level: Int = DEFAULT_LEVEL) - } - - data class Response(var type: String) - } - - data class StreamSettingsBean(var network: String = DEFAULT_NETWORK, - var security: String = "", - var tcpSettings: TcpSettingsBean? = null, - var kcpSettings: KcpSettingsBean? = null, - var wsSettings: WsSettingsBean? = null, - var httpSettings: HttpSettingsBean? = null, - var tlsSettings: TlsSettingsBean? = null, - var quicSettings: QuicSettingBean? = null, - var xtlsSettings: TlsSettingsBean? = null, - var grpcSettings: GrpcSettingsBean? = null, - val dsSettings: Any? = null, - val sockopt: Any? = null - ) { - - data class TcpSettingsBean(var header: HeaderBean = HeaderBean(), - val acceptProxyProtocol: Boolean? = null) { - data class HeaderBean(var type: String = "none", - var request: RequestBean? = null, - var response: Any? = null) { - data class RequestBean(var path: List = ArrayList(), - var headers: HeadersBean = HeadersBean(), - val version: String? = null, - val method: String? = null) { - data class HeadersBean(var Host: List = ArrayList(), - @SerializedName("User-Agent") - val userAgent: List? = null, - @SerializedName("Accept-Encoding") - val acceptEncoding: List? = null, - val Connection: List? = null, - val Pragma: String? = null) - } - } - } - - data class KcpSettingsBean(var mtu: Int = 1350, - var tti: Int = 50, - var uplinkCapacity: Int = 12, - var downlinkCapacity: Int = 100, - var congestion: Boolean = false, - var readBufferSize: Int = 1, - var writeBufferSize: Int = 1, - var header: HeaderBean = HeaderBean(), - var seed: String? = null) { - data class HeaderBean(var type: String = "none") - } - - data class WsSettingsBean(var path: String = "", - var headers: HeadersBean = HeadersBean(), - val maxEarlyData: Int? = null, - val useBrowserForwarding: Boolean? = null, - val acceptProxyProtocol: Boolean? = null) { - data class HeadersBean(var Host: String = "") - } - - data class HttpSettingsBean(var host: List = ArrayList(), - var path: String = "") - - data class TlsSettingsBean(var allowInsecure: Boolean = false, - var serverName: String = "", - val alpn: List? = null, - val minVersion: String? = null, - val maxVersion: String? = null, - val preferServerCipherSuites: Boolean? = null, - val cipherSuites: String? = null, - val fingerprint: String? = null, - val certificates: List? = null, - val disableSystemRoot: Boolean? = null, - val enableSessionResumption: Boolean? = null) - - data class QuicSettingBean(var security: String = "none", - var key: String = "", - var header: HeaderBean = HeaderBean()) { - data class HeaderBean(var type: String = "none") - } - - data class GrpcSettingsBean(var serviceName: String = "", - var multiMode: Boolean? = null) - - fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?, - quicSecurity: String?, key: String?, mode: String?, serviceName: String?): String { - var sni = "" - network = transport - when (network) { - "tcp" -> { - val tcpSetting = TcpSettingsBean() - if (headerType == HTTP) { - tcpSetting.header.type = HTTP - if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { - val requestObj = TcpSettingsBean.HeaderBean.RequestBean() - 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 - } - } else { - tcpSetting.header.type = "none" - sni = host ?: "" - } - tcpSettings = tcpSetting - } - "kcp" -> { - val kcpsetting = KcpSettingsBean() - kcpsetting.header.type = headerType ?: "none" - if (seed.isNullOrEmpty()) { - kcpsetting.seed = null - } else { - kcpsetting.seed = seed - } - kcpSettings = kcpsetting - } - "ws" -> { - val wssetting = WsSettingsBean() - wssetting.headers.Host = host ?: "" - sni = wssetting.headers.Host - wssetting.path = path ?: "/" - wsSettings = wssetting - } - "h2", "http" -> { - network = "h2" - val h2Setting = HttpSettingsBean() - h2Setting.host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() } - sni = h2Setting.host.getOrNull(0) ?: sni - h2Setting.path = path ?: "/" - httpSettings = h2Setting - } - "quic" -> { - val quicsetting = QuicSettingBean() - quicsetting.security = quicSecurity ?: "none" - quicsetting.key = key ?: "" - quicsetting.header.type = headerType ?: "none" - quicSettings = quicsetting - } - "grpc" -> { - val grpcSetting = GrpcSettingsBean() - grpcSetting.multiMode = mode == "multi" - grpcSetting.serviceName = serviceName ?: "" - sni = host ?: "" - grpcSettings = grpcSetting - } - } - return sni - } - - fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String) { - security = streamSecurity - val tlsSetting = TlsSettingsBean( - allowInsecure = allowInsecure, - serverName = sni - ) - if (security == TLS) { - tlsSettings = tlsSetting - } else if (security == XTLS) { - xtlsSettings = tlsSetting - } - } - } - - data class MuxBean(var enabled: Boolean, var concurrency: Int = 8) - - fun getServerAddress(): String? { - if (protocol.equals(EConfigType.VMESS.name, true) - || protocol.equals(EConfigType.VLESS.name, true)) { - return settings?.vnext?.get(0)?.address - } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) - || protocol.equals(EConfigType.SOCKS.name, true) - || protocol.equals(EConfigType.TROJAN.name, true)) { - return settings?.servers?.get(0)?.address - } - return null - } - - fun getServerPort(): Int? { - if (protocol.equals(EConfigType.VMESS.name, true) - || protocol.equals(EConfigType.VLESS.name, true)) { - return settings?.vnext?.get(0)?.port - } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) - || protocol.equals(EConfigType.SOCKS.name, true) - || protocol.equals(EConfigType.TROJAN.name, true)) { - return settings?.servers?.get(0)?.port - } - return null - } - - fun getPassword(): String? { - if (protocol.equals(EConfigType.VMESS.name, true) - || protocol.equals(EConfigType.VLESS.name, true)) { - return settings?.vnext?.get(0)?.users?.get(0)?.id - } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) - || protocol.equals(EConfigType.TROJAN.name, true)) { - return settings?.servers?.get(0)?.password - } else if (protocol.equals(EConfigType.SOCKS.name, true)) { - return settings?.servers?.get(0)?.users?.get(0)?.pass - } - return null - } - - fun getSecurityEncryption(): String? { - return when { - protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security - protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption - protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method - else -> null - } - } - - fun getTransportSettingDetails(): List? { - if (protocol.equals(EConfigType.VMESS.name, true) - || protocol.equals(EConfigType.VLESS.name, true)) { - val transport = streamSettings?.network ?: return null - return when (transport) { - "tcp" -> { - val tcpSetting = streamSettings?.tcpSettings ?: return null - listOf(tcpSetting.header.type, - tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(), - tcpSetting.header.request?.path?.joinToString().orEmpty()) - } - "kcp" -> { - val kcpSetting = streamSettings?.kcpSettings ?: return null - listOf(kcpSetting.header.type, - "", - kcpSetting.seed.orEmpty()) - } - "ws" -> { - val wsSetting = streamSettings?.wsSettings ?: return null - listOf("", - wsSetting.headers.Host, - wsSetting.path) - } - "h2" -> { - val h2Setting = streamSettings?.httpSettings ?: return null - listOf("", - h2Setting.host.joinToString(), - h2Setting.path) - } - "quic" -> { - val quicSetting = streamSettings?.quicSettings ?: return null - listOf(quicSetting.header.type, - quicSetting.security, - quicSetting.key) - } - "grpc" -> { - val grpcSetting = streamSettings?.grpcSettings ?: return null - listOf(if (grpcSetting.multiMode == true) "multi" else "gun", - "", - grpcSetting.serviceName) - } - else -> null - } - } - return null - } - } - - data class DnsBean(var servers: ArrayList? = null, - var hosts: Map? = null, - val clientIp: String? = null, - val disableCache: Boolean? = null, - val queryStrategy: String? = null, - val tag: String? = null - ) { - data class ServersBean(var address: String = "", - var port: Int? = null, - var domains: List? = null, - var expectIPs: List? = null, - val clientIp: String? = null) - } - - data class RoutingBean(var domainStrategy: String, - val domainMatcher: String? = null, - var rules: ArrayList, - val balancers: List? = null) { - - data class RulesBean(var type: String = "", - var ip: ArrayList? = null, - var domain: ArrayList? = null, - var outboundTag: String = "", - var balancerTag: String? = null, - var port: String? = null, - val sourcePort: String? = null, - val network: String? = null, - val source: List? = null, - val user: List? = null, - var inboundTag: List? = null, - val protocol: List? = null, - val attrs: String? = null, - val domainMatcher: String? = null - ) - } - - data class PolicyBean(var levels: Map, - var system: Any? = null) { - data class LevelBean( - var handshake: Int? = null, - var connIdle: Int? = null, - var uplinkOnly: Int? = null, - var downlinkOnly: Int? = null, - val statsUserUplink: Boolean? = null, - val statsUserDownlink: Boolean? = null, - var bufferSize: Int? = null) - } - - data class FakednsBean(var ipPool: String = "198.18.0.0/15", - 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 - } - } - return null - } - - fun toPrettyPrinting(): String { - return GsonBuilder() - .setPrettyPrinting() - .disableHtmlEscaping() - .registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start - object : TypeToken() {}.type, - JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) } - ) - .create() - .toJson(this) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt deleted file mode 100644 index a114336c..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.v2ray.ang.dto - -data class VmessQRCode(var v: String = "", - var ps: String = "", - var add: String = "", - var port: String = "", - var id: String = "", - var aid: String = "0", - var net: String = "", - var type: String = "", - var host: String = "", - var path: String = "", - var tls: String = "", - var sni: String = "") diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt deleted file mode 100644 index 9d8291c0..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.v2ray.ang.extension - -import android.content.Context -import android.os.Build -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 - -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) = putOpt(pair.first, pair.second) -fun JSONObject.putOpt(pairs: Map) = pairs.forEach { putOpt(it.key to it.value) } - -const val threshold = 1000 -const val divisor = 1024F - -fun Long.toSpeedString() = toTrafficString() + "/s" - -fun Long.toTrafficString(): String { - if (this == 0L) - return "\t\t\t0\t B" - - if (this < threshold) - return "${this.toFloat().toShortString()}\t B" - - val kib = this / divisor - if (kib < threshold) - return "${kib.toShortString()}\t KB" - - val mib = kib / divisor - if (mib < threshold) - return "${mib.toShortString()}\t MB" - - val gib = mib / divisor - if (gib < threshold) - return "${gib.toShortString()}\t GB" - - val tib = gib / divisor - if (tib < threshold) - return "${tib.toShortString()}\t TB" - - val pib = tib / divisor - if (pib < threshold) - return "${pib.toShortString()}\t PB" - - return "∞" -} - -private fun Float.toShortString(): String { - val s = "%.2f".format(this) - if (s.length <= 4) - return s - return s.substring(0, 4).removeSuffix(".") -} - -val URLConnection.responseLength: Long - get() = if (Build.VERSION.SDK_INT >= 24) contentLengthLong else contentLength.toLong() - -val URI.idnHost: String - get() = (host!!).replace("[", "").replace("]", "") \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt deleted file mode 100644 index 2998342a..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/ServiceControl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.v2ray.ang.service - -import android.app.Service - -interface ServiceControl { - fun getService(): Service - - fun startService(parameters: String) - - fun stopService() - - fun vpnProtect(socket: Int): Boolean - -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt deleted file mode 100644 index 4e4db1fb..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayProxyOnlyService.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.v2ray.ang.service - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import java.lang.ref.SoftReference - -class V2RayProxyOnlyService : Service(), ServiceControl { - override fun onCreate() { - super.onCreate() - V2RayServiceManager.serviceControl = SoftReference(this) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - V2RayServiceManager.startV2rayPoint() - return START_STICKY - } - - override fun onDestroy() { - super.onDestroy() - V2RayServiceManager.stopV2rayPoint() - } - - override fun getService(): Service { - return this - } - - override fun startService(parameters: String) { - // do nothing - } - - override fun stopService() { - stopSelf() - } - - override fun vpnProtect(socket: Int): Boolean { - return true - } - - override fun onBind(intent: Intent?): IBinder? { - return null - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt deleted file mode 100644 index 8ee39792..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayServiceManager.kt +++ /dev/null @@ -1,385 +0,0 @@ -package com.v2ray.ang.service - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.graphics.Color -import android.os.Build -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 -import com.v2ray.ang.AppConfig.TAG_DIRECT -import com.v2ray.ang.R -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.extension.toSpeedString -import com.v2ray.ang.extension.toast -import com.v2ray.ang.ui.MainActivity -import com.v2ray.ang.util.MessageUtil -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.Utils -import com.v2ray.ang.util.V2rayConfigUtil -import go.Seq -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import libv2ray.Libv2ray -import libv2ray.V2RayPoint -import libv2ray.V2RayVPNServiceSupportsSet -import rx.Observable -import rx.Subscription -import java.lang.ref.SoftReference -import kotlin.math.min - -object V2RayServiceManager { - private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0 - private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1 - private const val NOTIFICATION_ICON_THRESHOLD = 3000 - - val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback()) - 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) } - - var serviceControl: SoftReference? = 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) - } - } - var currentConfig: ServerConfig? = null - - private var lastQueryTime = 0L - private var mBuilder: NotificationCompat.Builder? = null - private var mSubscription: Subscription? = null - private var mNotificationManager: NotificationManager? = null - - fun startV2Ray(context: Context) { - if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) { - context.toast(R.string.toast_warning_pref_proxysharing_short) - }else{ - context.toast(R.string.toast_services_start) - } - val intent = if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") { - Intent(context.applicationContext, V2RayVpnService::class.java) - } else { - Intent(context.applicationContext, V2RayProxyOnlyService::class.java) - } - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } - } - - private class V2RayCallback : V2RayVPNServiceSupportsSet { - override fun shutdown(): Long { - val serviceControl = serviceControl?.get() ?: return -1 - // called by go - // shutdown the whole vpn service - return try { - serviceControl.stopService() - 0 - } catch (e: Exception) { - Log.d(ANG_PACKAGE, e.toString()) - -1 - } - } - - override fun prepare(): Long { - 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 onEmitStatus(l: Long, s: String?): Long { - //Logger.d(s) - return 0 - } - - override fun setup(s: String): Long { - val serviceControl = serviceControl?.get() ?: return -1 - //Logger.d(s) - return try { - serviceControl.startService(s) - lastQueryTime = System.currentTimeMillis() - startSpeedNotification() - 0 - } catch (e: Exception) { - Log.d(ANG_PACKAGE, e.toString()) - -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 - - 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() - } else { - MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") - cancelNotification() - } - } - } - - fun stopV2rayPoint() { - val service = serviceControl?.get()?.getService() ?: return - - if (v2rayPoint.isRunning) { - GlobalScope.launch(Dispatchers.Default) { - try { - v2rayPoint.stopLoop() - } catch (e: Exception) { - Log.d(ANG_PACKAGE, e.toString()) - } - } - } - - MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "") - cancelNotification() - - try { - service.unregisterReceiver(mMsgReceive) - } catch (e: Exception) { - Log.d(ANG_PACKAGE, e.toString()) - } - } - - private class ReceiveMessageHandler : BroadcastReceiver() { - override fun onReceive(ctx: Context?, intent: Intent?) { - val serviceControl = serviceControl?.get() ?: return - when (intent?.getIntExtra("key", 0)) { - AppConfig.MSG_REGISTER_CLIENT -> { - //Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString()) - if (v2rayPoint.isRunning) { - MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "") - } else { - MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "") - } - } - AppConfig.MSG_UNREGISTER_CLIENT -> { - // nothing to do - } - AppConfig.MSG_STATE_START -> { - // nothing to do - } - AppConfig.MSG_STATE_STOP -> { - serviceControl.stopService() - } - AppConfig.MSG_STATE_RESTART -> { - startV2rayPoint() - } - } - - when (intent?.action) { - Intent.ACTION_SCREEN_OFF -> { - Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats") - stopSpeedNotification() - } - Intent.ACTION_SCREEN_ON -> { - Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats") - startSpeedNotification() - } - } - } - } - - private fun showNotification() { - val service = serviceControl?.get()?.getService() ?: return - val startMainIntent = Intent(service, MainActivity::class.java) - val contentPendingIntent = PendingIntent.getActivity(service, - NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - } else { - PendingIntent.FLAG_UPDATE_CURRENT - }) - - val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) - stopV2RayIntent.`package` = ANG_PACKAGE - stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP) - - val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, - NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - } else { - PendingIntent.FLAG_UPDATE_CURRENT - }) - - val channelId = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel() - } else { - // If earlier version channel ID is not used - // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) - "" - } - - mBuilder = NotificationCompat.Builder(service, channelId) - .setSmallIcon(R.drawable.ic_v) - .setContentTitle(currentConfig?.remarks) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setOngoing(true) - .setShowWhen(false) - .setOnlyAlertOnce(true) - .setContentIntent(contentPendingIntent) - .addAction(R.drawable.ic_close_grey_800_24dp, - service.getString(R.string.notification_action_stop_v2ray), - stopV2RayPendingIntent) - //.build() - - //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使 - - service.startForeground(NOTIFICATION_ID, mBuilder?.build()) - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun createNotificationChannel(): String { - val channelId = "RAY_NG_M_CH_ID" - val channelName = "V2rayNG Background Service" - val chan = NotificationChannel(channelId, - channelName, NotificationManager.IMPORTANCE_HIGH) - chan.lightColor = Color.DKGRAY - chan.importance = NotificationManager.IMPORTANCE_NONE - chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE - getNotificationManager()?.createNotificationChannel(chan) - return channelId - } - - fun cancelNotification() { - val service = serviceControl?.get()?.getService() ?: return - service.stopForeground(true) - mBuilder = null - mSubscription?.unsubscribe() - mSubscription = null - } - - 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) - } else if (proxyTraffic > directTraffic) { - mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy) - } else { - mBuilder?.setSmallIcon(R.drawable.ic_stat_direct) - } - mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) - mBuilder?.setContentText(contentText) // Emui4.1 need content text even if style is set as BigTextStyle - getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build()) - } - } - - private fun getNotificationManager(): NotificationManager? { - if (mNotificationManager == null) { - val service = serviceControl?.get()?.getService() ?: return null - mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - } - return mNotificationManager - } - - fun startSpeedNotification() { - if (mSubscription == null && - v2rayPoint.isRunning && - settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true) { - var lastZeroSpeed = false - val outboundTags = currentConfig?.getAllOutboundTags() - outboundTags?.remove(TAG_DIRECT) - - mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS) - .subscribe { - val queryTime = System.currentTimeMillis() - val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0 - var proxyTotal = 0L - val text = StringBuilder() - outboundTags?.forEach { - val up = v2rayPoint.queryStats(it, "uplink") - val down = v2rayPoint.queryStats(it, "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) - if (!zeroSpeed || !lastZeroSpeed) { - if (proxyTotal == 0L) { - appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0) - } - appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds, - directDownlink / sinceLastQueryInSeconds) - updateNotification(text.toString(), proxyTotal, directDownlink + directUplink) - } - lastZeroSpeed = zeroSpeed - lastQueryTime = queryTime - } - } - } - - private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) { - var n = name ?: "no tag" - n = n.substring(0, min(n.length, 6)) - text.append(n) - for (i in n.length..6 step 2) { - text.append("\t") - } - text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n") - } - - fun stopSpeedNotification() { - if (mSubscription != null) { - mSubscription?.unsubscribe() //stop queryStats - mSubscription = null - updateNotification(currentConfig?.remarks, 0, 0) - } - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt deleted file mode 100644 index 240e6db8..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt +++ /dev/null @@ -1,258 +0,0 @@ -package com.v2ray.ang.service - -import android.app.* -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.* -import android.os.Build -import android.os.ParcelFileDescriptor -import android.os.StrictMode -import androidx.annotation.RequiresApi -import android.util.Log -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -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.io.File -import java.lang.ref.SoftReference - -class V2RayVpnService : VpnService(), ServiceControl { - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } - - private lateinit var mInterface: ParcelFileDescriptor - - /** - * 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() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) - .build() - } - - private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } - - @delegate:RequiresApi(Build.VERSION_CODES.P) - private val defaultNetworkCallback by lazy { - object : ConnectivityManager.NetworkCallback() { - 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) - } - } - } - - override fun onCreate() { - super.onCreate() - - val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() - StrictMode.setThreadPolicy(policy) - V2RayServiceManager.serviceControl = SoftReference(this) - } - - override fun onRevoke() { - stopV2Ray() - } - - override fun onLowMemory() { - stopV2Ray() - super.onLowMemory() - } - - override fun onDestroy() { - super.onDestroy() - stopV2Ray() - } - - private fun setup(parameters: String) { - - val prepare = prepare(this) - if (prepare != null) { - return - } - - // 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" - - 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]) - } - } - - if(!enableLocalDns) { - Utils.getVpnDnsServers() - .forEach { - if (Utils.isPureIpAddress(it)) { - builder.addDnsServer(it) - } - } - } - - builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty()) - - if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) { - val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET) - val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false - apps?.forEach { - try { - if (bypassApps) - builder.addDisallowedApplication(it) - else - builder.addAllowedApplication(it) - } catch (e: PackageManager.NameNotFoundException) { - //Logger.d(e) - } - } - } - - // Close the old interface since the parameters have been changed. - try { - mInterface.close() - } catch (ignored: Exception) { - // ignored - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - try { - connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) - } catch (e: Exception) { - e.printStackTrace() - } - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - builder.setMetered(false) - } - - // Create a new interface using the builder and save the parameters. - try { - mInterface = builder.establish()!! - } catch (e: Exception) { - // non-nullable lateinit var - e.printStackTrace() - stopV2Ray() - } - - sendFd() - } - - private fun sendFd() { - val fd = mInterface.fileDescriptor - val path = File(Utils.packagePath(applicationContext), "sock_path").absolutePath - - GlobalScope.launch(Dispatchers.IO) { - var tries = 0 - while (true) try { - Thread.sleep(1000L shl tries) - Log.d(packageName, "sendFd tries: $tries") - LocalSocket().use { localSocket -> - localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) - localSocket.setFileDescriptorsForSend(arrayOf(fd)) - localSocket.outputStream.write(42) - } - break - } catch (e: Exception) { - Log.d(packageName, e.toString()) - if (tries > 5) break - tries += 1 - } - } - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - V2RayServiceManager.startV2rayPoint() - return START_STICKY - //return super.onStartCommand(intent, flags, startId) - } - - private fun stopV2Ray(isForced: Boolean = true) { -// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "") -// val emptyInfo = VpnNetworkInfo() -// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo) -// saveVpnNetworkInfo(configName, info) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - try { - connectivity.unregisterNetworkCallback(defaultNetworkCallback) - } catch (ignored: Exception) { - // ignored - } - } - - V2RayServiceManager.stopV2rayPoint() - - if (isForced) { - //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped - //It's strage but true. - //This can be verified by putting stopself() behind and call stopLoop and startLoop - //in a row for several times. You will find that later created v2ray core report port in use - //which means the first v2ray core somehow failed to stop and release the port. - stopSelf() - - try { - mInterface.close() - } catch (ignored: Exception) { - // ignored - } - - } - } - - override fun getService(): Service { - return this - } - - override fun startService(parameters: String) { - setup(parameters) - } - - override fun stopService() { - stopV2Ray(true) - } - - override fun vpnProtect(socket: Int): Boolean { - return protect(socket) - } - -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt deleted file mode 100644 index 3f11b649..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseActivity.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.v2ray.ang.ui - -import androidx.appcompat.app.AppCompatActivity -import android.view.MenuItem - -abstract class BaseActivity : AppCompatActivity() { - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - android.R.id.home -> { - onBackPressed() - true - } - else -> super.onOptionsItemSelected(item) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt deleted file mode 100644 index 7128010f..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.text.method.ScrollingMovementMethod -import android.view.Menu -import android.view.MenuItem -import android.view.View -import com.v2ray.ang.AppConfig.ANG_PACKAGE -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ActivityLogcatBinding -import com.v2ray.ang.extension.toast -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 - -class LogcatActivity : BaseActivity() { - private lateinit var binding: ActivityLogcatBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityLogcatBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) - - title = getString(R.string.title_logcat) - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - logcat(false) - } - - private fun logcat(shouldFlushLog: Boolean) { - - try { - binding.pbWaiting.visibility = View.VISIBLE - - GlobalScope.launch(Dispatchers.Default) { - if (shouldFlushLog) { - val lst = LinkedHashSet() - lst.add("logcat") - lst.add("-c") - val process = Runtime.getRuntime().exec(lst.toTypedArray()) - process.waitFor() - } - val lst = LinkedHashSet() - lst.add("logcat") - lst.add("-d") - lst.add("-v") - lst.add("time") - lst.add("-s") - lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err") - val process = Runtime.getRuntime().exec(lst.toTypedArray()) -// val bufferedReader = BufferedReader( -// InputStreamReader(process.inputStream)) -// val allText = bufferedReader.use(BufferedReader::readText) - val allText = process.inputStream.bufferedReader().use { it.readText() } - launch(Dispatchers.Main) { - binding.tvLogcat.text = allText - binding.tvLogcat.movementMethod = ScrollingMovementMethod() - binding.pbWaiting.visibility = View.GONE - Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) } - } - } - } catch (e: IOException) { - e.printStackTrace() - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_logcat, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.copy_all -> { - Utils.setClipboard(this, binding.tvLogcat.text.toString()) - toast(R.string.toast_success) - true - } - R.id.delete -> { - logcat(true) - true - } - else -> super.onOptionsItemSelected(item) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt deleted file mode 100644 index c876fe3e..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt +++ /dev/null @@ -1,560 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.content.Intent -import android.net.Uri -import android.net.VpnService -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.result.contract.ActivityResultContracts -import androidx.activity.viewModels -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.dto.EConfigType -import com.v2ray.ang.extension.toast -import com.v2ray.ang.helper.SimpleItemTouchHelperCallback -import com.v2ray.ang.service.V2RayServiceManager -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.Utils -import com.v2ray.ang.viewmodel.MainViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import libv2ray.Libv2ray -import me.drakeet.support.toast.ToastCompat -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit - -class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { - private lateinit var binding: ActivityMainBinding - - private val adapter by lazy { MainRecyclerAdapter(this) } - 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 requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - startV2Ray() - } - } - private var mItemTouchHelper: ItemTouchHelper? = null - val mainViewModel: MainViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) - title = getString(R.string.title_server) - setSupportActionBar(binding.toolbar) - - binding.fab.setOnClickListener { - if (mainViewModel.isRunning.value == true) { - Utils.stopVService(this) - } else if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") { - val intent = VpnService.prepare(this) - if (intent == null) { - startV2Ray() - } else { - requestVpnPermission.launch(intent) - } - } else { - startV2Ray() - } - } - binding.layoutTest.setOnClickListener { - if (mainViewModel.isRunning.value == true) { - binding.tvTestState.text = getString(R.string.connection_test_testing) - mainViewModel.testCurrentServerRealPing() - } else { -// tv_test_state.text = getString(R.string.connection_test_fail) - } - } - - binding.recyclerView.setHasFixedSize(true) - binding.recyclerView.layoutManager = LinearLayoutManager(this) - binding.recyclerView.adapter = adapter - - val callback = SimpleItemTouchHelperCallback(adapter) - mItemTouchHelper = ItemTouchHelper(callback) - mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) - - - val toggle = ActionBarDrawerToggle( - this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) - binding.drawerLayout.addDrawerListener(toggle) - toggle.syncState() - binding.navView.setNavigationItemSelectedListener(this) - binding.version.text = "v${BuildConfig.VERSION_NAME} (${Libv2ray.checkVersionX()})" - - setupViewModelObserver() - migrateLegacy() - } - - private fun setupViewModelObserver() { - mainViewModel.updateListAction.observe(this) { - val index = it ?: return@observe - 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 - adapter.isRunning = isRunning - if (isRunning) { - binding.fab.setImageResource(R.drawable.ic_v) - binding.tvTestState.text = 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.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)) - } - } - } - } - } - - fun startV2Ray() { - if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) { - return - } - showCircle() -// toast(R.string.toast_services_start) - V2RayServiceManager.startV2Ray(this) - hideCircle() - } - - public override fun onResume() { - super.onResume() - mainViewModel.reloadServerList() - } - - public override fun onPause() { - super.onPause() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_main, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.import_qrcode -> { - importQRcode(true) - true - } - R.id.import_clipboard -> { - importClipboard() - true - } - R.id.import_manually_vmess -> { - startActivity(Intent().putExtra("createConfigType", EConfigType.VMESS.value). - setClass(this, ServerActivity::class.java)) - true - } - R.id.import_manually_ss -> { - startActivity(Intent().putExtra("createConfigType", EConfigType.SHADOWSOCKS.value). - setClass(this, ServerActivity::class.java)) - true - } - R.id.import_manually_socks -> { - startActivity(Intent().putExtra("createConfigType", EConfigType.SOCKS.value). - setClass(this, ServerActivity::class.java)) - true - } - R.id.import_config_custom_clipboard -> { - importConfigCustomClipboard() - true - } - R.id.import_config_custom_local -> { - importConfigCustomLocal() - true - } - R.id.import_config_custom_url -> { - importConfigCustomUrlClipboard() - true - } - R.id.import_config_custom_url_scan -> { - importQRcode(false) - true - } - -// R.id.sub_setting -> { -// startActivity() -// true -// } - - R.id.sub_update -> { - importConfigViaSub() - true - } - - R.id.export_all -> { - if (AngConfigManager.shareNonCustomConfigsToClipboard(this, mainViewModel.serverList) == 0) { - toast(R.string.toast_success) - } else { - toast(R.string.toast_failure) - } - true - } - - R.id.ping_all -> { - mainViewModel.testAllTcping() - true - } - -// R.id.settings -> { -// startActivity("isRunning" to isRunning) -// true -// } -// R.id.logcat -> { -// startActivity() -// true -// } - else -> super.onOptionsItemSelected(item) - } - - - /** - * import config from qrcode - */ - fun importQRcode(forConfig: Boolean): Boolean { -// try { -// startActivityForResult(Intent("com.google.zxing.client.android.SCAN") -// .addCategory(Intent.CATEGORY_DEFAULT) -// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode) -// } catch (e: Exception) { - RxPermissions(this) - .request(Manifest.permission.CAMERA) - .subscribe { - if (it) - if (forConfig) - scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) - else - scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java)) - else - toast(R.string.toast_permission_denied) - } -// } - return true - } - - private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - importBatchConfig(it.data?.getStringExtra("SCAN_RESULT")) - } - } - - private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT")) - } - } - - /** - * import config from clipboard - */ - fun importClipboard() - : Boolean { - try { - val clipboard = Utils.getClipboard(this) - importBatchConfig(clipboard) - } catch (e: Exception) { - e.printStackTrace() - return false - } - 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) - } - } - - fun importConfigCustomClipboard() - : Boolean { - try { - val configText = Utils.getClipboard(this) - if (TextUtils.isEmpty(configText)) { - toast(R.string.toast_none_data_clipboard) - return false - } - importCustomizeConfig(configText) - return true - } catch (e: Exception) { - e.printStackTrace() - return false - } - } - - /** - * import config from local config file - */ - fun importConfigCustomLocal(): Boolean { - try { - showFileChooser() - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - fun importConfigCustomUrlClipboard() - : Boolean { - try { - val url = Utils.getClipboard(this) - if (TextUtils.isEmpty(url)) { - toast(R.string.toast_none_data_clipboard) - return false - } - return importConfigCustomUrl(url) - } catch (e: Exception) { - e.printStackTrace() - return false - } - } - - /** - * import config from url - */ - fun importConfigCustomUrl(url: String?): Boolean { - try { - if (!Utils.isValidUrl(url)) { - toast(R.string.toast_invalid_url) - return false - } - GlobalScope.launch(Dispatchers.IO) { - val configText = try { - Utils.getUrlContentWithCustomUserAgent(url) - } catch (e: Exception) { - e.printStackTrace() - "" - } - launch(Dispatchers.Main) { - importCustomizeConfig(configText) - } - } - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - /** - * 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) - } - } - } - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - /** - * show file chooser - */ - private fun showFileChooser() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "*/*" - intent.addCategory(Intent.CATEGORY_OPENABLE) - - try { - chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) - } catch (ex: android.content.ActivityNotFoundException) { - toast(R.string.toast_require_file_manager) - } - } - - private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - val uri = it.data?.data - if (it.resultCode == RESULT_OK && uri != null) { - readContentFromUri(uri) - } - } - - /** - * read content from uri - */ - private fun readContentFromUri(uri: Uri) { - RxPermissions(this) - .request(Manifest.permission.READ_EXTERNAL_STORAGE) - .subscribe { - if (it) { - try { - contentResolver.openInputStream(uri).use { input -> - importCustomizeConfig(input?.bufferedReader()?.readText()) - } - } catch (e: Exception) { - e.printStackTrace() - } - } else - toast(R.string.toast_permission_denied) - } - } - - /** - * import customize config - */ - 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) - } catch (e: Exception) { - ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show() - e.printStackTrace() - return - } - } - -// val mConnection = object : ServiceConnection { -// override fun onServiceDisconnected(name: ComponentName?) { -// } -// -// override fun onServiceConnected(name: ComponentName?, service: IBinder?) { -// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "") -// } -// } - - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - if (keyCode == KeyEvent.KEYCODE_BACK) { - 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)) - } - 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.promotion -> { - Utils.openUri(this, AppConfig.promotionUrl) - } - R.id.donate -> { -// startActivity() - } - R.id.logcat -> { - startActivity(Intent(this, LogcatActivity::class.java)) - } - } - binding.drawerLayout.closeDrawer(GravityCompat.START) - return true - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt deleted file mode 100644 index 92e5c681..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt +++ /dev/null @@ -1,231 +0,0 @@ -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.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.google.gson.Gson -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.databinding.ItemQrcodeBinding -import com.v2ray.ang.databinding.ItemRecyclerFooterBinding -import com.v2ray.ang.databinding.ItemRecyclerMainBinding -import com.v2ray.ang.dto.EConfigType -import com.v2ray.ang.dto.SubscriptionItem -import com.v2ray.ang.extension.toast -import com.v2ray.ang.helper.ItemTouchHelperAdapter -import com.v2ray.ang.helper.ItemTouchHelperViewHolder -import com.v2ray.ang.service.V2RayServiceManager -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.MmkvManager -import com.v2ray.ang.util.Utils -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit - -class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter() - , ItemTouchHelperAdapter { - companion object { - private const val VIEW_TYPE_ITEM = 1 - private const val VIEW_TYPE_FOOTER = 2 - } - - 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 share_method: Array by lazy { - mActivity.resources.getStringArray(R.array.share_method) - } - var isRunning = false - - override fun getItemCount() = mActivity.mainViewModel.serverList.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 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)) - } else { - holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing)) - } - holder.itemMainBinding.tvSubscription.text = "" - val json = subStorage?.decodeString(config.subscriptionId) - if (!json.isNullOrBlank()) { - val sub = Gson().fromJson(json, SubscriptionItem::class.java) - holder.itemMainBinding.tvSubscription.text = sub.remarks - } - - var shareOptions = share_method.asList() - when (config.configType) { - EConfigType.CUSTOM -> { - holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config) - shareOptions = shareOptions.takeLast(1) - } - EConfigType.VLESS -> { - holder.itemMainBinding.tvType.text = config.configType.name - } - else -> { - holder.itemMainBinding.tvType.text = config.configType.name.lowercase() - } - } - holder.itemMainBinding.tvStatistics.text = "${outbound?.getServerAddress()} : ${outbound?.getServerPort()}" - - holder.itemMainBinding.layoutShare.setOnClickListener { - AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i -> - try { - when (i) { - 0 -> { - if (config.configType == EConfigType.CUSTOM) { - shareFullContent(guid) - } else { - val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity)) - ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid)) - AlertDialog.Builder(mActivity).setView(ivBinding.root).show() - } - } - 1 -> { - if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) { - mActivity.toast(R.string.toast_success) - } else { - mActivity.toast(R.string.toast_failure) - } - } - 2 -> shareFullContent(guid) - else -> mActivity.toast("else") - } - } catch (e: Exception) { - e.printStackTrace() - } - }.show() - } - - holder.itemMainBinding.layoutEdit.setOnClickListener { - val intent = Intent().putExtra("guid", guid) - .putExtra("isRunning", isRunning) - if (config.configType == EConfigType.CUSTOM) { - mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java)) - } else { - mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java)) - } - } - holder.itemMainBinding.layoutRemove.setOnClickListener { - if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) { - mActivity.mainViewModel.removeServer(guid) - notifyItemRemoved(position) - notifyItemRangeChanged(position, mActivity.mainViewModel.serverList.size) - } - } - - holder.itemMainBinding.infoContainer.setOnClickListener { - 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 (isRunning) { - mActivity.showCircle() - Utils.stopVService(mActivity) - Observable.timer(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - V2RayServiceManager.startV2Ray(mActivity) - mActivity.hideCircle() - } - } - } - } - } - if (holder is FooterViewHolder) { - //if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) { - if (true) { - holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE - } else { - holder.itemFooterBinding.layoutEdit.setOnClickListener { - Utils.openUri(mActivity, "${Utils.decode(AppConfig.promotionUrl)}?t=${System.currentTimeMillis()}") - } - } - } - } - - private fun shareFullContent(guid: String) { - if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) { - mActivity.toast(R.string.toast_success) - } else { - mActivity.toast(R.string.toast_failure) - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { - return when (viewType) { - VIEW_TYPE_ITEM -> - MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - else -> - FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - } - } - - override fun getItemViewType(position: Int): Int { - return if (position == mActivity.mainViewModel.serverList.size) { - VIEW_TYPE_FOOTER - } else { - VIEW_TYPE_ITEM - } - } - - open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - fun onItemSelected() { - itemView.setBackgroundColor(Color.LTGRAY) - } - - fun onItemClear() { - itemView.setBackgroundColor(0) - } - } - - class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) : - BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder - - class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) : - BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder - - override fun onItemDismiss(position: Int) { - val guid = mActivity.mainViewModel.serverList.getOrNull(position) ?: return - if (guid != mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) { -// mActivity.alert(R.string.del_config_comfirm) { -// positiveButton(android.R.string.ok) { - mActivity.mainViewModel.removeServer(guid) - notifyItemRemoved(position) -// } -// show() -// } - } - } - - override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { - mActivity.mainViewModel.swapServer(fromPosition, toPosition) - notifyItemMoved(fromPosition, toPosition) - // position is changed, since position is used by click callbacks, need to update range - if (toPosition > fromPosition) - notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1) - else - notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1) - return true - } - - override fun onItemMoveCompleted() { - // do nothing - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt deleted file mode 100644 index 435bd1e7..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt +++ /dev/null @@ -1,286 +0,0 @@ -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 com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.ANG_PACKAGE -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.Utils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.net.URL - -class PerAppProxyActivity : BaseActivity() { - private lateinit var binding: ActivityBypassListBinding - - private var adapter: PerAppProxyAdapter? = null - private var appsAll: List? = null - private val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityBypassListBinding.inflate(layoutInflater) - 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) - - AppManagerUtil.rxLoadNetworkAppList(this) - .subscribeOn(Schedulers.io()) - .map { - if (blacklist != null) { - it.forEach { one -> - if ((blacklist.contains(one.packageName))) { - one.isSelected = 1 - } else { - one.isSelected = 0 - } - } - val comparator = Comparator { p1, p2 -> - when { - p1.isSelected > p2.isSelected -> -1 - p1.isSelected == p2.isSelected -> 0 - else -> 1 - } - } - it.sortedWith(comparator) - } else { - val comparator = object : Comparator { - val collator = Collator.getInstance() - override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName) - } - it.sortedWith(comparator) - } - } -// .map { -// val comparator = object : Comparator { -// val collator = Collator.getInstance() -// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName) -// } -// it.sortedWith(comparator) -// } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - appsAll = it - adapter = PerAppProxyAdapter(this, it, blacklist) - binding.recyclerView.adapter = adapter - binding.pbWaiting.visibility = View.GONE - } - - 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 - } - }) - - binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked -> - defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_PER_APP_PROXY, isChecked).apply() - } - binding.switchPerAppProxy.isChecked = defaultSharedPreferences.getBoolean(AppConfig.PREF_PER_APP_PROXY, false) - - binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked -> - defaultSharedPreferences.edit().putBoolean(AppConfig.PREF_BYPASS_APPS, isChecked).apply() - } - binding.switchBypassApps.isChecked = defaultSharedPreferences.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) - - val key = v.text.toString().uppercase() - val apps = ArrayList() - 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 - } - } - } - - override fun onPause() { - super.onPause() - adapter?.let { - defaultSharedPreferences.edit().putStringSet(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist).apply() - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_bypass_list, menu) - return super.onCreateOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.select_all -> adapter?.let { - val pkgNames = it.apps.map { it.packageName } - if (it.blacklist.containsAll(pkgNames)) { - it.apps.forEach { - val packageName = it.packageName - adapter?.blacklist!!.remove(packageName) - } - } else { - it.apps.forEach { - val packageName = it.packageName - adapter?.blacklist!!.add(packageName) - } - - } - it.notifyDataSetChanged() - true - } ?: false - R.id.select_proxy_app -> { - selectProxyApp() - - true - } - else -> super.onOptionsItemSelected(item) - } - - 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() - "" - } - launch(Dispatchers.Main) { - Log.d(ANG_PACKAGE, content) - selectProxyApp(content) - toast(R.string.toast_success) - } - } - } - - private fun selectProxyApp(content: String): Boolean { - try { - var proxyApps = content - if (TextUtils.isEmpty(content)) { - val assets = Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt") - proxyApps = assets.lines().toString() - } - if (TextUtils.isEmpty(proxyApps)) { - return false - } - - 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) - println(packageName) - return@block - } - } - it.notifyDataSetChanged() - } - } else { - adapter?.let { - it.apps.forEach block@{ - val packageName = it.packageName - Log.d(ANG_PACKAGE, packageName) - if (proxyApps.indexOf(packageName) >= 0) { - adapter?.blacklist!!.add(packageName) - println(packageName) - return@block - } - } - it.notifyDataSetChanged() - } - } - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt deleted file mode 100644 index dc681fdc..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.v2ray.ang.ui - -import android.graphics.Color -import android.os.Bundle -import com.v2ray.ang.R -import androidx.fragment.app.Fragment -import com.google.android.material.tabs.TabLayoutMediator -import com.v2ray.ang.AppConfig -import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding - -class RoutingSettingsActivity : BaseActivity() { - private lateinit var binding: ActivityRoutingSettingsBinding - - private val titles: Array by lazy { - resources.getStringArray(R.array.routing_tag) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityRoutingSettingsBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) - - title = getString(R.string.routing_settings_title) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - val fragments = ArrayList() - fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT)) - fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT)) - fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)) - - val adapter = FragmentAdapter(this, fragments) - binding.viewpager.adapter = adapter - binding.tablayout.setTabTextColors(Color.BLACK, Color.RED) - TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position -> - tab.text = titles[position] - }.attach() - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt deleted file mode 100644 index 7283537e..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.app.Activity.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 android.view.MenuInflater -import androidx.activity.result.contract.ActivityResultContracts -import com.tbruyelle.rxpermissions.RxPermissions -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.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 - companion object { - private const val routing_arg = "routing_arg" - } - - val defaultSharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - binding = FragmentRoutingSettingsBinding.inflate(layoutInflater) - return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false) - } - - fun newInstance(arg: String): Fragment { - val fragment = RoutingSettingsFragment() - val bundle = Bundle() - bundle.putString(routing_arg, arg) - fragment.arguments = bundle - return 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!!) - - setHasOptionsMenu(true) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.menu_routing, menu) - return super.onCreateOptionsMenu(menu, inflater) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.save_routing -> { - val content = binding.etRoutingContent.text.toString() - defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply() - activity?.toast(R.string.toast_success) - true - } - R.id.del_routing -> { - binding.etRoutingContent.text = null - true - } - R.id.scan_replace -> { - scanQRcode(true) - true - } - R.id.scan_append -> { - scanQRcode(false) - true - } - R.id.default_rules -> { - setDefaultRules() - true - } - else -> super.onOptionsItemSelected(item) - } - - private fun saveRouting() { - val content = binding.etRoutingContent.text.toString() - defaultSharedPreferences.edit().putString(requireArguments().getString(routing_arg), content).apply() - activity?.toast(R.string.toast_success) - } - - fun scanQRcode(forReplace: Boolean): Boolean { -// try { -// startActivityForResult(Intent("com.google.zxing.client.android.SCAN") -// .addCategory(Intent.CATEGORY_DEFAULT) -// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode) -// } catch (e: Exception) { - RxPermissions(requireActivity()) - .request(Manifest.permission.CAMERA) - .subscribe { - if (it) - if (forReplace) - scanQRCodeForReplace.launch(Intent(activity, ScannerActivity::class.java)) - else - scanQRCodeForAppend.launch(Intent(activity, ScannerActivity::class.java)) - else - activity?.toast(R.string.toast_permission_denied) - } -// } - return true - } - - private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - val content = it.data?.getStringExtra("SCAN_RESULT") - binding.etRoutingContent.text = Utils.getEditable(content!!) - } - } - - private val scanQRCodeForAppend = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - val content = it.data?.getStringExtra("SCAN_RESULT") - binding.etRoutingContent.text = Utils.getEditable("${binding.etRoutingContent.text},$content") - } - } - - fun setDefaultRules(): Boolean { - var url = AppConfig.v2rayCustomRoutingListUrl - when (requireArguments().getString(routing_arg)) { - AppConfig.PREF_V2RAY_ROUTING_AGENT -> { - url += AppConfig.TAG_AGENT - } - AppConfig.PREF_V2RAY_ROUTING_DIRECT -> { - url += AppConfig.TAG_DIRECT - } - AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> { - url += AppConfig.TAG_BLOCKED - } - } - - activity?.toast(R.string.msg_downloading_content) - GlobalScope.launch(Dispatchers.IO) { - val content = try { - URL(url).readText() - } catch (e: Exception) { - e.printStackTrace() - "" - } - launch(Dispatchers.Main) { - val routingList = if (TextUtils.isEmpty(content)) { - Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag") - } else { - content - } - binding.etRoutingContent.text = Utils.getEditable(routingList) - saveRouting() - //toast(R.string.toast_success) - } - } - return true - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt deleted file mode 100644 index c0e6563d..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt +++ /dev/null @@ -1,45 +0,0 @@ -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.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts -import com.v2ray.ang.extension.toast - -class ScScannerActivity : BaseActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_none) - importQRcode() - } - - fun importQRcode(): Boolean { - RxPermissions(this) - .request(Manifest.permission.CAMERA) - .subscribe { - if (it) - scanQRCode.launch(Intent(this, ScannerActivity::class.java)) - else - toast(R.string.toast_permission_denied) - } - - return true - } - - private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == RESULT_OK) { - val count = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "") - if (count > 0) { - toast(R.string.toast_success) - } else { - toast(R.string.toast_failure) - } - startActivity(Intent(this, MainActivity::class.java)) - } - finish() - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt deleted file mode 100644 index e4982d83..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.app.Activity -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.view.Menu -import android.view.MenuItem -import androidx.activity.result.contract.ActivityResultContracts -import com.google.zxing.BarcodeFormat -import com.tbruyelle.rxpermissions.RxPermissions -import com.v2ray.ang.R -import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.QRCodeDecoder - -class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler { - - private var mScannerView: ZXingScannerView? = null - - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - mScannerView = ZXingScannerView(this) // Programmatically initialize the scanner view - - mScannerView?.setAutoFocus(true) - val formats = ArrayList() - formats.add(BarcodeFormat.QR_CODE) - mScannerView?.setFormats(formats) - - setContentView(mScannerView) // Set the scanner view as the content view - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - public override fun onResume() { - super.onResume() - mScannerView!!.setResultHandler(this) // Register ourselves as a handler for scan results. - mScannerView!!.startCamera() // Start camera on resume - } - - 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 finished(text: String) { - val intent = Intent() - intent.putExtra("SCAN_RESULT", text) - setResult(Activity.RESULT_OK, intent) - finish() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_scanner, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.select_photo -> { - 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) - } - true - } - else -> super.onOptionsItemSelected(item) - } - - private fun showFileChooser() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "image/*" - intent.addCategory(Intent.CATEGORY_OPENABLE) - //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - - 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 { - val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri)) - val text = QRCodeDecoder.syncDecodeQRCode(bitmap) - finished(text) - } catch (e: Exception) { - e.printStackTrace() - toast(e.message.toString()) - } - } - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt deleted file mode 100644 index 3abcee52..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt +++ /dev/null @@ -1,354 +0,0 @@ -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.* -import com.tencent.mmkv.MMKV -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_PORT -import com.v2ray.ang.dto.V2rayConfig.Companion.XTLS -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 - -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 editGuid by lazy { intent.getStringExtra("guid").orEmpty() } - private val isRunning by lazy { - intent.getBooleanExtra("isRunning", false) - && editGuid.isNotEmpty() - && editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER) - } - private val createConfigType by lazy { - EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value)) ?: EConfigType.VMESS - } - private val securitys: Array by lazy { - resources.getStringArray(R.array.securitys) - } - private val shadowsocksSecuritys: Array by lazy { - resources.getStringArray(R.array.ss_securitys) - } - private val flows: Array by lazy { - resources.getStringArray(R.array.flows) - } - private val networks: Array by lazy { - resources.getStringArray(R.array.networks) - } - private val tcpTypes: Array by lazy { - resources.getStringArray(R.array.header_type_tcp) - } - private val kcpAndQuicTypes: Array by lazy { - resources.getStringArray(R.array.header_type_kcp_and_quic) - } - private val grpcModes: Array by lazy { - resources.getStringArray(R.array.mode_type_grpc) - } - private val streamSecuritys: Array by lazy { - resources.getStringArray(R.array.streamsecurityxs) - } - private val allowinsecures: Array by lazy { - resources.getStringArray(R.array.allowinsecures) - } - - // 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 et_sni: EditText? by lazy { findViewById(R.id.et_sni) } - 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 et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) } - private val et_path: EditText? by lazy { findViewById(R.id.et_path) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - title = getString(R.string.title_server) - - val config = MmkvManager.decodeServerConfig(editGuid) - 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) - } - sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - 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) - 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]) - } - } - override fun onNothingSelected(parent: AdapterView<*>?) { - // do nothing - } - } - if (config != null) { - bindingServer(config) - } else { - clearServer() - } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - /** - * bingding seleced 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_id.text = Utils.getEditable(outbound.getPassword().orEmpty()) - if (config.configType == EConfigType.SOCKS) { - 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()) - if (flow >= 0) { - //sp_flow.setSelection(flow) - } - } - 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 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) - } - et_request_host?.text = Utils.getEditable(tlsSetting.serverName) - } - } - val network = Utils.arrayFind(networks, streamSetting.network) - if (network >= 0) { - sp_network?.setSelection(network) - } - return true - } - - /** - * clear or init server config - */ - private fun clearServer(): Boolean { - et_remarks.text = null - et_address.text = null - et_port.text = Utils.getEditable(DEFAULT_PORT.toString()) - et_id.text = null - sp_security?.setSelection(0) - sp_network?.setSelection(0) - - sp_header_type?.setSelection(0) - et_request_host?.text = null - et_path?.text = null - sp_stream_security?.setSelection(0) - sp_allow_insecure?.setSelection(0) - - //et_security.text = null - //sp_flow?.setSelection(0) - return true - } - - /** - * save server config - */ - private fun saveServer(): Boolean { - if (TextUtils.isEmpty(et_remarks.text.toString())) { - toast(R.string.server_lab_remarks) - return false - } - if (TextUtils.isEmpty(et_address.text.toString())) { - toast(R.string.server_lab_address) - return false - } - val port = Utils.parseInt(et_port.text.toString()) - if (port <= 0) { - toast(R.string.server_lab_port) - return false - } - 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) - return false - } - - config.remarks = et_remarks.text.toString().trim() - config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> - saveVnext(vnext, port, config) - } - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - saveServers(server, port, config) - } - config.outboundBean?.streamSettings?.let { - saveStreamSettings(it) - } - - MmkvManager.encodeServerConfig(editGuid, config) - toast(R.string.toast_success) - finish() - return true - } - - 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 ?: 0] - } else if (config.configType == EConfigType.VLESS) { - vnext.users[0].encryption = et_security?.text.toString().trim() - if (streamSecuritys[sp_stream_security?.selectedItemPosition ?: 0] == XTLS) { -// vnext.users[0].flow = flows[sp_flow.selectedItemPosition].ifBlank { V2rayConfig.DEFAULT_FLOW } - } else { - vnext.users[0].flow = "" - } - } - } - - 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 ?: 0] - } else if (config.configType == EConfigType.SOCKS) { - 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() - socksUsersBean.pass = et_id.text.toString().trim() - server.users = listOf(socksUsersBean) - } - } else if (config.configType == EConfigType.TROJAN) { - server.password = et_id.text.toString().trim() - } - } - - 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 - - var sni = streamSetting.populateTransportSettings( - transport = networks[network], - headerType = transportTypes(networks[network])[type], - host = requestHost, - path = path, - seed = path, - quicSecurity = requestHost, - key = path, - mode = transportTypes(networks[network])[type], - serviceName = path - ) - //if (sniField.isNotBlank()) { - // sni = sniField - //} - val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) { - false//settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false - } else { - allowinsecures[allowInsecureField].toBoolean() - } - streamSetting.populateTlsSettings(streamSecuritys[streamSecurity], allowInsecure, sni) - - } - - private fun transportTypes(network: String?): Array { - return if (network == "tcp") { - tcpTypes - } else if (network == "kcp" || network == "quic") { - kcpAndQuicTypes - } else if (network == "grpc") { - grpcModes - } else { - arrayOf("---") - } - } - - /** - * save server config - */ - 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() - } - return true - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.action_server, menu) - val delButton = menu.findItem(R.id.del_config) - val saveButton = menu.findItem(R.id.save_config) - - if (editGuid.isNotEmpty()) { - if (isRunning) { - delButton?.isVisible = false - saveButton?.isVisible = false - } - } else { - delButton?.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) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt deleted file mode 100644 index c32d74cc..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt +++ /dev/null @@ -1,249 +0,0 @@ -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 com.v2ray.ang.AppConfig -import com.v2ray.ang.extension.toast -import com.v2ray.ang.service.V2RayServiceManager -import com.v2ray.ang.util.Utils -import com.v2ray.ang.viewmodel.SettingsViewModel - -class SettingsActivity : BaseActivity() { - private val settingsViewModel: SettingsViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) - - 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 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 } - - // val autoRestart by lazy { findPreference(PREF_AUTO_RESTART) as CheckBoxPreference } - - -// val socksPort by lazy { findPreference(PREF_SOCKS_PORT) as EditTextPreference } -// val httpPort by lazy { findPreference(PREF_HTTP_PORT) as EditTextPreference } - - 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 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 - } - - override fun onCreatePreferences(bundle: Bundle?, s: String?) { - addPreferencesFromResource(R.xml.pref_settings) - - perAppProxy.setOnPreferenceClickListener { - if (isRunning()) { - Utils.stopVService(requireContext()) - } - 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)) - 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 -> - updateLocalDns(any as Boolean) - true - } - localDnsPort?.setOnPreferenceChangeListener { _, any -> - val nval = any as String - localDnsPort?.summary = if (TextUtils.isEmpty(nval)) "10807" else nval - true - } - vpnDns?.setOnPreferenceChangeListener { _, any -> - vpnDns?.summary = any as String - true - } - mode.setOnPreferenceChangeListener { _, newValue -> - updateMode(newValue.toString()) - true - } - mode.dialogLayoutResource = R.layout.preference_with_help_link - -// donate.onClick { -// startActivity() -// } - -// 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, "") - - 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) - -// socksPort.summary = defaultSharedPreferences.getString(PREF_SOCKS_PORT, "10808") -// lanconnPort.summary = defaultSharedPreferences.getString(PREF_HTTP_PORT, "") - } - - 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) - localDns?.isEnabled = vpn - fakeDns?.isEnabled = vpn - localDnsPort?.isEnabled = vpn - vpnDns?.isEnabled = vpn - if (vpn) { - updateLocalDns(defaultSharedPreferences.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)) - } - } - - private fun updateLocalDns(enabled: Boolean) { - fakeDns?.isEnabled = enabled - localDnsPort?.isEnabled = enabled - vpnDns?.isEnabled = !enabled - } - } - - fun onModeHelpClicked(view: View) { - Utils.openUri(this, AppConfig.v2rayNGWikiMode) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt deleted file mode 100644 index f7b1c30a..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt +++ /dev/null @@ -1,132 +0,0 @@ -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 com.google.gson.Gson -import com.tencent.mmkv.MMKV -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.util.MmkvManager -import com.v2ray.ang.util.Utils - -class SubEditActivity : BaseActivity() { - private lateinit var binding: ActivitySubEditBinding - - var del_config: MenuItem? = null - var save_config: MenuItem? = null - - private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) } - private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivitySubEditBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) - title = getString(R.string.title_sub_setting) - - val json = subStorage?.decodeString(editSubId) - if (!json.isNullOrBlank()) { - bindingServer(Gson().fromJson(json, SubscriptionItem::class.java)) - } else { - clearServer() - } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - /** - * bingding seleced server config - */ - private fun bindingServer(subItem: SubscriptionItem): Boolean { - binding.etRemarks.text = Utils.getEditable(subItem.remarks) - binding.etUrl.text = Utils.getEditable(subItem.url) - - return true - } - - /** - * clear or init server config - */ - private fun clearServer(): Boolean { - binding.etRemarks.text = null - binding.etUrl.text = null - - return true - } - - /** - * save server config - */ - private fun saveServer(): Boolean { - val subItem: SubscriptionItem - val json = subStorage?.decodeString(editSubId) - var subId = editSubId - if (!json.isNullOrBlank()) { - subItem = Gson().fromJson(json, SubscriptionItem::class.java) - } else { - subId = Utils.getUuid() - subItem = SubscriptionItem() - } - - subItem.remarks = binding.etRemarks.text.toString() - subItem.url = binding.etUrl.text.toString() - - 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 - } - - subStorage?.encode(subId, Gson().toJson(subItem)) - toast(R.string.toast_success) - finish() - return true - } - - /** - * save server config - */ - private fun deleteServer(): Boolean { - if (editSubId.isNotEmpty()) { - AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) - .setPositiveButton(android.R.string.ok) { _, _ -> - MmkvManager.removeSubscription(editSubId) - finish() - } - .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 (editSubId.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) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt deleted file mode 100644 index a0030730..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.Intent -import android.view.Menu -import android.view.MenuItem -import com.v2ray.ang.R -import android.os.Bundle -import androidx.recyclerview.widget.LinearLayoutManager -import com.v2ray.ang.databinding.ActivitySubSettingBinding -import com.v2ray.ang.dto.SubscriptionItem -import com.v2ray.ang.util.MmkvManager - -class SubSettingActivity : BaseActivity() { - private lateinit var binding: ActivitySubSettingBinding - - var subscriptions:List> = listOf() - private val adapter by lazy { SubSettingRecyclerAdapter(this) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivitySubSettingBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) - - title = getString(R.string.title_sub_setting) - - binding.recyclerView.setHasFixedSize(true) - binding.recyclerView.layoutManager = LinearLayoutManager(this) - binding.recyclerView.adapter = adapter - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - override fun onResume() { - super.onResume() - subscriptions = MmkvManager.decodeSubscriptions() - adapter.notifyDataSetChanged() - } - - 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) - } - - override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.add_config -> { - startActivity(Intent(this, SubEditActivity::class.java)) - true - } - else -> super.onOptionsItemSelected(item) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt deleted file mode 100644 index 80a20c10..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.Intent -import android.graphics.Color -import androidx.recyclerview.widget.RecyclerView -import android.view.LayoutInflater -import android.view.ViewGroup -import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding - -class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter() { - - private var mActivity: SubSettingActivity = activity - - override fun getItemCount() = mActivity.subscriptions.size - - override fun onBindViewHolder(holder: MainViewHolder, position: Int) { - val subId = mActivity.subscriptions[position].first - val subItem = mActivity.subscriptions[position].second - holder.itemSubSettingBinding.tvName.text = subItem.remarks - holder.itemSubSettingBinding.tvUrl.text = subItem.url - holder.itemView.setBackgroundColor(Color.TRANSPARENT) - - holder.itemSubSettingBinding.layoutEdit.setOnClickListener { - mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java) - .putExtra("subId", subId) - ) - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder { - return MainViewHolder(ItemRecyclerSubSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - } - - class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root) -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt deleted file mode 100644 index 6f12e7f6..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt +++ /dev/null @@ -1,717 +0,0 @@ -package com.v2ray.ang.util - -import android.content.Context -import android.content.SharedPreferences -import android.graphics.Bitmap -import androidx.preference.PreferenceManager -import android.text.TextUtils -import com.google.gson.Gson -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.ANG_CONFIG -import com.v2ray.ang.AppConfig.HTTPS_PROTOCOL -import com.v2ray.ang.AppConfig.HTTP_PROTOCOL -import com.v2ray.ang.R -import com.v2ray.ang.dto.* -import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_SECURITY -import com.v2ray.ang.dto.V2rayConfig.Companion.TLS -import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER -import java.net.URI -import java.util.* -import com.v2ray.ang.extension.idnHost - -object AngConfigManager { - 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) } - private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) } - - /** - * Legacy loading config - */ - fun migrateLegacyConfig(c: Context): Boolean? { - try { - val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(c) - val context = defaultSharedPreferences.getString(ANG_CONFIG, "") - if (context.isNullOrBlank()) { - return null - } - val angConfig = Gson().fromJson(context, AngConfig::class.java) - for (i in angConfig.vmess.indices) { - upgradeServerVersion(angConfig.vmess[i]) - } - - copyLegacySettings(defaultSharedPreferences) - migrateVmessBean(angConfig, defaultSharedPreferences) - migrateSubItemBean(angConfig) - - defaultSharedPreferences.edit().remove(ANG_CONFIG).apply() - return true - } catch (e: Exception) { - e.printStackTrace() - } - return false - } - - private fun copyLegacySettings(sharedPreferences: SharedPreferences) { - listOf( - AppConfig.PREF_MODE, - AppConfig.PREF_REMOTE_DNS, - AppConfig.PREF_DOMESTIC_DNS, - AppConfig.PREF_LOCAL_DNS_PORT, -// AppConfig.PREF_SOCKS_PORT, -// AppConfig.PREF_HTTP_PORT, -// AppConfig.PREF_LOGLEVEL, - AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, - AppConfig.PREF_ROUTING_MODE, - AppConfig.PREF_V2RAY_ROUTING_AGENT, - AppConfig.PREF_V2RAY_ROUTING_BLOCKED, - AppConfig.PREF_V2RAY_ROUTING_DIRECT, - ).forEach { key -> - settingsStorage?.encode(key, sharedPreferences.getString(key, null)) - } - listOf( - AppConfig.PREF_SPEED_ENABLED, - AppConfig.PREF_PROXY_SHARING, - AppConfig.PREF_LOCAL_DNS_ENABLED, -// AppConfig.PREF_ALLOW_INSECURE, -// AppConfig.PREF_PREFER_IPV6, - AppConfig.PREF_PER_APP_PROXY, - AppConfig.PREF_BYPASS_APPS, - ).forEach { key -> - settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false)) - } - settingsStorage?.encode(AppConfig.PREF_SNIFFING_ENABLED, sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true)) - settingsStorage?.encode(AppConfig.PREF_PER_APP_PROXY_SET, sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf())) - } - - private fun migrateVmessBean(angConfig: AngConfig, sharedPreferences: SharedPreferences) { - angConfig.vmess.forEachIndexed { index, vmessBean -> - val type = EConfigType.fromInt(vmessBean.configType) ?: return@forEachIndexed - val config = ServerConfig.create(type) - config.remarks = vmessBean.remarks - config.subscriptionId = vmessBean.subid - if (type == EConfigType.CUSTOM) { - val jsonConfig = sharedPreferences.getString(ANG_CONFIG + vmessBean.guid, "") - val v2rayConfig = try { - Gson().fromJson(jsonConfig, V2rayConfig::class.java) - } catch (e: Exception) { - e.printStackTrace() - return@forEachIndexed - } - config.fullConfig = v2rayConfig - serverRawStorage?.encode(vmessBean.guid, jsonConfig) - } else { - config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = vmessBean.address - vnext.port = vmessBean.port - vnext.users[0].id = vmessBean.id - if (config.configType == EConfigType.VMESS) { - vnext.users[0].security = vmessBean.security - } else if (config.configType == EConfigType.VLESS) { - vnext.users[0].encryption = vmessBean.security -// vnext.users[0].flow = vmessBean.flow - } - } - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = vmessBean.address - server.port = vmessBean.port - if (config.configType == EConfigType.SHADOWSOCKS) { - server.password = vmessBean.id - server.method = vmessBean.security - } else if (config.configType == EConfigType.SOCKS) { - if (TextUtils.isEmpty(vmessBean.security) && TextUtils.isEmpty(vmessBean.id)) { - server.users = null - } else { - val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean() - socksUsersBean.user = vmessBean.security - socksUsersBean.pass = vmessBean.id - server.users = listOf(socksUsersBean) - } - } else if (config.configType == EConfigType.TROJAN) { - server.password = vmessBean.id - } - } - config.outboundBean?.streamSettings?.let { streamSetting -> - val sni = streamSetting.populateTransportSettings(vmessBean.network, vmessBean.headerType, - vmessBean.requestHost, vmessBean.path, vmessBean.path, vmessBean.requestHost, vmessBean.path, - vmessBean.headerType, vmessBean.path) -// val allowInsecure = if (vmessBean.allowInsecure.isBlank()) { -// settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false -// } else { -// vmessBean.allowInsecure.toBoolean() -// } - streamSetting.populateTlsSettings(vmessBean.streamSecurity, false, - sni)//vmessBean.sni.ifBlank { sni }) - } - } - val key = MmkvManager.encodeServerConfig(vmessBean.guid, config) - if (index == angConfig.index) { - mainStorage?.encode(KEY_SELECTED_SERVER, key) - } - } - } - - private fun migrateSubItemBean(angConfig: AngConfig) { - angConfig.subItem.forEach { - val subItem = SubscriptionItem() - subItem.remarks = it.remarks - subItem.url = it.url - //subItem.enabled = it.enabled - subStorage?.encode(it.id, Gson().toJson(subItem)) - } - } - - /** - * import config form qrcode or... - */ - private fun importConfig(str: String?, subid: String, removedSelectedServer: ServerConfig?): Int { - try { - if (str == null || TextUtils.isEmpty(str)) { - return R.string.toast_none_data - } - - //maybe sub - if (TextUtils.isEmpty(subid) && (str.startsWith(HTTP_PROTOCOL) || str.startsWith(HTTPS_PROTOCOL))) { - MmkvManager.importUrlAsSubscription(str) - return 0 - } - - var config: ServerConfig? = null - val allowInsecure = false//settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false - if (str.startsWith(EConfigType.VMESS.protocolScheme)) { - config = ServerConfig.create(EConfigType.VMESS) - val streamSetting = config.outboundBean?.streamSettings ?: return -1 - - if (!tryParseNewVmess(str, config, allowInsecure)) { - if (str.indexOf("?") > 0) { - if (!tryResolveVmess4Kitsunebi(str, config)) { - return R.string.toast_incorrect_protocol - } - } else { - var result = str.replace(EConfigType.VMESS.protocolScheme, "") - result = Utils.decode(result) - if (TextUtils.isEmpty(result)) { - return R.string.toast_decoding_failed - } - 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) - ) { - return R.string.toast_incorrect_protocol - } - - 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].encryption = DEFAULT_SECURITY - } - val sni = streamSetting.populateTransportSettings(vmessQRCode.net, vmessQRCode.type, vmessQRCode.host, - vmessQRCode.path, vmessQRCode.path, vmessQRCode.host, vmessQRCode.path, vmessQRCode.type, vmessQRCode.path) - streamSetting.populateTlsSettings(vmessQRCode.tls, allowInsecure, - if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni) - } - } - } else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) { - 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 R.string.toast_incorrect_protocol - - 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() - } - } - } else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) { - var result = str.replace(EConfigType.SOCKS.protocolScheme, "") - val indexSplit = result.indexOf("#") - config = ServerConfig.create(EConfigType.SOCKS) - 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 R.string.toast_incorrect_protocol - - 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].lowercase() - socksUsersBean.pass = match.groupValues[2] - server.users = listOf(socksUsersBean) - } - } else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) { - val uri = URI(str) - config = ServerConfig.create(EConfigType.TROJAN) - config.remarks = Utils.urlDecode(uri.fragment ?: "") - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = uri.idnHost - server.port = uri.port - server.password = uri.userInfo - } - var sni = "" - uri.rawQuery?.let { rawQuery -> - val queryParam = rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - sni = queryParam["sni"] ?: "" - } - config.outboundBean?.streamSettings?.populateTlsSettings(TLS, allowInsecure, sni) - } else if (str.startsWith(EConfigType.VLESS.protocolScheme)) { - val uri = URI(str) - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - config = ServerConfig.create(EConfigType.VLESS) - val streamSetting = config.outboundBean?.streamSettings ?: return -1 - 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"]) - streamSetting.populateTlsSettings(queryParam["security"] ?: "", allowInsecure, queryParam["sni"] ?: sni) - } - if (config == null){ - return R.string.toast_incorrect_protocol - } - config.subscriptionId = subid - val guid = MmkvManager.encodeServerConfig("", config) - if (removedSelectedServer != null && - config.getProxyOutbound()?.getServerAddress() == removedSelectedServer.getProxyOutbound()?.getServerAddress() && - config.getProxyOutbound()?.getServerPort() == removedSelectedServer.getProxyOutbound()?.getServerPort()) { - mainStorage?.encode(KEY_SELECTED_SERVER, guid) - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - private fun tryParseNewVmess(uriString: String, config: ServerConfig, allowInsecure: Boolean): Boolean { - return runCatching { - val uri = URI(uriString) - check(uri.scheme == "vmess") - val (_, protocol, tlsStr, uuid) = - Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})") - .matchEntire(uri.userInfo)?.groupValues - ?: error("parse user info fail.") - val tls = tlsStr.isNotBlank() - val queryParam = uri.rawQuery.split("&") - .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } - - val streamSetting = config.outboundBean?.streamSettings ?: return false - 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 = uuid - vnext.users[0].encryption = DEFAULT_SECURITY - } - - val sni = streamSetting.populateTransportSettings(protocol, queryParam["type"], - queryParam["host"]?.split("|")?.get(0) ?: "", - queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", queryParam["seed"], queryParam["security"], - queryParam["key"], queryParam["mode"], queryParam["serviceName"]) - streamSetting.populateTlsSettings(if (tls) TLS else "", allowInsecure, sni) - true - }.getOrElse { false } - } - - private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean { - - var result = server.replace(EConfigType.VMESS.protocolScheme, "") - val indexSplit = result.indexOf("?") - if (indexSplit > 0) { - result = result.substring(0, indexSplit) - } - result = Utils.decode(result) - - val arr1 = result.split('@') - if (arr1.count() != 2) { - return false - } - val arr21 = arr1[0].split(':') - val arr22 = arr1[1].split(':') - if (arr21.count() != 2) { - return false - } - - config.remarks = "Alien" - config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> - vnext.address = arr22[0] - vnext.port = Utils.parseInt(arr22[1]) - vnext.users[0].id = arr21[1] - vnext.users[0].encryption = arr21[0] - } - return true - } - - private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean { - val uri = URI(str.replace(" ", "%20")) - 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 && arrUserInfo.count() != 3) { - return false - } - method = arrUserInfo[0] - password = base64Decode.substringAfter(":") - } - - config.outboundBean?.settings?.servers?.get(0)?.let { server -> - server.address = uri.idnHost - server.port = uri.port - server.password = password - server.method = method - } - return true - } - - /** - * share config - */ - private fun shareConfig(guid: String): String { - try { - val config = MmkvManager.decodeServerConfig(guid) ?: return "" - val outbound = config.getProxyOutbound() ?: return "" - val streamSetting = outbound.streamSettings ?: return "" - return config.configType.protocolScheme + when (config.configType) { - EConfigType.VMESS -> { - 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.net = streamSetting.network - vmessQRCode.tls = streamSetting.security - vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty() - outbound.getTransportSettingDetails()?.let { transportDetails -> - vmessQRCode.type = transportDetails[0] - vmessQRCode.host = transportDetails[1] - vmessQRCode.path = transportDetails[2] - } - val json = Gson().toJson(vmessQRCode) - Utils.encode(json) - } - EConfigType.CUSTOM -> "" - EConfigType.SHADOWSOCKS -> { - val remark = "#" + Utils.urlEncode(config.remarks) - val url = String.format("%s:%s@%s:%s", - outbound.getSecurityEncryption(), - outbound.getPassword(), - outbound.getServerAddress(), - outbound.getServerPort()) - Utils.encode(url) + remark - } - EConfigType.SOCKS -> { - val remark = "#" + Utils.urlEncode(config.remarks) - val url = String.format("%s:%s@%s:%s", - outbound.settings?.servers?.get(0)?.users?.get(0)?.user, - outbound.getPassword(), - outbound.getServerAddress(), - outbound.getServerPort()) - Utils.encode(url) + remark - } - EConfigType.VLESS -> { - val remark = "#" + Utils.urlEncode(config.remarks) - - val dicQuery = HashMap() - 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.xtlsSettings)?.let { tlsSetting -> - if (!TextUtils.isEmpty(tlsSetting.serverName)) { - dicQuery["sni"] = tlsSetting.serverName - } - } - 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" -> { - 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["serviceName"] = 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()) - url + query + remark - } - EConfigType.TROJAN -> { - val remark = "#" + Utils.urlEncode(config.remarks) - var query = "" - (streamSetting.tlsSettings?: streamSetting.xtlsSettings)?.let { tlsSetting -> - if (!TextUtils.isEmpty(tlsSetting.serverName)) { - query = "?sni=${tlsSetting.serverName}" - } - } - val url = String.format("%s@%s:%s", - outbound.getPassword(), - Utils.getIpv6Address(outbound.getServerAddress()!!), - outbound.getServerPort()) - url + query + remark - } - } - } catch (e: Exception) { - e.printStackTrace() - return "" - } - } - - /** - * share2Clipboard - */ - fun share2Clipboard(context: Context, guid: String): Int { - try { - val conf = shareConfig(guid) - if (TextUtils.isEmpty(conf)) { - return -1 - } - - Utils.setClipboard(context, conf) - - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * share2Clipboard - */ - fun shareNonCustomConfigsToClipboard(context: Context, serverList: List): Int { - try { - val sb = StringBuilder() - for (guid in serverList) { - val url = shareConfig(guid) - if (TextUtils.isEmpty(url)) { - continue - } - sb.append(url) - sb.appendLine() - } - if (sb.count() > 0) { - Utils.setClipboard(context, sb.toString()) - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * share2QRCode - */ - fun share2QRCode(guid: String): Bitmap? { - try { - val conf = shareConfig(guid) - if (TextUtils.isEmpty(conf)) { - return null - } - return Utils.createQRCode(conf) - - } catch (e: Exception) { - e.printStackTrace() - return null - } - } - - /** - * shareFullContent2Clipboard - */ - fun shareFullContent2Clipboard(context: Context, guid: String?): Int { - try { - if (guid == null) return -1 - val result = V2rayConfigUtil.getV2rayConfig(context, guid) - if (result.status) { - Utils.setClipboard(context, result.content) - } else { - return -1 - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * upgrade - */ - private fun upgradeServerVersion(vmess: AngConfig.VmessBean): Int { - try { - if (vmess.configVersion == 2) { - return 0 - } - - when (vmess.network) { - "ws", "h2" -> { - var path = "" - var host = "" - val lstParameter = vmess.requestHost.split(";") - if (lstParameter.isNotEmpty()) { - path = lstParameter[0].trim() - } - if (lstParameter.size > 1) { - path = lstParameter[0].trim() - host = lstParameter[1].trim() - } - vmess.path = path - vmess.requestHost = host - } - } - vmess.configVersion = 2 - return 0 - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - } - - fun importBatchConfig(servers: String?, subid: String): Int { - try { - if (servers == null) { - return 0 - } - val removedSelectedServer = - if (!TextUtils.isEmpty(subid)) { - MmkvManager.decodeServerConfig(mainStorage?.decodeString(KEY_SELECTED_SERVER) ?: "")?.let { - if (it.subscriptionId == subid) { - return@let it - } - return@let null - } - } else { - null - } - MmkvManager.removeServerViaSubid(subid) - -// var servers = server -// if (server.indexOf("vmess") >= 0 && server.indexOf("vmess") == server.lastIndexOf("vmess")) { -// servers = server.replace("\n", "") -// } - - var count = 0 - servers.lines() - .forEach { - val resId = importConfig(it, subid, removedSelectedServer) - if (resId == 0) { - count++ - } - } - return count - } catch (e: Exception) { - e.printStackTrace() - } - return 0 - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt deleted file mode 100644 index b95b408b..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AppManagerUtil.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.v2ray.ang.util - -import android.Manifest -import android.content.Context -import android.content.pm.ApplicationInfo -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 { - val packageManager = ctx.packageManager - val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) - val apps = ArrayList() - - for (pkg in packages) { - if (!pkg.hasInternetPermission && pkg.packageName != "android") continue - - val applicationInfo = pkg.applicationInfo - - val appName = applicationInfo.loadLabel(packageManager).toString() - val appIcon = applicationInfo.loadIcon(packageManager) - val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0 - - val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) - apps.add(appInfo) - } - - return apps - } - - fun rxLoadNetworkAppList(ctx: Context): Observable> = Observable.unsafeCreate { - it.onNext(loadNetworkAppList(ctx)) - } - - val PackageInfo.hasInternetPermission: Boolean - get() { - val permissions = requestedPermissions - return permissions?.any { it == Manifest.permission.INTERNET } ?: false - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt deleted file mode 100644 index 486023c8..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MessageUtil.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.v2ray.ang.util - -import android.content.Context -import android.content.Intent -import com.v2ray.ang.AppConfig - - -object MessageUtil { - - fun sendMsg2Service(ctx: Context, what: Int, content: String) { - sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content) - } - - fun sendMsg2UI(ctx: Context, what: Int, content: String) { - sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content) - } - - private fun sendMsg(ctx: Context, action: String, what: Int, content: String) { - try { - val intent = Intent() - intent.action = action - intent.`package` = AppConfig.ANG_PACKAGE - intent.putExtra("key", what) - intent.putExtra("content", content) - ctx.sendBroadcast(intent) - } catch (e: Exception) { - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt deleted file mode 100644 index ace87de0..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.v2ray.ang.util - -import com.google.gson.Gson -import com.tencent.mmkv.MMKV -import com.v2ray.ang.dto.ServerAffiliationInfo -import com.v2ray.ang.dto.ServerConfig -import com.v2ray.ang.dto.SubscriptionItem - -object MmkvManager { - const val ID_MAIN = "MAIN" - const val ID_SERVER_CONFIG = "SERVER_CONFIG" - const val ID_SERVER_RAW = "SERVER_RAW" - const val ID_SERVER_AFF = "SERVER_AFF" - const val ID_SUB = "SUB" - const val ID_SETTING = "SETTING" - const val KEY_SELECTED_SERVER = "SELECTED_SERVER" - const val KEY_ANG_CONFIGS = "ANG_CONFIGS" - - private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } - private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } - private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) } - private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) } - - fun decodeServerList(): MutableList { - val json = mainStorage?.decodeString(KEY_ANG_CONFIGS) - return if (json.isNullOrBlank()) { - mutableListOf() - } else { - Gson().fromJson(json, Array::class.java).toMutableList() - } - } - - fun decodeServerConfig(guid: String): ServerConfig? { - if (guid.isBlank()) { - return null - } - val json = serverStorage?.decodeString(guid) - if (json.isNullOrBlank()) { - return null - } - return Gson().fromJson(json, ServerConfig::class.java) - } - - fun encodeServerConfig(guid: String, config: ServerConfig): String { - val key = guid.ifBlank { Utils.getUuid() } - serverStorage?.encode(key, Gson().toJson(config)) - val serverList= decodeServerList() - if (!serverList.contains(key)) { - serverList.add(key) - mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) - if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) { - mainStorage?.encode(KEY_SELECTED_SERVER, key) - } - } - return key - } - - fun removeServer(guid: String) { - if (guid.isBlank()) { - return - } - if (mainStorage?.decodeString(KEY_SELECTED_SERVER) == guid) { - mainStorage?.remove(KEY_SELECTED_SERVER) - } - val serverList= decodeServerList() - serverList.remove(guid) - mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) - serverStorage?.remove(guid) - serverAffStorage?.remove(guid) - } - - fun removeServerViaSubid(subid: String) { - if (subid.isBlank()) { - return - } - serverStorage?.allKeys()?.forEach { key -> - decodeServerConfig(key)?.let { config -> - if (config.subscriptionId == subid) { - removeServer(key) - } - } - } - } - - fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? { - if (guid.isBlank()) { - return null - } - val json = serverAffStorage?.decodeString(guid) - if (json.isNullOrBlank()) { - return null - } - return Gson().fromJson(json, ServerAffiliationInfo::class.java) - } - - fun encodeServerTestDelayMillis(guid: String, testResult: Long) { - if (guid.isBlank()) { - return - } - val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo() - aff.testDelayMillis = testResult - serverAffStorage?.encode(guid, Gson().toJson(aff)) - } - - fun clearAllTestDelayResults() { - serverAffStorage?.allKeys()?.forEach { key -> - decodeServerAffiliationInfo(key)?.let { aff -> - aff.testDelayMillis = 0 - serverAffStorage?.encode(key, Gson().toJson(aff)) - } - } - } - - fun importUrlAsSubscription(url: String): Int { - val subscriptions = decodeSubscriptions() - subscriptions.forEach { - if (it.second.url == url) { - return 0 - } - } - val subItem = SubscriptionItem() - subItem.remarks = "import sub" - subItem.url = url - subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem)) - return 1 - } - - fun decodeSubscriptions(): List> { - val subscriptions = mutableListOf>() - subStorage?.allKeys()?.forEach { key -> - val json = subStorage?.decodeString(key) - if (!json.isNullOrBlank()) { - subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java))) - } - } - subscriptions.sortedBy { (_, value) -> value.addedTime } - return subscriptions - } - - fun removeSubscription(subid: String) { - subStorage?.remove(subid) - removeServerViaSubid(subid) - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt deleted file mode 100644 index 804011c8..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt +++ /dev/null @@ -1,489 +0,0 @@ -package com.v2ray.ang.util - -import android.content.ClipboardManager -import android.content.Context -import android.text.Editable -import android.util.Base64 -import com.google.zxing.WriterException -import android.graphics.Bitmap -import com.google.zxing.BarcodeFormat -import com.google.zxing.qrcode.QRCodeWriter -import com.google.zxing.EncodeHintType -import java.util.* -import kotlin.collections.HashMap -import android.content.ClipData -import android.content.Intent -import android.net.Uri -import android.os.SystemClock -import android.util.Log -import android.util.Patterns -import android.webkit.URLUtil -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -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 - -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() - - /** - * convert string to editalbe for kotlin - * - * @param text - * @return - */ - fun getEditable(text: String): Editable { - return Editable.Factory.getInstance().newEditable(text) - } - - /** - * find value in array position - */ - fun arrayFind(array: Array, value: String): Int { - for (i in array.indices) { - if (array[i] == value) { - return i - } - } - return -1 - } - - /** - * parseInt - */ - fun parseInt(str: String): Int { - return try { - Integer.parseInt(str) - } catch (e: Exception) { - e.printStackTrace() - 0 - } - } - - /** - * get text from clipboard - */ - fun getClipboard(context: Context): String { - return try { - val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - cmb.primaryClip?.getItemAt(0)?.text.toString() - } catch (e: Exception) { - e.printStackTrace() - "" - } - } - - /** - * set text to clipboard - */ - fun setClipboard(context: Context, content: String) { - try { - val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = ClipData.newPlainText(null, content) - cmb.setPrimaryClip(clipData) - } catch (e: Exception) { - e.printStackTrace() - } - } - - /** - * 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 tryDecodeBase64(text: String): String? { - try { - return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8")) - } catch (e: Exception) { - Log.i(AppConfig.ANG_PACKAGE, "Parse base64 standard failed $e") - } - try { - return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(charset("UTF-8")) - } catch (e: Exception) { - Log.i(AppConfig.ANG_PACKAGE, "Parse base64 url safe failed $e") - } - return null - } - - /** - * base64 encode - */ - fun encode(text: String): String { - return try { - Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP) - } catch (e: Exception) { - e.printStackTrace() - "" - } - } - - /** - * get remote dns servers from preference - */ - fun getRemoteDnsServers(): List { - val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_AGENT - val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) } - if (ret.isEmpty()) { - return listOf(AppConfig.DNS_AGENT) - } - return ret - } - - fun getVpnDnsServers(): List { - val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS) - ?: settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) - ?: AppConfig.DNS_AGENT - return vpnDns.split(",").filter { isPureIpAddress(it) } - // allow empty, in that case dns will use system default - } - - /** - * get remote dns servers from preference - */ - fun getDomesticDnsServers(): List { - val domesticDns = settingsStorage?.decodeString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT - val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) } - if (ret.isEmpty()) { - return listOf(AppConfig.DNS_DIRECT) - } - return ret - } - - /** - * create qrcode using zxing - */ - fun createQRCode(text: String, size: Int = 800): Bitmap? { - try { - val hints = HashMap() - 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 - */ - fun isIpAddress(value: String): Boolean { - try { - var addr = value - if (addr.isEmpty() || addr.isBlank()) { - return false - } - //CIDR - if (addr.indexOf("/") > 0) { - val arr = addr.split("/") - if (arr.count() == 2 && Integer.parseInt(arr[1]) > 0) { - addr = arr[0] - } - } - - // "::ffff:192.168.173.22" - // "[::ffff:192.168.173.22]:80" - if (addr.startsWith("::ffff:") && '.' in addr) { - addr = addr.drop(7) - } else if (addr.startsWith("[::ffff:") && '.' in addr) { - addr = addr.drop(8).replace("]", "") - } - - // addr = addr.toLowerCase() - val octets = addr.split('.').toTypedArray() - if (octets.size == 4) { - if(octets[3].indexOf(":") > 0) { - addr = addr.substring(0, addr.indexOf(":")) - } - return isIpv4Address(addr) - } - - // Ipv6addr [2001:abc::123]:8080 - return isIpv6Address(addr) - } catch (e: Exception) { - e.printStackTrace() - return false - } - } - - fun isPureIpAddress(value: String): Boolean { - return (isIpv4Address(value) || isIpv6Address(value)) - } - - fun isIpv4Address(value: String): Boolean { - val regV4 = Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") - return regV4.matches(value) - } - - fun isIpv6Address(value: String): Boolean { - var addr = value - if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) { - addr = addr.drop(1) - addr = addr.dropLast(addr.count() - addr.lastIndexOf("]")) - } - val regV6 = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$") - return regV6.matches(addr) - } - - private fun isCoreDNSAddress(s: String): Boolean { - return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") - } - - /** - * is valid url - */ - fun isValidUrl(value: String?): Boolean { - try { - if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) { - return true - } - } catch (e: WriterException) { - e.printStackTrace() - return false - } - return false - } - - fun startVServiceFromToggle(context: Context): Boolean { - if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) { - context.toast(R.string.app_tile_first_use) - return false - } - V2RayServiceManager.startV2Ray(context) - return true - } - - /** - * stopVService - */ - fun stopVService(context: Context) { - context.toast(R.string.toast_services_stop) - MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "") - } - - fun openUri(context: Context, uriString: String) { - val uri = Uri.parse(uriString) - context.startActivity(Intent(Intent.ACTION_VIEW, uri)) - } - - /** - * uuid - */ - fun getUuid(): String { - return try { - UUID.randomUUID().toString().replace("-", "") - } catch (e: Exception) { - e.printStackTrace() - "" - } - } - - fun urlDecode(url: String): String { - return try { - URLDecoder.decode(url, "UTF-8") - } catch (e: Exception) { - e.printStackTrace() - url - } - } - - fun urlEncode(url: String): String { - return try { - URLEncoder.encode(url, "UTF-8") - } 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 { - val content = context.assets.open(fileName).bufferedReader().use { - it.readText() - } - return content - } - - /** - * ping - */ - 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 (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" - } - } - } 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() - } - } - - @Throws(IOException::class) - fun getUrlContentWithCustomUserAgent(urlStr: String?): String { - val url = URL(urlStr) - val conn = url.openConnection() - conn.setRequestProperty("Connection", "close") - conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}") - url.userInfo?.let { - conn.setRequestProperty("Authorization", - "Basic ${encode(urlDecode(it))}") - } - conn.useCaches = false - return conn.inputStream.use { - it.bufferedReader().readText() - } - } - - fun getIpv6Address(address: String): String { - return if (isIpv6Address(address)) { - String.format("[%s]", address) - } else { - address - } - } -} - diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt deleted file mode 100644 index b487213d..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt +++ /dev/null @@ -1,421 +0,0 @@ -package com.v2ray.ang.util - -import android.content.Context -import android.text.TextUtils -import android.util.Log -import com.google.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.dto.EConfigType -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) } - - data class Result(var status: Boolean, var content: String) - - /** - * 生成v2ray的客户端配置文件 - */ - fun getV2rayConfig(context: Context, guid: String): Result { - try { - val config = MmkvManager.decodeServerConfig(guid) ?: return Result(false, "") - if (config.configType == EConfigType.CUSTOM) { - val raw = serverRawStorage?.decodeString(guid) - val customConfig = if (raw.isNullOrBlank()) { - config.fullConfig?.toPrettyPrinting() ?: return Result(false, "") - } else { - raw - } - 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) - return result - } catch (e: Exception) { - e.printStackTrace() - return Result(false, "") - } - } - - /** - * 生成v2ray的客户端配置文件 - */ - private fun getV2rayNonCustomConfig(context: Context, outbound: V2rayConfig.OutboundBean): Result { - val result = Result(false, "") - //取得默认配置 - val assets = Utils.readTextFromAssets(context, "v2ray_config.json") - if (TextUtils.isEmpty(assets)) { - return result - } - - //转成Json - val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result - - //v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning" - - inbounds(v2rayConfig) - - httpRequestObject(outbound) - - v2rayConfig.outbounds[0] = outbound - - routing(v2rayConfig) - - fakedns(v2rayConfig) - - dns(v2rayConfig) - - if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { - customLocalDns(v2rayConfig) - } - if (settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) != true) { - v2rayConfig.stats = null - v2rayConfig.policy = null - } - result.status = true - result.content = v2rayConfig.toPrettyPrinting() - return result - } - - /** - * - */ - 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) - - v2rayConfig.inbounds.forEach { curInbound -> - if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) { - //bind all inbounds to localhost if the user requests - curInbound.listen = "127.0.0.1" - } - } - v2rayConfig.inbounds[0].port = 10808 - val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) - ?: false - val sniffAllTlsAndHttp = settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true) - ?: true - v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp - if (!sniffAllTlsAndHttp) { - v2rayConfig.inbounds[0].sniffing?.destOverride?.clear() - } - if (fakedns) { - v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns") - } - - //v2rayConfig.inbounds[1].port = httpPort - -// if (httpPort > 0) { -// val httpCopy = v2rayConfig.inbounds[0].copy() -// httpCopy.port = httpPort -// httpCopy.protocol = "http" -// v2rayConfig.inbounds.add(httpCopy) -// } - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - private fun fakedns(v2rayConfig: V2rayConfig) { - if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) { - v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean()) - v2rayConfig.outbounds.filter { it.protocol == "freedom" }.forEach { - it.settings?.domainStrategy = "UseIP" - } - } - } - - /** - * routing - */ - 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) - - v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) - ?: "IPIfNonMatch" - val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE) ?: "0" - - // Hardcode googleapis.cn - val googleapisRoute = V2rayConfig.RoutingBean.RulesBean( - type = "field", - outboundTag = AppConfig.TAG_AGENT, - domain = arrayListOf("domain:googleapis.cn") - ) - - when (routingMode) { - "1" -> { - routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig) - } - "2" -> { - routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig) - v2rayConfig.routing.rules.add(0, googleapisRoute) - } - "3" -> { - routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig) - routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig) - v2rayConfig.routing.rules.add(0, googleapisRoute) - } - } - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - 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") - v2rayConfig.routing.rules.add(rulesIP) - } - - if (ipOrDomain == "domain" || ipOrDomain == "") { - //Domain - val rulesDomain = V2rayConfig.RoutingBean.RulesBean() - rulesDomain.type = "field" - rulesDomain.outboundTag = tag - rulesDomain.domain = ArrayList() - rulesDomain.domain?.add("geosite:$code") - v2rayConfig.routing.rules.add(rulesDomain) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun routingUserRule(userRule: String, tag: String, v2rayConfig: V2rayConfig) { - try { - 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:")) { - rulesIP.ip?.add(it) - } else if (it.isNotEmpty()) -// if (Utils.isValidUrl(it) -// || it.startsWith("geosite:") -// || it.startsWith("regexp:") -// || it.startsWith("domain:") -// || it.startsWith("full:")) - { - rulesDomain.domain?.add(it) - } - } - if (rulesDomain.domain?.size!! > 0) { - v2rayConfig.routing.rules.add(rulesDomain) - } - if (rulesIP.ip?.size!! > 0) { - v2rayConfig.routing.rules.add(rulesIP) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun userRule2Domian(userRule: String): ArrayList { - val domain = ArrayList() - userRule.split(",").map { it.trim() }.forEach { - if (it.startsWith("geosite:") || it.startsWith("domain:")) { - domain.add(it) - } - } - return domain - } - - /** - * Custom Dns - */ - private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean { - 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) - ?: "") - // 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))) - } - - // 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") - - //val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT) ?: AppConfig.PORT_LOCAL_DNS) - v2rayConfig.inbounds.add( - V2rayConfig.InboundBean( - tag = "dns-in", - port = 10807, - 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)) - } - - // DNS routing tag - v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean( - type = "field", - inboundTag = arrayListOf("dns-in"), - outboundTag = "dns-out", - domain = null) - ) - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - private fun dns(v2rayConfig: V2rayConfig): Boolean { - try { - val hosts = mutableMapOf() - val servers = ArrayList() - val remoteDns = Utils.getRemoteDnsServers() - val proxyDomain = userRule2Domian(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)) - } - - // 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 blkDomain = userRule2Domian(settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED) - ?: "") - if (blkDomain.size > 0) { - hosts.putAll(blkDomain.map { it to "127.0.0.1" }) - } - - // hardcode googleapi rule to fix play store problems - hosts["domain:googleapis.cn"] = "googleapis.com" - - // DNS dns对象 - v2rayConfig.dns = V2rayConfig.DnsBean( - 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, - port = "53", - ip = arrayListOf(remoteDns.first()), - domain = null) - ) - } - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - private fun httpRequestObject(outbound: V2rayConfig.OutboundBean): Boolean { - try { - if (outbound.streamSettings?.network == DEFAULT_NETWORK - && outbound.streamSettings?.tcpSettings?.header?.type == HTTP) { - val path = outbound.streamSettings?.tcpSettings?.header?.request?.path - val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host - - val requestString: String by lazy { - """{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""" - } - outbound.streamSettings?.tcpSettings?.header?.request = Gson().fromJson( - requestString, - V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java - ) - outbound.streamSettings?.tcpSettings?.header?.request?.path = - if (path.isNullOrEmpty()) { - listOf("/") - } else { - path - } - outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!! - } - - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt deleted file mode 100644 index c2b106c3..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt +++ /dev/null @@ -1,147 +0,0 @@ -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.util.Log -import com.google.gson.Gson -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AngApplication -import com.v2ray.ang.AppConfig -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.extension.toast -import com.v2ray.ang.util.* -import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS -import kotlinx.coroutines.* -import java.util.* -import java.util.concurrent.ConcurrentHashMap - -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) } - - var serverList = MmkvManager.decodeServerList() - private set - val serversCache = ConcurrentHashMap() - val isRunning by lazy { MutableLiveData() } - val updateListAction by lazy { MutableLiveData() } - val updateTestResultAction by lazy { MutableLiveData() } - - private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) } - - fun startListenBroadcast() { - isRunning.value = false - getApplication().registerReceiver(mMsgReceiver, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) - MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "") - } - - override fun onCleared() { - getApplication().unregisterReceiver(mMsgReceiver) - tcpingTestScope.coroutineContext[Job]?.cancelChildren() - Utils.closeAllTcpSockets() - Log.i(AppConfig.ANG_PACKAGE, "Main ViewModel is cleared") - super.onCleared() - } - - fun reloadServerList() { - serverList = MmkvManager.decodeServerList() - updateCache() - updateListAction.value = -1 - } - - fun removeServer(guid: String) { - serverList.remove(guid) - MmkvManager.removeServer(guid) - } - - 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 swapServer(fromPosition: Int, toPosition: Int) { - Collections.swap(serverList, fromPosition, toPosition) - mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList)) - } - - fun updateCache() { - serversCache.clear() - GlobalScope.launch(Dispatchers.Default) { - serverList.forEach { guid -> - MmkvManager.decodeServerConfig(guid)?.let { - serversCache[guid] = it - } - } - } - } - - fun testAllTcping() { - tcpingTestScope.coroutineContext[Job]?.cancelChildren() - Utils.closeAllTcpSockets() - MmkvManager.clearAllTestDelayResults() - updateListAction.value = -1 // update all - - getApplication().toast(R.string.connection_test_testing) - for (guid in serverList) { - serversCache.getOrElse(guid) { MmkvManager.decodeServerConfig(guid) }?.getProxyOutbound()?.let { outbound -> - val serverAddress = outbound.getServerAddress() - val serverPort = outbound.getServerPort() - if (serverAddress != null && serverPort != null) { - tcpingTestScope.launch { - val testResult = Utils.tcping(serverAddress, serverPort) - launch(Dispatchers.Main) { - MmkvManager.encodeServerTestDelayMillis(guid, testResult) - updateListAction.value = serverList.indexOf(guid) - } - } - } - } - } - } - - 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 - } - } - } - - private val mMsgReceiver = object : BroadcastReceiver() { - override fun onReceive(ctx: Context?, intent: Intent?) { - when (intent?.getIntExtra("key", 0)) { - AppConfig.MSG_STATE_RUNNING -> { - isRunning.value = true - } - AppConfig.MSG_STATE_NOT_RUNNING -> { - isRunning.value = false - } - AppConfig.MSG_STATE_START_SUCCESS -> { - getApplication().toast(R.string.toast_services_success) - isRunning.value = true - } - AppConfig.MSG_STATE_START_FAILURE -> { - getApplication().toast(R.string.toast_services_failure) - isRunning.value = false - } - AppConfig.MSG_STATE_STOP_SUCCESS -> { - isRunning.value = false - } - } - } - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt deleted file mode 100644 index daac06c3..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/SettingsViewModel.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.v2ray.ang.viewmodel - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import android.content.SharedPreferences -import androidx.preference.PreferenceManager -import android.util.Log -import com.tencent.mmkv.MMKV -import com.v2ray.ang.AppConfig -import com.v2ray.ang.util.MmkvManager - -class SettingsViewModel(application: Application) : AndroidViewModel(application), SharedPreferences.OnSharedPreferenceChangeListener { - - private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) } - - fun startListenPreferenceChange() { - PreferenceManager.getDefaultSharedPreferences(getApplication()).registerOnSharedPreferenceChangeListener(this) - } - - override fun onCleared() { - PreferenceManager.getDefaultSharedPreferences(getApplication()).unregisterOnSharedPreferenceChangeListener(this) - Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared") - super.onCleared() - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { - Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key") - when(key) { - AppConfig.PREF_MODE, - AppConfig.PREF_VPN_DNS, - AppConfig.PREF_REMOTE_DNS, - AppConfig.PREF_DOMESTIC_DNS, - AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, - AppConfig.PREF_ROUTING_MODE, - AppConfig.PREF_V2RAY_ROUTING_AGENT, - AppConfig.PREF_V2RAY_ROUTING_BLOCKED, - AppConfig.PREF_V2RAY_ROUTING_DIRECT, -> { - settingsStorage?.encode(key, sharedPreferences.getString(key, "")) - } - AppConfig.PREF_SPEED_ENABLED, - AppConfig.PREF_PROXY_SHARING, - AppConfig.PREF_LOCAL_DNS_ENABLED, - AppConfig.PREF_FAKE_DNS_ENABLED, - AppConfig.PREF_FORWARD_IPV6, - AppConfig.PREF_PER_APP_PROXY, - AppConfig.PREF_BYPASS_APPS, -> { - settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false)) - } - AppConfig.PREF_SNIFFING_ENABLED -> { - settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true)) - } - AppConfig.PREF_PER_APP_PROXY_SET -> { - settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf())) - } - } - } -} diff --git a/V2rayNG/app/src/main/res/anim/fade_in.xml b/V2rayNG/app/src/main/res/anim/fade_in.xml deleted file mode 100644 index 29e04320..00000000 --- a/V2rayNG/app/src/main/res/anim/fade_in.xml +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/V2rayNG/app/src/main/res/anim/fade_out.xml b/V2rayNG/app/src/main/res/anim/fade_out.xml deleted file mode 100644 index 2b8bb1cb..00000000 --- a/V2rayNG/app/src/main/res/anim/fade_out.xml +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_direct.png new file mode 100644 index 00000000..a9fe7cdf Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_direct.png differ diff --git a/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_name.png new file mode 100644 index 00000000..1f0f31fc Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_name.png differ diff --git a/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_proxy.png new file mode 100644 index 00000000..048bb16a Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-hdpi/ic_stat_proxy.png differ diff --git a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_direct.png new file mode 100644 index 00000000..0f0d00c6 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_direct.png differ diff --git a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name.png new file mode 100644 index 00000000..ac443403 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name.png differ diff --git a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name_black.png b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name_black.png new file mode 100644 index 00000000..01dc4ef9 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_name_black.png differ diff --git a/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_proxy.png new file mode 100644 index 00000000..f0a39da4 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-mdpi/ic_stat_proxy.png differ diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_about_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_about_24dp.xml new file mode 100644 index 00000000..d256a2a5 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_about_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_action_done.xml b/V2rayNG/app/src/main/res/drawable-night/ic_action_done.xml new file mode 100644 index 00000000..c3fb8a47 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_action_done.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_add_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_add_24dp.xml new file mode 100644 index 00000000..d2948026 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_add_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_backup_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_backup_24dp.xml new file mode 100644 index 00000000..9f774c85 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_backup_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml new file mode 100644 index 00000000..5a7c2fd0 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_cloud_download_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_cloud_download_24dp.xml new file mode 100644 index 00000000..171a9b30 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_cloud_download_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml b/V2rayNG/app/src/main/res/drawable-night/ic_copy.xml similarity index 55% rename from V2rayNG/app/src/main/res/drawable/ic_copy_white.xml rename to V2rayNG/app/src/main/res/drawable-night/ic_copy.xml index e50927b2..9cafc852 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_copy_white.xml +++ b/V2rayNG/app/src/main/res/drawable-night/ic_copy.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" /> diff --git a/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_delete_24dp.xml similarity index 61% rename from V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml rename to V2rayNG/app/src/main/res/drawable-night/ic_delete_24dp.xml index ab38bb6d..4c803eca 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_delete_white_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable-night/ic_delete_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z" /> diff --git a/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_description_24dp.xml similarity index 56% rename from V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml rename to V2rayNG/app/src/main/res/drawable-night/ic_description_24dp.xml index 7e0d28e3..6f55b8de 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_description_white_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable-night/ic_description_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z" /> diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_edit_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_edit_24dp.xml new file mode 100644 index 00000000..85d32985 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_edit_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_fab_check.xml b/V2rayNG/app/src/main/res/drawable-night/ic_fab_check.xml new file mode 100644 index 00000000..54f825f8 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_fab_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_feedback_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_feedback_24dp.xml new file mode 100644 index 00000000..212bc072 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_feedback_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_file_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_file_24dp.xml new file mode 100644 index 00000000..73387126 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_file_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_image_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_image_24dp.xml new file mode 100644 index 00000000..00449aaf --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_image_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml new file mode 100644 index 00000000..48309ad9 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_logcat_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_logcat_24dp.xml new file mode 100644 index 00000000..77e41a46 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_logcat_24dp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_more_vert_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_more_vert_24dp.xml new file mode 100644 index 00000000..2a252346 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_more_vert_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_outline_filter_alt_24.xml b/V2rayNG/app/src/main/res/drawable-night/ic_outline_filter_alt_24.xml new file mode 100644 index 00000000..07553f4e --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_outline_filter_alt_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_per_apps_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_per_apps_24dp.xml new file mode 100644 index 00000000..99d8212e --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_per_apps_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_play_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_play_24dp.xml new file mode 100644 index 00000000..a3bc803f --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_play_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_privacy_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_privacy_24dp.xml new file mode 100644 index 00000000..1cec1d77 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_privacy_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_promotion_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_promotion_24dp.xml new file mode 100644 index 00000000..e6142817 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_promotion_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_restore_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_restore_24dp.xml new file mode 100644 index 00000000..6b372816 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_restore_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_routing_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_routing_24dp.xml new file mode 100644 index 00000000..7002c085 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_routing_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_save_24dp.xml similarity index 69% rename from V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml rename to V2rayNG/app/src/main/res/drawable-night/ic_save_24dp.xml index a7a81a25..47a92519 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_save_white_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable-night/ic_save_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" /> diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_scan_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_scan_24dp.xml new file mode 100644 index 00000000..9e260e7d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_scan_24dp.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_select_all_24dp.xml similarity index 91% rename from V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml rename to V2rayNG/app/src/main/res/drawable-night/ic_select_all_24dp.xml index a24c01bf..3d37d197 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_select_all_white_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable-night/ic_select_all_24dp.xml @@ -1,9 +1,9 @@ + android:viewportHeight="24.0"> diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_settings_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_settings_24dp.xml new file mode 100644 index 00000000..49347d0d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_settings_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_share_24dp.xml similarity index 79% rename from V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml rename to V2rayNG/app/src/main/res/drawable-night/ic_share_24dp.xml index 90406663..b09d63e8 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_share_white_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable-night/ic_share_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" /> diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_source_code_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_source_code_24dp.xml new file mode 100644 index 00000000..43b540fb --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_source_code_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_stop_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_stop_24dp.xml new file mode 100644 index 00000000..d3fbe696 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_stop_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_subscriptions_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_subscriptions_24dp.xml new file mode 100644 index 00000000..e935b87d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_subscriptions_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_telegram_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_telegram_24dp.xml new file mode 100644 index 00000000..b35b623c --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable-night/ic_telegram_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable-night/nav_header_bg.png b/V2rayNG/app/src/main/res/drawable-night/nav_header_bg.png new file mode 100644 index 00000000..82cb00af Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-night/nav_header_bg.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_direct.png new file mode 100644 index 00000000..2dcd5d92 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_direct.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name.png new file mode 100644 index 00000000..7af529f1 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name_black.png b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name_black.png new file mode 100644 index 00000000..00207a53 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_name_black.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_proxy.png new file mode 100644 index 00000000..36bba560 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xhdpi/ic_stat_proxy.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png deleted file mode 100644 index 8825c532..00000000 Binary files a/V2rayNG/app/src/main/res/drawable-xxhdpi/donate.png and /dev/null differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_direct.png new file mode 100644 index 00000000..f8bd77ef Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_direct.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name.png new file mode 100644 index 00000000..0ac2448f Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name_black.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name_black.png new file mode 100644 index 00000000..516f0893 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_name_black.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_proxy.png new file mode 100644 index 00000000..a10f0fe0 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxhdpi/ic_stat_proxy.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml b/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml deleted file mode 100644 index 6d81870b..00000000 --- a/V2rayNG/app/src/main/res/drawable-xxhdpi/side_nav_bar.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_direct.png similarity index 100% rename from V2rayNG/app/src/main/res/drawable/ic_stat_direct.png rename to V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_direct.png diff --git a/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png new file mode 100644 index 00000000..dd313da0 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png differ diff --git a/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name_black.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name_black.png new file mode 100644 index 00000000..a18be25c Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_name_black.png differ diff --git a/V2rayNG/app/src/main/res/drawable/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_proxy.png similarity index 100% rename from V2rayNG/app/src/main/res/drawable/ic_stat_proxy.png rename to V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_proxy.png diff --git a/V2rayNG/app/src/main/res/drawable/background_test_button.xml b/V2rayNG/app/src/main/res/drawable/background_test_button.xml deleted file mode 100644 index ed2c5e68..00000000 --- a/V2rayNG/app/src/main/res/drawable/background_test_button.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/custom_divider.xml b/V2rayNG/app/src/main/res/drawable/custom_divider.xml new file mode 100644 index 00000000..94f03147 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/custom_divider.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_about_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_about_24dp.xml new file mode 100644 index 00000000..5e1ac138 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_about_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_action_done.xml b/V2rayNG/app/src/main/res/drawable/ic_action_done.xml index 33a117f6..bda675f1 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_action_done.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_action_done.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:fillColor="#FF000000" + android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" /> diff --git a/V2rayNG/app/src/main/res/drawable/ic_add_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_add_24dp.xml new file mode 100644 index 00000000..fedd077d --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_add_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml deleted file mode 100644 index b9b8eca8..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_add_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml deleted file mode 100644 index b520fc98..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_attach_money_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml deleted file mode 100644 index 2b65f0c6..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_attach_money_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_backup_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_backup_24dp.xml new file mode 100644 index 00000000..0eca2402 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_backup_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml new file mode 100644 index 00000000..b0f163c6 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_circle.xml b/V2rayNG/app/src/main/res/drawable/ic_circle.xml new file mode 100644 index 00000000..13bb8e65 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_circle.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml deleted file mode 100644 index 8a9a5226..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_close_grey_800_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_cloud_download_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_cloud_download_24dp.xml new file mode 100644 index 00000000..aebcd40a --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_cloud_download_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_copy.xml b/V2rayNG/app/src/main/res/drawable/ic_copy.xml new file mode 100644 index 00000000..380c8783 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_delete_24dp.xml similarity index 61% rename from V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_delete_24dp.xml index 2f5557af..c3027b2e 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_delete_black_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_delete_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z" /> diff --git a/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_description_24dp.xml similarity index 56% rename from V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_description_24dp.xml index 38c33351..a30bc6ca 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_description_black_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_description_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z" /> diff --git a/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_edit_24dp.xml similarity index 69% rename from V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_edit_24dp.xml index 2ab2fb75..5505cc77 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_edit_black_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_edit_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" /> diff --git a/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml b/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml index 54f825f8..6c1b1677 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_fab_check.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/V2rayNG/app/src/main/res/drawable/ic_fab_uncheck.png b/V2rayNG/app/src/main/res/drawable/ic_fab_uncheck.png deleted file mode 100644 index 39a10413..00000000 Binary files a/V2rayNG/app/src/main/res/drawable/ic_fab_uncheck.png and /dev/null differ diff --git a/V2rayNG/app/src/main/res/drawable/ic_feedback_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_feedback_24dp.xml new file mode 100644 index 00000000..58d18767 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_feedback_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml deleted file mode 100644 index 3e08fae2..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_feedback_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_file_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_file_24dp.xml new file mode 100644 index 00000000..50ca7039 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_file_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_image_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_image_24dp.xml new file mode 100644 index 00000000..e2ff6078 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_image_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml b/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml deleted file mode 100644 index c701a63f..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_image_photo.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml deleted file mode 100644 index 34b8202e..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_info_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_lock_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_lock_24dp.xml new file mode 100644 index 00000000..da0e84b4 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_lock_24dp.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_logcat_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_logcat_24dp.xml new file mode 100644 index 00000000..95fe140c --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_logcat_24dp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml deleted file mode 100644 index e7d3eb31..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_logcat_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_more_vert_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_more_vert_24dp.xml new file mode 100644 index 00000000..32d39196 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_more_vert_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_24.xml b/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_24.xml new file mode 100644 index 00000000..77112182 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_outline_filter_alt_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_per_apps_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_per_apps_24dp.xml new file mode 100644 index 00000000..c45cd8c5 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_per_apps_24dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_play_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_play_24dp.xml new file mode 100644 index 00000000..a3bc803f --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_play_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_privacy_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_privacy_24dp.xml new file mode 100644 index 00000000..ad1a12d7 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_privacy_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_promotion_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_promotion_24dp.xml new file mode 100644 index 00000000..f4283b58 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_promotion_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_scan_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_24dp.xml new file mode 100644 index 00000000..2435e3b8 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_24dp.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml deleted file mode 100644 index a31063ba..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_qu_scan_black_24dp.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml deleted file mode 100644 index 01f193f6..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_qu_settings_black_24dp.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_switch_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_24dp.xml new file mode 100644 index 00000000..79be6f81 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_24dp.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml deleted file mode 100644 index 6bc10fd8..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_qu_switch_black_24dp.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_restore_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_restore_24dp.xml new file mode 100644 index 00000000..12d78b2c --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_restore_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_active.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_active.xml new file mode 100644 index 00000000..3dcd95f7 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_active.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml deleted file mode 100644 index 92f9dc67..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_grey.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_inactive.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_inactive.xml new file mode 100644 index 00000000..96a98158 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_inactive.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml b/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml deleted file mode 100644 index 0463d8bf..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_rounded_corner_theme.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_routing_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_routing_24dp.xml new file mode 100644 index 00000000..9032a2e6 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_routing_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_save_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_save_24dp.xml new file mode 100644 index 00000000..0651fcc6 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_save_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_scan_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_scan_24dp.xml new file mode 100644 index 00000000..f7cc6c32 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_scan_24dp.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml deleted file mode 100644 index a31063ba..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_scan_black_24dp.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_select_all_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_select_all_24dp.xml new file mode 100644 index 00000000..f923b7e8 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_select_all_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_settings_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_settings_24dp.xml new file mode 100644 index 00000000..9bda1725 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_settings_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml deleted file mode 100644 index ce997a72..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_settings_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_share_24dp.xml similarity index 79% rename from V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml rename to V2rayNG/app/src/main/res/drawable/ic_share_24dp.xml index e3fe874d..338d95ad 100644 --- a/V2rayNG/app/src/main/res/drawable/ic_share_black_24dp.xml +++ b/V2rayNG/app/src/main/res/drawable/ic_share_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" /> diff --git a/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml b/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml deleted file mode 100644 index 61d2d992..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_shortcut_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/drawable/ic_source_code_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_source_code_24dp.xml new file mode 100644 index 00000000..800f7b5f --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_source_code_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_start_busy.xml b/V2rayNG/app/src/main/res/drawable/ic_start_busy.xml deleted file mode 100644 index f0154c37..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_start_busy.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_start_connected.xml b/V2rayNG/app/src/main/res/drawable/ic_start_connected.xml deleted file mode 100644 index bc8c3664..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_start_connected.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_start_connected_black.xml b/V2rayNG/app/src/main/res/drawable/ic_start_connected_black.xml deleted file mode 100644 index 90385316..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_start_connected_black.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_start_idle.xml b/V2rayNG/app/src/main/res/drawable/ic_start_idle.xml deleted file mode 100644 index ef9b99d3..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_start_idle.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_stop_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_stop_24dp.xml new file mode 100644 index 00000000..d3fbe696 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_stop_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_24dp.xml new file mode 100644 index 00000000..9e82c7f9 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml deleted file mode 100644 index 6f0ed455..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml deleted file mode 100644 index bc20a83a..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_subscriptions_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_telegram_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_telegram_24dp.xml new file mode 100644 index 00000000..f1f8fa5a --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/ic_telegram_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/ic_v.xml b/V2rayNG/app/src/main/res/drawable/ic_v.xml deleted file mode 100644 index cb4930cc..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_v.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_v_connected_black.xml b/V2rayNG/app/src/main/res/drawable/ic_v_connected_black.xml deleted file mode 100644 index 0c216c12..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_v_connected_black.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_v_idle.xml b/V2rayNG/app/src/main/res/drawable/ic_v_idle.xml deleted file mode 100644 index 87f0cca1..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_v_idle.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml deleted file mode 100644 index 1cbc037f..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_whatshot_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml deleted file mode 100644 index ad460f3c..00000000 --- a/V2rayNG/app/src/main/res/drawable/ic_whatshot_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/V2rayNG/app/src/main/res/drawable/license_24px.xml b/V2rayNG/app/src/main/res/drawable/license_24px.xml new file mode 100644 index 00000000..5b63fd29 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/license_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/nav_header_bg.png b/V2rayNG/app/src/main/res/drawable/nav_header_bg.png index d383f6a5..fd79d42d 100644 Binary files a/V2rayNG/app/src/main/res/drawable/nav_header_bg.png and b/V2rayNG/app/src/main/res/drawable/nav_header_bg.png differ diff --git a/V2rayNG/app/src/main/res/font/montserrat_thin.ttf b/V2rayNG/app/src/main/res/font/montserrat_thin.ttf new file mode 100644 index 00000000..4c5c8f7e Binary files /dev/null and b/V2rayNG/app/src/main/res/font/montserrat_thin.ttf differ diff --git a/V2rayNG/app/src/main/res/layout/activity_about.xml b/V2rayNG/app/src/main/res/layout/activity_about.xml new file mode 100644 index 00000000..62053559 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml index 7b1028db..0fc67600 100644 --- a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml +++ b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml @@ -1,113 +1,106 @@ - + android:layout_height="match_parent" + android:fitsSystemWindows="true" + android:orientation="vertical" + tools:context=".ui.PerAppProxyActivity"> - + android:indeterminate="true" + android:visibility="invisible" + app:indicatorColor="@color/color_fab_active" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:scrollbars="vertical" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_check_update.xml b/V2rayNG/app/src/main/res/layout/activity_check_update.xml new file mode 100644 index 00000000..29345075 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_check_update.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_logcat.xml b/V2rayNG/app/src/main/res/layout/activity_logcat.xml index 06916be1..05db70a7 100644 --- a/V2rayNG/app/src/main/res/layout/activity_logcat.xml +++ b/V2rayNG/app/src/main/res/layout/activity_logcat.xml @@ -1,38 +1,26 @@ + android:fitsSystemWindows="true" + tools:context=".ui.LogcatActivity"> - + android:layout_height="match_parent"> - - - - + android:layout_height="match_parent" + android:scrollbars="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_main.xml b/V2rayNG/app/src/main/res/layout/activity_main.xml index 972af7c1..e5f6964c 100644 --- a/V2rayNG/app/src/main/res/layout/activity_main.xml +++ b/V2rayNG/app/src/main/res/layout/activity_main.xml @@ -1,38 +1,30 @@ + android:layout_height="match_parent"> + android:layout_height="wrap_content"> + android:layout_height="?attr/actionBarSize" /> - + android:layout_height="match_parent"> + + + + + android:nextFocusRight="@+id/fab" + android:scrollbars="vertical" /> + android:focusable="true" + android:nextFocusLeft="@+id/recycler_view" + android:nextFocusRight="@+id/fab" + android:orientation="vertical"> + + - + android:textAppearance="@style/TextAppearance.AppCompat.Small" /> - - + android:layout_gravity="bottom|end" + android:layout_marginEnd="@dimen/padding_spacing_dp16"> + android:nextFocusLeft="@+id/layout_test" + android:src="@drawable/ic_play_24dp" + app:layout_anchorGravity="bottom|right|end" /> - + @@ -111,24 +119,9 @@ android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/nav_header" - app:itemIconTint="@color/colorPrimary_dark" - app:itemTextColor="@color/colorPrimary" - app:menu="@menu/menu_drawer" > + app:itemIconTint="@color/colorAccent" + app:menu="@menu/menu_drawer"> - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_none.xml b/V2rayNG/app/src/main/res/layout/activity_none.xml index 1aa97aea..c7d9548b 100644 --- a/V2rayNG/app/src/main/res/layout/activity_none.xml +++ b/V2rayNG/app/src/main/res/layout/activity_none.xml @@ -1,6 +1,4 @@ - + android:layout_height="match_parent"> diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml b/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml new file mode 100644 index 00000000..53a49fec --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_setting.xml b/V2rayNG/app/src/main/res/layout/activity_routing_setting.xml new file mode 100644 index 00000000..d79b4c84 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_routing_setting.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml b/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml deleted file mode 100644 index 7052cc5f..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml b/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml index 4f0a5e68..d6d587b8 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml @@ -3,7 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.ServerActivity"> + android:fitsSystemWindows="true" + tools:context=".ui.ServerCustomConfigActivity"> + android:layout_margin="@dimen/padding_spacing_dp8" + android:orientation="vertical"> + android:textColor="@color/color_secondary" /> + android:layout_margin="@dimen/padding_spacing_dp8" + android:orientation="vertical"> @@ -64,19 +65,17 @@ + android:id="@+id/editor" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="@dimen/padding_spacing_dp16" + android:gravity="top|start" /> - - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml b/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml new file mode 100644 index 00000000..86df50ef --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml index 3f0af8fb..0d9e958f 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml @@ -3,86 +3,21 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> + android:padding="@dimen/padding_spacing_dp16"> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + android:inputType="text" /> @@ -102,7 +37,7 @@ + + + - \ No newline at end of file + diff --git a/V2rayNG/app/src/main/res/layout/activity_server_socks.xml b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml index 6658178d..90c1b883 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_socks.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml @@ -3,41 +3,33 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> + android:padding="@dimen/padding_spacing_dp16"> + + - + android:text="@string/server_lab_security4" /> - - - @@ -45,18 +37,18 @@ + android:text="@string/server_lab_id4" /> @@ -64,67 +56,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginTop="@dimen/padding_spacing_dp16" + android:layout_marginBottom="@dimen/padding_spacing_dp16" + android:orientation="vertical" /> - + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml b/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml new file mode 100644 index 00000000..2159ec43 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server_trojan.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server_vless.xml b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml new file mode 100644 index 00000000..7d29efd2 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml index ff11d9f0..4e77cbdf 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml @@ -3,86 +3,21 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".ui.ServerActivity"> + android:padding="@dimen/padding_spacing_dp16"> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -101,7 +36,7 @@ - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml b/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml new file mode 100644 index 00000000..7ecd7ce1 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server_wireguard.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_settings.xml b/V2rayNG/app/src/main/res/layout/activity_settings.xml index f674f2ae..618bc8c0 100644 --- a/V2rayNG/app/src/main/res/layout/activity_settings.xml +++ b/V2rayNG/app/src/main/res/layout/activity_settings.xml @@ -1,8 +1,16 @@ - \ No newline at end of file + android:fitsSystemWindows="true" + android:orientation="vertical" + tools:context=".ui.SettingsActivity"> + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml index 91f17357..af5d70cd 100644 --- a/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml +++ b/V2rayNG/app/src/main/res/layout/activity_sub_edit.xml @@ -1,9 +1,11 @@ + android:fitsSystemWindows="true" + tools:context=".ui.SubEditActivity"> + android:padding="@dimen/padding_spacing_dp16"> @@ -50,7 +52,7 @@ + android:minLines="2" + android:scrollbars="vertical" + tools:ignore="TextFields" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml b/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml index 5e313362..1654701d 100644 --- a/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml +++ b/V2rayNG/app/src/main/res/layout/activity_sub_setting.xml @@ -1,22 +1,25 @@ - + tools:context=".ui.SubSettingActivity"> - + android:layout_height="wrap_content" + android:indeterminate="true" + android:visibility="invisible" + app:indicatorColor="@color/color_fab_active" /> - + - - + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_tasker.xml b/V2rayNG/app/src/main/res/layout/activity_tasker.xml index 8994d841..56a1276d 100644 --- a/V2rayNG/app/src/main/res/layout/activity_tasker.xml +++ b/V2rayNG/app/src/main/res/layout/activity_tasker.xml @@ -1,31 +1,32 @@ + android:layout_height="wrap_content" + android:padding="@dimen/padding_spacing_dp8"> + android:checked="true" + app:theme="@style/BrandedSwitch" /> @@ -45,4 +46,4 @@ android:choiceMode="singleChoice" /> - + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_user_asset.xml b/V2rayNG/app/src/main/res/layout/activity_user_asset.xml new file mode 100644 index 00000000..08d7583f --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_user_asset.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml b/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml new file mode 100644 index 00000000..e1b55173 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml b/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml new file mode 100644 index 00000000..0bbc9003 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/dialog_config_filter.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml b/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml deleted file mode 100644 index 815b3a31..00000000 --- a/V2rayNG/app/src/main/res/layout/fragment_routing_settings.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/item_qrcode.xml b/V2rayNG/app/src/main/res/layout/item_qrcode.xml index 3c53162a..d3ea9075 100644 --- a/V2rayNG/app/src/main/res/layout/item_qrcode.xml +++ b/V2rayNG/app/src/main/res/layout/item_qrcode.xml @@ -1,5 +1,6 @@ + app:srcCompat="@drawable/ic_fab_check" /> diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml b/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml index d705e5eb..889cb3c6 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_bypass_list.xml @@ -2,51 +2,47 @@ + android:focusable="true" + android:gravity="center_vertical" + android:padding="@dimen/padding_spacing_dp8"> + android:layout_width="@dimen/view_height_dp48" + android:layout_height="@dimen/view_height_dp48" + android:padding="@dimen/padding_spacing_dp8" /> + android:orientation="vertical"> + android:layout_height="wrap_content" + android:maxLines="1" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> - + android:padding="@dimen/padding_spacing_dp8" /> - + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml index 268f7690..01db7a96 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_footer.xml @@ -8,25 +8,25 @@ + android:padding="@dimen/padding_spacing_dp16" + android:visibility="invisible"> + android:orientation="vertical"> + android:textColor="@color/color_secondary" /> \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_logcat.xml b/V2rayNG/app/src/main/res/layout/item_recycler_logcat.xml new file mode 100644 index 00000000..73d4e23d --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/item_recycler_logcat.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml index a0855c40..313492d4 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml @@ -1,186 +1,219 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/item_bg" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical"> - + android:layout_gravity="center" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:gravity="center" + android:nextFocusRight="@+id/layout_share" + android:orientation="horizontal" + android:paddingStart="@dimen/padding_spacing_dp4" + android:paddingTop="@dimen/padding_spacing_dp8" + android:paddingEnd="@dimen/padding_spacing_dp4" + android:paddingBottom="@dimen/padding_spacing_dp8"> + android:orientation="vertical" /> - + - - - + android:orientation="horizontal"> + android:layout_weight="1" + android:orientation="vertical"> - + android:orientation="vertical" + android:paddingStart="@dimen/padding_spacing_dp8"> - - - - - - - - - - - - + android:maxLines="2" + android:minLines="1" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> + + + + + + + + + + + + + + + android:gravity="center" + android:nextFocusLeft="@+id/info_container" + android:orientation="vertical" + android:padding="@dimen/padding_spacing_dp8"> + android:layout_height="@dimen/image_size_dp24" + app:srcCompat="@drawable/ic_share_24dp" /> + android:padding="@dimen/padding_spacing_dp8"> + android:layout_width="@dimen/image_size_dp24" + android:layout_height="@dimen/image_size_dp24" + app:srcCompat="@drawable/ic_edit_24dp" /> + android:padding="@dimen/padding_spacing_dp8"> + android:layout_width="@dimen/image_size_dp24" + android:layout_height="@dimen/image_size_dp24" + app:srcCompat="@drawable/ic_delete_24dp" /> + + + + + + - + + + + + + - - - + android:lines="1" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + android:textColor="@color/colorPing" + android:textSize="11sp" + tools:text="214ms" /> + - + + diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_routing_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_routing_setting.xml new file mode 100644 index 00000000..e104ef97 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/item_recycler_routing_setting.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml index aa370e41..47c2b4c5 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml @@ -1,106 +1,158 @@ - + android:padding="@dimen/padding_spacing_dp8"> + android:layout_height="wrap_content" + android:orientation="vertical"> - - - - - - - - - - - - + android:paddingStart="@dimen/padding_spacing_dp8"> - + + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + - - + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml new file mode 100644 index 00000000..61cd2a67 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/layout_address_port.xml b/V2rayNG/app/src/main/res/layout/layout_address_port.xml new file mode 100644 index 00000000..1c0111ca --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/layout_address_port.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/layout_tls.xml b/V2rayNG/app/src/main/res/layout/layout_tls.xml new file mode 100644 index 00000000..b7c34a12 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/layout_tls.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/layout_tls_hysteria2.xml b/V2rayNG/app/src/main/res/layout/layout_tls_hysteria2.xml new file mode 100644 index 00000000..fc279bcb --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/layout_tls_hysteria2.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/layout_transport.xml b/V2rayNG/app/src/main/res/layout/layout_transport.xml new file mode 100644 index 00000000..85712ff8 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/layout_transport.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/nav_header.xml b/V2rayNG/app/src/main/res/layout/nav_header.xml index 8eeb7936..44a175d2 100644 --- a/V2rayNG/app/src/main/res/layout/nav_header.xml +++ b/V2rayNG/app/src/main/res/layout/nav_header.xml @@ -1,24 +1,21 @@ - + android:padding="@dimen/padding_spacing_dp16"> + android:textAppearance="@style/TextAppearance.AppCompat.Display1" /> diff --git a/V2rayNG/app/src/main/res/layout/nav_toolbar.xml b/V2rayNG/app/src/main/res/layout/nav_toolbar.xml deleted file mode 100644 index c77a5f07..00000000 --- a/V2rayNG/app/src/main/res/layout/nav_toolbar.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/V2rayNG/app/src/main/res/layout/nav_view.xml b/V2rayNG/app/src/main/res/layout/nav_view.xml deleted file mode 100644 index 8268c6e8..00000000 --- a/V2rayNG/app/src/main/res/layout/nav_view.xml +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml b/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml index ec6e6423..6d21827b 100644 --- a/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml +++ b/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml @@ -2,10 +2,10 @@