diff --git a/.github/issue_template.md b/.github/ISSUE_TEMPLATE/bug_cn.md similarity index 69% rename from .github/issue_template.md rename to .github/ISSUE_TEMPLATE/bug_cn.md index 71a6f01c..eaa1bc38 100644 --- a/.github/issue_template.md +++ b/.github/ISSUE_TEMPLATE/bug_cn.md @@ -1,3 +1,8 @@ +--- +name: v2rayNG程序问题 +about: 创建一个报告来帮助我们改进 +--- + 在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。 @@ -14,7 +19,8 @@ ### 日志信息
-通过 `adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main` 获取日志。请自行删减日志中可能出现的敏感信息。 + +通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。 如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。 ``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3cd47d28 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: V2Ray程序问题 + url: https://github.com/v2fly/v2ray-core/ + about: 如果您有V2Ray而非v2rayNG的问题,请至这个链接讨论。 \ No newline at end of file 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 ca9dfff2..9582b273 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +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 -V2rayNG/gradle/wrapper/gradle-wrapper.properties -V2rayNG/gradle/wrapper/gradle-wrapper.properties +# Ignore data and key store files *.dat *.jks -V2rayNG/gradle/wrapper/gradle-wrapper.properties -V2rayNG/gradle/wrapper/gradle-wrapper.properties -V2rayNG/app/release/output.json \ No newline at end of file + +# Ignore output JSON file +V2rayNG/app/release/output.json + +# Ignore IDE and build system directories +.idea/ +.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/.travis.yml b/AndroidLibV2rayLite/.travis.yml deleted file mode 100644 index c3beb7db..00000000 --- a/AndroidLibV2rayLite/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -sudo: required -language: go -go: - - "1.12" -go_import_path: github.com/2dust/AndroidLibV2rayLite -git: - depth: 5 -addons: - apt: - update: true -before_script: -- sudo ntpdate -u time.google.com -- date -- make all -- make downloadGoMobile -script: -- make BuildMobile -after_success: -deploy: - provider: releases - api_key: ${GH_TOKEN} - file: - - libv2ray.aar - skip_cleanup: true - on: - tags: true diff --git a/AndroidLibV2rayLite/CoreI/Status.go b/AndroidLibV2rayLite/CoreI/Status.go deleted file mode 100644 index a5d4b942..00000000 --- a/AndroidLibV2rayLite/CoreI/Status.go +++ /dev/null @@ -1,63 +0,0 @@ -package CoreI - -import ( - v2core "v2ray.com/core" -) - -type Status struct { - IsRunning bool - PackageName string - - Vpoint v2core.Server -} - -func CheckVersion() int { - return 20 -} - -func (v *Status) GetDataDir() string { - return v.PackageName -} - -func (v *Status) GetApp(name string) string { - return v.PackageName + 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 f4a4ec68..00000000 --- a/AndroidLibV2rayLite/Makefile +++ /dev/null @@ -1,34 +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 - -shippedBinary: - cd shippedBinarys; $(MAKE) shippedBinary - -fetchDep: - -go get github.com/2dust/AndroidLibV2rayLite - go get github.com/2dust/AndroidLibV2rayLite - -ANDROID_HOME=$(HOME)/android-sdk-linux -export ANDROID_HOME -PATH:=$(PATH):$(GOPATH)/bin -export PATH -downloadGoMobile: - go get golang.org/x/mobile/cmd/... - 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/ - gomobile init ;gomobile bind -v -tags json github.com/2dust/AndroidLibV2rayLite - -BuildMobile: - @echo Stub - -all: asset pb shippedBinary fetchDep - @echo DONE diff --git a/AndroidLibV2rayLite/Process/Escort/escort.go b/AndroidLibV2rayLite/Process/Escort/escort.go deleted file mode 100644 index 69c9430a..00000000 --- a/AndroidLibV2rayLite/Process/Escort/escort.go +++ /dev/null @@ -1,87 +0,0 @@ -package Escort - -import ( - "os" - "os/exec" - "time" - - "log" - - "github.com/2dust/AndroidLibV2rayLite/CoreI" -) - -func (v *Escorting) EscortRun(proc string, pt []string, additionalEnv string, sendFd func() int) { - 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 count > 0 { - go func() { - time.Sleep(time.Second) - sendFd() - }() - } - - 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 1c1962bc..00000000 --- a/AndroidLibV2rayLite/README.md +++ /dev/null @@ -1 +0,0 @@ -# AndroidLibV2rayLite diff --git a/AndroidLibV2rayLite/VPN/vpnservice_support.go b/AndroidLibV2rayLite/VPN/vpnservice_support.go deleted file mode 100644 index f7456cac..00000000 --- a/AndroidLibV2rayLite/VPN/vpnservice_support.go +++ /dev/null @@ -1,279 +0,0 @@ -package VPN - -import ( - "context" - "errors" - "fmt" - "log" - "net" - "os" - "strings" - "sync" - "time" - - "golang.org/x/sys/unix" - v2net "v2ray.com/core/common/net" - v2internet "v2ray.com/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 -} - -// 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 883f2f6e..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 "v2ray.com/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/interact.go b/AndroidLibV2rayLite/interact.go deleted file mode 100644 index b15df2dc..00000000 --- a/AndroidLibV2rayLite/interact.go +++ /dev/null @@ -1,255 +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" - "github.com/2dust/AndroidLibV2rayLite/shippedBinarys" - mobasset "golang.org/x/mobile/asset" - - v2core "v2ray.com/core" - v2filesystem "v2ray.com/core/common/platform/filesystem" - v2stats "v2ray.com/core/features/stats" - v2serial "v2ray.com/core/infra/conf/serial" - _ "v2ray.com/core/main/distro/all" - v2internet "v2ray.com/core/transport/internet" - - v2applog "v2ray.com/core/app/log" - v2commlog "v2ray.com/core/common/log" -) - -const ( - v2Assert = "v2ray.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 - DomainName string - ConfigureFileContent string - EnableLocalDNS bool - ForwardIpv6 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 - SendFd() 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 - - 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() - v.SupportSet.Shutdown() - } - - // 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 { - close(v.closeChan) - v.shutdownInit() - v.SupportSet.OnEmitStatus(0, "Closed") - } - return -} - -//Delegate Funcation -func (v *V2RayPoint) GetIsRunning() bool { - return v.status.IsRunning -} - -//Delegate Funcation -func (v V2RayPoint) QueryStats(tag string, direct string) int64 { - if v.statsManager == nil { - return 0 - } - counter := v.statsManager.GetCounter(fmt.Sprintf("inbound>>>%s>>>traffic>>>%s", tag, direct)) - if counter == nil { - return 0 - } - return counter.Set(0) -} - -func (v *V2RayPoint) shutdownInit() { - v.status.IsRunning = false - v.status.Vpoint.Close() - v.status.Vpoint = nil - v.statsManager = nil - v.escorter.EscortingDown() -} - -func (v *V2RayPoint) pointloop() error { - if err := v.runTun2socks(); err != nil { - log.Println(err) - return err - } - - log.Printf("EnableLocalDNS: %v\nForwardIpv6: %v\nDomainName: %s", - v.EnableLocalDNS, - v.ForwardIpv6, - v.DomainName) - - 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") - 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 { - shipb := shippedBinarys.FirstRun{Status: v.status} - if err := shipb.CheckAndExport(); err != nil { - log.Println(err) - return err - } - - v.escorter.EscortingUp() - go v.escorter.EscortRun( - v.status.GetApp("tun2socks"), - v.status.GetTun2socksArgs(v.EnableLocalDNS, v.ForwardIpv6), "", - v.SupportSet.SendFd) - - 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/shippedBinarys/ArchDep/386/tun2socks b/AndroidLibV2rayLite/shippedBinarys/ArchDep/386/tun2socks deleted file mode 100644 index f5c05b58..00000000 Binary files a/AndroidLibV2rayLite/shippedBinarys/ArchDep/386/tun2socks and /dev/null differ diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchDep/amd64/tun2socks b/AndroidLibV2rayLite/shippedBinarys/ArchDep/amd64/tun2socks deleted file mode 100644 index 8f426499..00000000 Binary files a/AndroidLibV2rayLite/shippedBinarys/ArchDep/amd64/tun2socks and /dev/null differ diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm/tun2socks b/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm/tun2socks deleted file mode 100644 index b1aa74e3..00000000 Binary files a/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm/tun2socks and /dev/null differ diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm64/tun2socks b/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm64/tun2socks deleted file mode 100644 index b46649ef..00000000 Binary files a/AndroidLibV2rayLite/shippedBinarys/ArchDep/arm64/tun2socks and /dev/null differ diff --git a/AndroidLibV2rayLite/shippedBinarys/ArchIndep/holder b/AndroidLibV2rayLite/shippedBinarys/ArchIndep/holder deleted file mode 100644 index e69de29b..00000000 diff --git a/AndroidLibV2rayLite/shippedBinarys/Makefile b/AndroidLibV2rayLite/shippedBinarys/Makefile deleted file mode 100644 index f9c450e0..00000000 --- a/AndroidLibV2rayLite/shippedBinarys/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -Platdep=shippedBinary.386 shippedBinary.amd64 shippedBinary.arm64 shippedBinary.arm - -shippedBinaryDep: - go get -u github.com/jteeuwen/go-bindata/... - -shippedBinary.%: - go-bindata -nometadata -nomemcopy -pkg shippedBinarys -o ./binary_$*.go -tags $* ArchIndep/ ArchDep/$*/ - -shippedBinary:shippedBinaryDep $(Platdep) - @echo "Done" - -clean: - -rm binary* diff --git a/AndroidLibV2rayLite/shippedBinarys/firstrun.go b/AndroidLibV2rayLite/shippedBinarys/firstrun.go deleted file mode 100644 index fcbd8c7c..00000000 --- a/AndroidLibV2rayLite/shippedBinarys/firstrun.go +++ /dev/null @@ -1,69 +0,0 @@ -package shippedBinarys - -import ( - "log" - "os" - "strconv" - - "github.com/2dust/AndroidLibV2rayLite/CoreI" -) - -type FirstRun struct { - Status *CoreI.Status -} - -func (v *FirstRun) checkIfRcExist() error { - datadir := v.Status.GetDataDir() - if _, err := os.Stat(datadir + strconv.Itoa(CoreI.CheckVersion())); !os.IsNotExist(err) { - log.Println("file exists") - return nil - } - - - IndepDir, err := AssetDir("ArchIndep") - log.Println(IndepDir) - if err != nil { - return err - } - for _, fn := range IndepDir { - log.Println(datadir+"ArchIndep/"+fn) - - err := RestoreAsset(datadir, "ArchIndep/"+fn) - log.Println(err) - - //GrantPremission - os.Chmod(datadir+"ArchIndep/"+fn, 0700) - log.Println(os.Remove(datadir + fn)) - log.Println(os.Symlink(datadir+"ArchIndep/"+fn, datadir + fn)) - } - - - DepDir, err := AssetDir("ArchDep") - log.Println(DepDir) - if err != nil { - return err - } - for _, fn := range DepDir { - DepDir2, err := AssetDir("ArchDep/" + fn) - log.Println("ArchDep/" + fn) - if err != nil { - return err - } - for _, FND := range DepDir2 { - log.Println(datadir+"ArchDep/"+fn+"/"+FND) - - RestoreAsset(datadir, "ArchDep/"+fn+"/"+FND) - os.Chmod(datadir+"ArchDep/"+fn+"/"+FND, 0700) - log.Println(os.Remove(datadir + FND)) - log.Println(os.Symlink(datadir+"ArchDep/"+fn+"/"+FND, datadir+FND)) - } - } - s, _ := os.Create(datadir + strconv.Itoa(CoreI.CheckVersion())) - s.Close() - - return nil -} - -func (v *FirstRun) CheckAndExport() error { - return v.checkIfRcExist() -} 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 e78a5abd..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 "v2ray.com/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/README.md b/README.md index 3e26b85f..4bd6f8ec 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,32 @@ # v2rayNG - -Get it on Google Play - +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-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) + +### 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/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) + +### 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 [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 a1ff9805..00000000 --- a/V2rayNG/app/build.gradle +++ /dev/null @@ -1,130 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' - -android { - compileSdkVersion 28 - buildToolsVersion '28.0.3' - - compileOptions { - targetCompatibility = "8" - sourceCompatibility = "8" - } - - defaultConfig { - applicationId "com.v2ray.ang" - minSdkVersion 17 - targetSdkVersion Integer.parseInt("$targetSdkVer") - multiDexEnabled true - versionCode 212 - versionName "1.0.2" - } - - signingConfigs { - release { - storeFile file("../key.jks") - keyAlias 'ang' - keyPassword '123456' - storePassword '123456' - } - debug { - storeFile file("../key.jks") - keyAlias 'ang' - keyPassword '123456' - storePassword '123456' - } - } - - buildTypes { - release { - minifyEnabled false - zipAlignEnabled false - shrinkResources false - signingConfig signingConfigs.release -// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - debug { - minifyEnabled false - zipAlignEnabled false - shrinkResources false - signingConfig signingConfigs.release - } - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - 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.versionCodeOverride = - project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * - 1000000 + android.defaultConfig.versionCode - } - } -} - -dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - testImplementation 'junit:junit:4.12' - implementation project(':dpreference') - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" - // Android support library - implementation "com.android.support:support-v4:$supportLibVersion" - implementation "com.android.support:appcompat-v7:$supportLibVersion" - implementation "com.android.support:design:$supportLibVersion" - implementation "com.android.support:cardview-v7:$supportLibVersion" - implementation "com.android.support:preference-v7:$supportLibVersion" - implementation "com.android.support:recyclerview-v7:$supportLibVersion" - implementation "com.android.support:multidex:1.0.3" - // DSL - implementation "org.jetbrains.anko:anko-sdk15:$ankoVersion" - implementation "org.jetbrains.anko:anko-support-v4:$ankoVersion" - implementation "org.jetbrains.anko:anko-appcompat-v7:$ankoVersion" - implementation "org.jetbrains.anko:anko-design:$ankoVersion" - implementation 'com.google.code.gson:gson:2.8.5' - implementation 'io.reactivex:rxjava:1.3.4' - implementation 'io.reactivex:rxandroid:1.2.1' - implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar' - implementation 'com.dinuscxj:recycleritemdecoration:1.0.0' - implementation 'io.reactivex:rxkotlin:0.60.0' - 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 'com.beust:klaxon:3.0.1' - implementation 'com.android.support:multidex:1.0.3' - - implementation(name: 'libv2ray', ext: 'aar') - //implementation(name: 'tun2socks', ext: 'aar') -} - -buildscript { - repositories { - google() - jcenter() - maven { url 'https://maven.google.com' } - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion" - } -} -repositories { - flatDir { - dirs 'libs' - } -} \ No newline at end of file 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 9b0f9359..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 e56daed3..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,46 +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 org.jetbrains.anko.internals.** --keep class org.jetbrains.anko.internals.** { *;} - --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 b47a46b6..00e4b747 100644 --- a/V2rayNG/app/src/main/AndroidManifest.xml +++ b/V2rayNG/app/src/main/AndroidManifest.xml @@ -1,48 +1,72 @@ + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="MissingLeanbackLauncher"> + android:anyDensity="true" + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" + android:xlargeScreens="true" /> - - + - - + + + + + + + + + - - + + + + + + android:theme="@style/AppThemeDayNight" + android:usesCleartextTraffic="true" + tools:targetApi="m"> + + android:exported="true" + android:launchMode="singleTask" + android:theme="@style/AppThemeDayNight.NoActionBar"> - - - - - - - + @@ -54,34 +78,84 @@ + android:name=".ui.SettingsActivity" + android:exported="false" /> - - - - - + android:name=".ui.PerAppProxyActivity" + android:exported="false" /> - + android:name=".ui.ScannerActivity" + android:exported="false" /> + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -91,45 +165,108 @@ + - - - - + + + - - - - - + + + + + + + + + + + + + + + + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" + android:process=":RunSoLibV2RayDaemon" + tools:targetApi="24"> + + android:exported="true" + android:icon="@mipmap/ic_launcher"> - + + + + + + + + + + - \ No newline at end of file + 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:

+ + +
                                 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.
+
+
+
+ + +
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
+
+
+ + +
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 a6033161..12abf7db 100644 --- a/V2rayNG/app/src/main/assets/proxy_packagename.txt +++ b/V2rayNG/app/src/main/assets/proxy_packagename.txt @@ -1,196 +1,413 @@ -com.android.chrome -com.google.android.googlequicksearchbox -com.google.android.apps.photos -com.google.android.youtube -com.google.android.gm -com.google.android.apps.plus -com.android.vending -com.google.android.inputmethod.latin -com.google.android.apps.paidtasks -com.google.android.keep -com.google.android.gms.setup -com. google.android. apps.magazines -com.google.android.videos -com. google.android.gms -com.google.android.apps.books -com.google.android.music -com.google.android.play.games -com.google.android.gsf -com.google.android.gsf.login -com.app.pornhub -com.spotify.music -org.thunderdog.challegram -com.tumblr -com.twitter.android -com.xda.labs -com.kapp.youtube.final -com.google.android.ims -com.wire -mark.via.gp -com.downloader.video.tumblr -com.sololearn -com.cygames.shadowverse -com.felixfilip.scpae amanita_design.samorost3.gp -com.devolver.reigns2 -com.utopia.pxview -ch.protonmail.android -com.perol.asdpl.pixivez -com.pinterest -com.paypal.android.p2pmobile -com.arthurivanets.owly -com.rubenmayayo.reddit -com.rayark.cytus2 -com.rayark.pluto -com.rayark.implosion -com.fireproofstudios.theroom4 -com.netflix.mediaclient -com.instagram.android -com.google.android.apps.hangoutsdialer -com.google.android.talk -com.google.android.apps.plus -com.google.android.apps.pdfviewer -com.google.android.apps.magazines -com.google.android.apps.nbu.files -com.evernote -net.tsapps.appsales -com.google.android.apps.translate -com.google.ar.lens -com.google.android.apps.adm -com.google.android.apps.googleassistant -tw.com.gamer.android.activecenter -org.telegram.plus -com.brave.browser -com.breel.wallpapers18 -com.teslacoilsw.launcher -com.lastpass.lpandroid -org.kustom.widget -com.fooview.android.fooview -com.google.android.apps.docs -com.google.android.apps.maps -com.facebook.services -com.facebook.system -com.facebook.katana -com.nianticlabs.ingress.prime.qa -com.vanced.android.youtube -com.nianticproject.ingress -com.quoord.tapatalkpro.activity -org.mozilla.firefox -com.reddit.frontpage -com.google.android.apps.fitness +android au.com.shiftyjelly.pocketcasts -com.google.android.gms -com.android.providers.telephony -com.resilio.sync -com.google.android.apps.googlevoice -com.discord -com.cradle.iitc_mobile +bbc.mobile.news.ww be.mygod.vpnhotspot +ch.protonmail.android +cm.aptoide.pt +co.wanqu.android com.alphainventor.filemanager +com.amazon.kindle +com.amazon.mshop.android.shopping +com.android.chrome com.android.providers.downloads +com.android.providers.downloads.ui +com.android.providers.telephony +com.android.settings +com.android.vending +com.android6park.m6park com.apkpure.aegon +com.apkupdater +com.app.pornhub +com.arthurivanets.owly +com.asahi.tida.tablet +com.authy.authy +com.avmovie com.ballistiq.artstation +com.binance.dev com.bitly.app +com.brave.browser +com.brave.browser_beta +com.breel.wallpapers18 +com.bvanced.android.youtube +com.chrome.beta 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 +com.downloader.video.tumblr +com.driverbrowser com.dropbox.android +com.duolingo +com.duckduckgo.mobile.android +com.dv.adm com.estrongs.android.pop com.estrongs.android.pop.pro +com.evernote +com.facebook.katana +com.facebook.lite +com.facebook.mlite +com.facebook.orca +com.facebook.services +com.facebook.system com.fastaccess.github +com.felixfilip.scpae +com.fireproofstudios.theroom4 com.firstrowria.pushnotificationtester +com.flyersoft.moonreaderp +com.fooview.android.fooview com.fvd.eversync +com.gameloft.android.anmp.glofta8hm +com.gameloft.android.anmp.glofta9hm com.gianlu.aria2app com.github.yeriomin.yalpstore +com.google.android.apps.adm +com.google.android.apps.books +com.google.android.apps.docs com.google.android.apps.docs.editors.sheets +com.google.android.apps.fitness +com.google.android.apps.googleassistant +com.google.android.apps.googlevoice +com.google.android.apps.hangoutsdialer +com.google.android.apps.inbox +com.google.android.apps.magazines +com.google.android.apps.maps +com.google.android.apps.nbu.files +com.google.android.apps.paidtasks +com.google.android.apps.pdfviewer +com.google.android.apps.photos +com.google.android.apps.plus +com.google.android.apps.translate +com.google.android.gm +com.google.android.gms +com.google.android.gms.setup +com.google.android.googlequicksearchbox +com.google.android.gsf +com.google.android.gsf.login +com.google.android.ims +com.google.android.inputmethod.latin com.google.android.instantapps.supervisor +com.google.android.keep +com.google.android.music com.google.android.ogyoutube com.google.android.partnersetup +com.google.android.play.games +com.google.android.street com.google.android.syncadapters.calendar com.google.android.syncadapters.contacts +com.google.android.talk com.google.android.tts +com.google.android.videos +com.google.android.youtube +com.google.ar.lens com.hochan.coldsoup 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 +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 com.microsoft.office.powerpoint +com.microsoft.skydrive com.mixplorer com.msd.consumerchinese com.msd.professionalchinese com.mss2011c.sharehelper +com.netflix.mediaclient com.newin.nplayer.pro +com.nianticlabs.ingress.prime.qa +com.nianticproject.ingress +com.ninefolders.hd3 +com.ninegag.android.app +com.nintendo.zara +com.nytimes.cn com.oasisfeng.island +com.ocnt.liveapp.hw com.orekie.search +com.patreon.android +com.paypal.android.p2pmobile +com.perol.asdpl.pixivez +com.pinterest +com.popularapp.periodcalendar com.popularapp.videodownloaderforinstagram com.pushbullet.android +com.quoord.tapatalkpro.activity +com.quora.android +com.rayark.cytus2 +com.rayark.implosion +com.rayark.pluto +com.reddit.frontpage +com.resilio.sync com.rhmsoft.edit +com.rubenmayayo.reddit +com.sec.android.app.sbrowser +com.sec.android.app.sbrowser.beta +com.shanga.walli +com.simplehabit.simplehabitapp com.slack +com.snaptube.premium +com.sololearn +com.sonelli.juicessh +com.sparkslab.dcardreader +com.spotify.music com.tencent.huatuo com.termux +com.teslacoilsw.launcher +com.theinitium.news +com.thomsonreuters.reuters com.thunkable.android.hritvik00.freenom com.topjohnwu.magisk +com.tripadvisor.tripadvisor +com.tumblr +com.twitter.android com.u91porn com.u9porn +com.ubisoft.dance.justdance2015companion +com.udn.news +com.utopia.pxview +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 +com.vkontakte.android +com.whatsapp +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 dk.tacit.android.foldersync.full es.rafalense.telegram.themes es.rafalense.themes flipboard.app +fm.moon.app +fr.gouv.etalab.mastodon github.tornaco.xposedmoduletest +idm.internet.download.manager +idm.internet.download.manager.plus +io.github.javiewer +io.github.skyhacker2.magnetsearch io.va.exposed +it.mvilla.android.fenix2 +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 +net.tsapps.appsales onion.fire org.fdroid.fdroid +org.freedownloadmanager.fdm +org.kustom.widget org.mozilla.fennec_aurora +org.mozilla.fenix +org.mozilla.fenix.nightly +org.mozilla.firefox +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 org.torproject.android +org.torproject.torbrowser_alpha +org.wikipedia org.xbmc.kodi pl.zdunex25.updater -videodownloader.downloadvideo.downloader -com.quora.android -com.lingodeer -org.wikipedia -com.ninegag.android.app -com.duolingo -com.patreon.android -com.valvesoftware.android.steam.communimunity -co.wanqu.android -jp.bokete.app.android -com.vkontakte.android -com.amazon.mshop.android.shopping -com.ubisoft.dance.justdance2015companion -com.gameloft.android.anmp.glofta8hm -com.gameloft.android.anmp.glofta9hm -com.binance.dev -com.asahi.tida.tablet -com.theinitium.news -com.driverbrowser -com.thomsonreuters.reuters -com.nytimes.cn -com.android.providers.downloads.ui -com.avmovie -bbc.mobile.news.ww -org.mozilla.focus -io.github.javiewer -com.sonelli.juicessh -con.medium.reader -com.microsoft.skydrive -com.valvesoftware.android.steam.community -com.nintendo.zara -org.torproject.torbrowser_alpha tv.twitch.android.app -com.shanga.walli -com.whatsapp -com.wire -com.simplehabit.simplehabitapp \ No newline at end of file +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 c88ca53c..4f8c3d7e 100644 --- a/V2rayNG/app/src/main/assets/v2ray_config.json +++ b/V2rayNG/app/src/main/assets/v2ray_config.json @@ -13,8 +13,8 @@ } }, "system": { - "statsInboundUplink": true, - "statsInboundDownlink": true + "statsOutboundUplink": true, + "statsOutboundDownlink": true } }, "inbounds": [{ @@ -54,7 +54,7 @@ "users": [ { "id": "a3482e88-686a-4a58-8126-99c9df64b7bf", - "alterId": 64, + "alterId": 0, "security": "auto", "level": 8 } @@ -81,7 +81,9 @@ }, { "protocol": "freedom", - "settings": {}, + "settings": { + "domainStrategy": "UseIP" + }, "tag": "direct" }, { @@ -95,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/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt new file mode 100644 index 00000000..bb4d20be --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerAffiliationInfo.kt @@ -0,0 +1,10 @@ +package com.v2ray.ang.dto + +data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) { + fun getTestDelayString(): String { + if (testDelayMillis == 0L) { + return "" + } + return testDelayMillis.toString() + "ms" + } +} 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 54% 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 4cf695f2..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,46 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package com.v2ray.ang.helper; - -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.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 + 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 66% 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 e20014de..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 android.support.v7.widget.helper.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 163f94de..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 android.support.v7.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 2d281d5f..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.java +++ /dev/null @@ -1,123 +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 android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.helper.ItemTouchHelper; - -/** - * 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, 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(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { - if (source.getItemViewType() != target.getItemViewType()) { - return false; - } - - // Notify the adapter of the move - mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition()); - return true; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { - // Notify the adapter of the dismissal - mAdapter.onItemDismiss(viewHolder.getAdapterPosition()); - } - - @Override - public void onChildDraw(Canvas c, RecyclerView recyclerView, 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(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - - 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/java/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt new file mode 100644 index 00000000..bdb7bf9a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/TaskerReceiver.kt @@ -0,0 +1,41 @@ +package com.v2ray.ang.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.text.TextUtils +import com.v2ray.ang.AppConfig +import com.v2ray.ang.service.V2RayServiceManager + +class TaskerReceiver : BroadcastReceiver() { + + /** + * 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).orEmpty() + + if (switch == null || TextUtils.isEmpty(guid)) { + return + } else if (switch) { + if (guid == AppConfig.TASKER_DEFAULT_GUID) { + V2RayServiceManager.startVServiceFromToggle(context) + } else { + V2RayServiceManager.startVService(context, guid) + } + } else { + V2RayServiceManager.stopVService(context) + } + } catch (e: Exception) { + android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e) + } + } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt new file mode 100644 index 00000000..99d9bda2 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/receiver/WidgetProvider.kt @@ -0,0 +1,100 @@ +package com.v2ray.ang.receiver + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Build +import android.widget.RemoteViews +import com.v2ray.ang.AppConfig +import com.v2ray.ang.R +import com.v2ray.ang.service.V2RayServiceManager + +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.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) + intent.action = AppConfig.BROADCAST_ACTION_WIDGET_CLICK + val pendingIntent = PendingIntent.getBroadcast( + context, + R.id.layout_switch, + intent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + 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.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.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) { + appWidgetManager.updateAppWidget(appWidgetId, remoteViews) + } + } + + /** + * 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.isRunning()) { + V2RayServiceManager.stopVService(context) + } else { + 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 + ) + } + + 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 + ) + } + } + } + } + } +} 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 cbb82425..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,69 +1,87 @@ package com.v2ray.ang.service -import android.annotation.TargetApi import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.drawable.Icon -import android.net.VpnService 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.extension.defaultDPreference import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.Utils -import org.jetbrains.anko.toast 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 = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "NG") - 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 -> { - val intent = VpnService.prepare(this) - if (intent == null) - if (!Utils.startVService(this)) { - toast(R.string.app_tile_first_use) - } + V2RayServiceManager.startVServiceFromToggle(this) } + Tile.STATE_ACTIVE -> { - Utils.stopVService(this) + V2RayServiceManager.stopVService(this) } } } @@ -71,26 +89,30 @@ 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) } } } } -} \ No newline at end of file +} 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/java/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt new file mode 100644 index 00000000..deeb43c3 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/FragmentAdapter.kt @@ -0,0 +1,17 @@ +package com.v2ray.ang.ui + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter + +class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List) : + FragmentStateAdapter(fragmentActivity) { + + override fun createFragment(position: Int): Fragment { + return mFragments[position] + } + + override fun getItemCount(): Int { + return mFragments.size + } +} 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 52% 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 f2e5eeef..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,27 +1,21 @@ package com.v2ray.ang.ui -import android.graphics.Color -import android.support.v7.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 kotlinx.android.synthetic.main.item_recycler_bypass_list.view.* -import org.jetbrains.anko.image -import org.jetbrains.anko.layoutInflater -import org.jetbrains.anko.textColor -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 private const val VIEW_TYPE_ITEM = 1 } - private var mActivity: BaseActivity = activity - val blacklist = if (blacklist == null) HashSet() else HashSet(blacklist) + val blacklist = if (blacklist == null) HashSet() else HashSet(blacklist) override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { if (holder is AppViewHolder) { @@ -38,15 +32,16 @@ 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 // .inflate(R.layout.item_recycler_bypass_list, parent, false)) - else -> AppViewHolder(ctx.layoutInflater - .inflate(R.layout.item_recycler_bypass_list, parent, false)) + else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false)) } } @@ -55,43 +50,39 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List, bl open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) - inner class AppViewHolder(itemView: View) : BaseViewHolder(itemView), - View.OnClickListener { + inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root), + View.OnClickListener { private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName) private lateinit var appInfo: AppInfo - val icon = itemView.icon!! - val name = itemView.name!! - val package_name = itemView.package_name!! - val checkBox = itemView.check_box!! - fun bind(appInfo: AppInfo) { this.appInfo = appInfo - icon.image = appInfo.appIcon -// name.text = appInfo.appName - - checkBox.isChecked = inBlacklist - package_name.text = appInfo.packageName - if (appInfo.isSystemApp) { - name.text = String.format("** %1s", appInfo.appName) - name.textColor = Color.RED + // Set app icon and name + itemBypassBinding.icon.setImageDrawable(appInfo.appIcon) + itemBypassBinding.name.text = if (appInfo.isSystemApp) { + String.format("** %s", appInfo.appName) } else { - name.text = appInfo.appName - name.textColor = 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) - checkBox.isChecked = false + itemBypassBinding.checkBox.isChecked = false } else { blacklist.add(appInfo.packageName) - checkBox.isChecked = true + itemBypassBinding.checkBox.isChecked = true } } } -} \ No newline at end of file +} 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/java/com/v2ray/ang/ui/ScSwitchActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt new file mode 100644 index 00000000..0495318a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScSwitchActivity.kt @@ -0,0 +1,21 @@ +package com.v2ray.ang.ui + +import android.os.Bundle +import com.v2ray.ang.R +import com.v2ray.ang.service.V2RayServiceManager + +class ScSwitchActivity : BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + moveTaskToBack(true) + + setContentView(R.layout.activity_none) + + if (V2RayServiceManager.isRunning()) { + V2RayServiceManager.stopVService(this) + } else { + 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/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt new file mode 100644 index 00000000..44e0b32b --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt @@ -0,0 +1,148 @@ +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.blacksquircle.ui.editorkit.utils.EditorTheme +import com.blacksquircle.ui.language.json.JsonLanguage +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.ProfileItem +import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess +import com.v2ray.ang.fmt.CustomFmt +import com.v2ray.ang.handler.MmkvManager +import com.v2ray.ang.util.Utils + +class ServerCustomConfigActivity : BaseActivity() { + private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) } + + private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() } + private val isRunning by lazy { + intent.getBooleanExtra("isRunning", false) + && editGuid.isNotEmpty() + && editGuid == MmkvManager.getSelectServer() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + 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) { + bindingServer(config) + } else { + clearServer() + } + } + + /** + * Binding selected server config + */ + private fun bindingServer(config: ProfileItem): Boolean { + binding.etRemarks.text = Utils.getEditable(config.remarks) + val raw = MmkvManager.decodeServerRaw(editGuid) + val configContent = raw.orEmpty() + + binding.editor.setTextContent(Utils.getEditable(configContent)) + return true + } + + /** + * clear or init server config + */ + private fun clearServer(): Boolean { + binding.etRemarks.text = null + return true + } + + /** + * save server config + */ + private fun saveServer(): Boolean { + if (TextUtils.isEmpty(binding.etRemarks.text.toString())) { + toast(R.string.server_lab_remarks) + return false + } + + val profileItem = try { + CustomFmt.parse(binding.editor.text.toString()) + } catch (e: Exception) { + 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) ?: 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) + MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) + toastSuccess(R.string.toast_success) + finish() + return true + } + + /** + * 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() + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + // do nothing + } + .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/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 68% 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 fac81876..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,43 +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 com.v2ray.ang.util.AngConfigManager -import android.content.Intent -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import com.google.zxing.WriterException import com.v2ray.ang.AppConfig -import kotlinx.android.synthetic.main.activity_tasker.* - +import com.v2ray.ang.R +import com.v2ray.ang.databinding.ActivityTaskerBinding +import com.v2ray.ang.handler.MmkvManager class TaskerActivity : BaseActivity() { + private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) } + private var listview: ListView? = null private var lstData: ArrayList = ArrayList() private var lstGuid: ArrayList = ArrayList() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_tasker) + setContentView(binding.root) //add def value lstData.add("Default") lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) - AngConfigManager.configs.vmess.forEach { - lstData.add(it.remarks) - lstGuid.add(it.guid) + 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() } @@ -51,14 +54,14 @@ class TaskerActivity : BaseActivity() { if (switch == null || TextUtils.isEmpty(guid)) { return } else { - switch_start_service.isChecked = switch + binding.switchStartService.isChecked = switch val pos = lstGuid.indexOf(guid.toString()) if (pos >= 0) { listview?.setItemChecked(pos, true) } } - } catch (e: WriterException) { - e.printStackTrace() + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e) } } @@ -70,28 +73,26 @@ class TaskerActivity : BaseActivity() { } val extraBundle = Bundle() - extraBundle.putBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, switch_start_service.isChecked) + extraBundle.putBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, binding.switchStartService.isChecked) extraBundle.putString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, lstGuid[position]) val intent = Intent() val remarks = lstData[position] - var blurb = "" - - if (switch_start_service.isChecked) { - blurb = "Start $remarks" + val blurb = if (binding.switchStartService.isChecked) { + "Start $remarks" } else { - blurb = "Stop $remarks" + "Stop $remarks" } 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() } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.action_server, menu) - val del_config = menu?.findItem(R.id.del_config) + val del_config = menu.findItem(R.id.del_config) del_config?.isVisible = false return super.onCreateOptionsMenu(menu) } @@ -100,10 +101,12 @@ 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/AssetsUtil.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java deleted file mode 100644 index 38f17d9c..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AssetsUtil.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.v2ray.ang.util; - -import static android.content.Context.MODE_PRIVATE; - -import android.content.Context; -import android.content.res.AssetManager; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; - -public class AssetsUtil { - public static boolean copyAssetFolder(AssetManager assetManager, - String fromAssetPath, String toPath) { - try { - String[] files = assetManager.list(fromAssetPath); - new File(toPath).mkdirs(); - boolean res = true; - for (String file : files) - if (file.contains(".")) - res &= copyAsset(assetManager, - fromAssetPath + "/" + file, - toPath + "/" + file); - else - res &= copyAssetFolder(assetManager, - fromAssetPath + "/" + file, - toPath + "/" + file); - return res; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - public static boolean copyAsset(AssetManager assetManager, - String fromAssetPath, String toPath) { - InputStream in = null; - OutputStream out = null; - try { - in = assetManager.open(fromAssetPath); - new File(toPath).createNewFile(); - out = new FileOutputStream(toPath); - copyFile(in, out); - in.close(); - return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } finally { - try { - if (out != null) { - out.close(); - } - if (in != null) { - in.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - public static String readTextFromAssets(AssetManager assetManager, String fileName) { - try { - InputStreamReader inputReader = new InputStreamReader(assetManager.open(fileName)); - BufferedReader bufReader = new BufferedReader(inputReader); - String line; - String Result = ""; - while ((line = bufReader.readLine()) != null) - Result += line; - return Result; - } catch (Exception e) { - e.printStackTrace(); - } - - return null; - } - - public static String getAssetPath(Context context, String assetPath) { - InputStream in = null; - OutputStream out = null; - try { - context.deleteFile(assetPath); - - in = context.getAssets().open(assetPath); - out = context.openFileOutput(assetPath, MODE_PRIVATE); - copyFile(in, out); - in.close(); - - String path = context.getFilesDir().toString(); - return path + "/" + assetPath; - - } catch (Exception e) { - e.printStackTrace(); - return ""; - } finally { - try { - if (out != null) { - out.close(); - } - if (in != null) { - in.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private static void copyFile(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[1024]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java deleted file mode 100644 index eb69a062..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64.java +++ /dev/null @@ -1,570 +0,0 @@ -// Portions copyright 2002, Google, Inc. -// -// 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.util; - -// This code was converted from code at http://iharder.sourceforge.net/base64/ -// Lots of extraneous features were removed. -/* The original code said: - *

- * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit - * http://iharder.net/xmlizable - * periodically to check for updates or to contribute improvements. - *

- * - * @author Robert Harder - * @author rharder@usa.net - * @version 1.3 - */ - -/** - * Base64 converter class. This code is not a complete MIME encoder; - * it simply converts binary data to base64 data and back. - * - *

Note {@link CharBase64} is a GWT-compatible implementation of this - * class. - */ -public class Base64 { - /** Specify encoding (value is {@code true}). */ - public final static boolean ENCODE = true; - - /** Specify decoding (value is {@code false}). */ - public final static boolean DECODE = false; - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte) '='; - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte) '\n'; - - /** - * The 64 valid Base64 values. - */ - private final static byte[] ALPHABET = - {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', - (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', - (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', - (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', - (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', - (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', - (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', - (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', - (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', - (byte) '9', (byte) '+', (byte) '/'}; - - /** - * The 64 valid web safe Base64 values. - */ - private final static byte[] WEBSAFE_ALPHABET = - {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', - (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', - (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', - (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', - (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', - (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', - (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', - (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', - (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', - (byte) '9', (byte) '-', (byte) '_'}; - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - /** The web safe decodabet */ - private final static byte[] WEBSAFE_DECODABET = - {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 - 62, // Dash '-' sign at decimal 45 - -9, -9, // Decimal 46-47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, // Decimal 91-94 - 63, // Underscore '_' at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - // Indicates white space in encoding - private final static byte WHITE_SPACE_ENC = -5; - // Indicates equals sign in encoding - private final static byte EQUALS_SIGN_ENC = -1; - - /** Defeats instantiation. */ - private Base64() { - } - - /* ******** E N C O D I N G M E T H O D S ******** */ - - /** - * Encodes up to three bytes of the array source - * and writes the resulting four Base64 bytes to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accommodate srcOffset + 3 for - * the source array or destOffset + 4 for - * the destination array. - * The actual number of significant bytes in your array is - * given by numSigBytes. - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param alphabet is the encoding alphabet - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4(byte[] source, int srcOffset, - int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index alphabet - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = - (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) - | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) - | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; - return destination; - case 2: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - case 1: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - default: - return destination; - } // end switch - } // end encode3to4 - - /** - * Encodes a byte array into Base64 notation. - * Equivalent to calling - * {@code encodeBytes(source, 0, source.length)} - * - * @param source The data to convert - * @since 1.4 - */ - public static String encode(byte[] source) { - return encode(source, 0, source.length, ALPHABET, true); - } - - /** - * Encodes a byte array into web safe Base64 notation. - * - * @param source The data to convert - * @param doPadding is {@code true} to pad result with '=' chars - * if it does not fall on 3 byte boundaries - */ - public static String encodeWebSafe(byte[] source, boolean doPadding) { - return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); - } - - /** - * Encodes a byte array into Base64 notation. - * - * @param source the data to convert - * @param off offset in array where conversion should begin - * @param len length of data to convert - * @param alphabet the encoding alphabet - * @param doPadding is {@code true} to pad result with '=' chars - * if it does not fall on 3 byte boundaries - * @since 1.4 - */ - public static String encode(byte[] source, int off, int len, byte[] alphabet, - boolean doPadding) { - byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); - int outLen = outBuff.length; - - // If doPadding is false, set length to truncate '=' - // padding characters - while (doPadding == false && outLen > 0) { - if (outBuff[outLen - 1] != '=') { - break; - } - outLen -= 1; - } - - return new String(outBuff, 0, outLen); - } - - /** - * Encodes a byte array into Base64 notation. - * - * @param source the data to convert - * @param off offset in array where conversion should begin - * @param len length of data to convert - * @param alphabet is the encoding alphabet - * @param maxLineLength maximum length of one line. - * @return the BASE64-encoded byte array - */ - public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, - int maxLineLength) { - int lenDiv3 = (len + 2) / 3; // ceil(len / 3) - int len43 = lenDiv3 * 4; - byte[] outBuff = new byte[len43 // Main 4:3 - + (len43 / maxLineLength)]; // New lines - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - - // The following block of code is the same as - // encode3to4( source, d + off, 3, outBuff, e, alphabet ); - // but inlined for faster encoding (~20% improvement) - int inBuff = - ((source[d + off] << 24) >>> 8) - | ((source[d + 1 + off] << 24) >>> 16) - | ((source[d + 2 + off] << 24) >>> 24); - outBuff[e] = alphabet[(inBuff >>> 18)]; - outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; - - lineLength += 4; - if (lineLength == maxLineLength) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // end for: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, alphabet); - - lineLength += 4; - if (lineLength == maxLineLength) { - // Add a last newline - outBuff[e + 4] = NEW_LINE; - e++; - } - e += 4; - } - - assert (e == outBuff.length); - return outBuff; - } - - - /* ******** D E C O D I N G M E T H O D S ******** */ - - - /** - * Decodes four bytes from array source - * and writes the resulting bytes (up to three of them) - * to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accommodate srcOffset + 4 for - * the source array or destOffset + 3 for - * the destination array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param decodabet the decodabet for decoding Base64 content - * @return the number of decoded bytes converted - * @since 1.3 - */ - private static int decode4to3(byte[] source, int srcOffset, - byte[] destination, int destOffset, byte[] decodabet) { - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); - - destination[destOffset] = (byte) (outBuff >>> 16); - return 1; - } else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Example: DkL= - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) - | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); - - destination[destOffset] = (byte) (outBuff >>> 16); - destination[destOffset + 1] = (byte) (outBuff >>> 8); - return 2; - } else { - // Example: DkLE - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) - | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) - | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); - - destination[destOffset] = (byte) (outBuff >> 16); - destination[destOffset + 1] = (byte) (outBuff >> 8); - destination[destOffset + 2] = (byte) (outBuff); - return 3; - } - } // end decodeToBytes - - - /** - * Decodes data from Base64 notation. - * - * @param s the string to decode (decoded in default encoding) - * @return the decoded data - * @since 1.4 - */ - public static byte[] decode(String s) throws Base64DecoderException { - byte[] bytes = s.getBytes(); - return decode(bytes, 0, bytes.length); - } - - /** - * Decodes data from web safe Base64 notation. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param s the string to decode (decoded in default encoding) - * @return the decoded data - */ - public static byte[] decodeWebSafe(String s) throws Base64DecoderException { - byte[] bytes = s.getBytes(); - return decodeWebSafe(bytes, 0, bytes.length); - } - - /** - * Decodes Base64 content in byte array format and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @return decoded data - * @since 1.3 - * @throws Base64DecoderException - */ - public static byte[] decode(byte[] source) throws Base64DecoderException { - return decode(source, 0, source.length); - } - - /** - * Decodes web safe Base64 content in byte array format and returns - * the decoded data. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param source the string to decode (decoded in default encoding) - * @return the decoded data - */ - public static byte[] decodeWebSafe(byte[] source) - throws Base64DecoderException { - return decodeWebSafe(source, 0, source.length); - } - - /** - * Decodes Base64 content in byte array format and returns - * the decoded byte array. - * - * @param source the Base64 encoded data - * @param off the offset of where to begin decoding - * @param len the length of characters to decode - * @return decoded data - * @since 1.3 - * @throws Base64DecoderException - */ - public static byte[] decode(byte[] source, int off, int len) - throws Base64DecoderException { - return decode(source, off, len, DECODABET); - } - - /** - * Decodes web safe Base64 content in byte array format and returns - * the decoded byte array. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param source the Base64 encoded data - * @param off the offset of where to begin decoding - * @param len the length of characters to decode - * @return decoded data - */ - public static byte[] decodeWebSafe(byte[] source, int off, int len) - throws Base64DecoderException { - return decode(source, off, len, WEBSAFE_DECODABET); - } - - /** - * Decodes Base64 content using the supplied decodabet and returns - * the decoded byte array. - * - * @param source the Base64 encoded data - * @param off the offset of where to begin decoding - * @param len the length of characters to decode - * @param decodabet the decodabet for decoding Base64 content - * @return decoded data - */ - public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) - throws Base64DecoderException { - int len34 = len * 3 / 4; - byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output - int outBuffPosn = 0; - - byte[] b4 = new byte[4]; - int b4Posn = 0; - int i = 0; - byte sbiCrop = 0; - byte sbiDecode = 0; - for (i = 0; i < len; i++) { - sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits - sbiDecode = decodabet[sbiCrop]; - - if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better - if (sbiDecode >= EQUALS_SIGN_ENC) { - // An equals sign (for padding) must not occur at position 0 or 1 - // and must be the last byte[s] in the encoded value - if (sbiCrop == EQUALS_SIGN) { - int bytesLeft = len - i; - byte lastByte = (byte) (source[len - 1 + off] & 0x7f); - if (b4Posn == 0 || b4Posn == 1) { - throw new Base64DecoderException( - "invalid padding byte '=' at byte offset " + i); - } else if ((b4Posn == 3 && bytesLeft > 2) - || (b4Posn == 4 && bytesLeft > 1)) { - throw new Base64DecoderException( - "padding byte '=' falsely signals end of encoded value " - + "at offset " + i); - } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { - throw new Base64DecoderException( - "encoded value has invalid trailing byte"); - } - break; - } - - b4[b4Posn++] = sbiCrop; - if (b4Posn == 4) { - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); - b4Posn = 0; - } - } - } else { - throw new Base64DecoderException("Bad Base64 input character at " + i - + ": " + source[i + off] + "(decimal)"); - } - } - - // Because web safe encoding allows non padding base64 encodes, we - // need to pad the rest of the b4 buffer with equal signs when - // b4Posn != 0. There can be at most 2 equal signs at the end of - // four characters, so the b4 buffer must have two or three - // characters. This also catches the case where the input is - // padded with EQUALS_SIGN - if (b4Posn != 0) { - if (b4Posn == 1) { - throw new Base64DecoderException("single trailing character at offset " - + (len - 1)); - } - b4[b4Posn++] = EQUALS_SIGN; - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); - } - - byte[] out = new byte[outBuffPosn]; - System.arraycopy(outBuff, 0, out, 0, outBuffPosn); - return out; - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java deleted file mode 100644 index b113e43f..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Base64DecoderException.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2002, Google, Inc. -// -// 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.util; - -/** - * Exception thrown when encountering an invalid Base64 input character. - * - * @author nelson - */ -public class Base64DecoderException extends Exception { - public Base64DecoderException() { - super(); - } - - public Base64DecoderException(String s) { - super(s); - } - - private static final long serialVersionUID = 1L; -} 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/IabException.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java deleted file mode 100644 index e6320808..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabException.java +++ /dev/null @@ -1,43 +0,0 @@ -/* Copyright (c) 2012 Google Inc. - * - * 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.util; - -/** - * Exception thrown when something went wrong with in-app billing. - * An IabException has an associated IabResult (an error). - * To get the IAB result that caused this exception to be thrown, - * call {@link #getResult()}. - */ -public class IabException extends Exception { - IabResult mResult; - - public IabException(IabResult r) { - this(r, null); - } - public IabException(int response, String message) { - this(new IabResult(response, message)); - } - public IabException(IabResult r, Exception cause) { - super(r.getMessage(), cause); - mResult = r; - } - public IabException(int response, String message, Exception cause) { - this(new IabResult(response, message), cause); - } - - /** Returns the IAB result (error) that this exception signals. */ - public IabResult getResult() { return mResult; } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java deleted file mode 100644 index 911d20da..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabHelper.java +++ /dev/null @@ -1,979 +0,0 @@ -/* Copyright (c) 2012 Google Inc. - * - * 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.util; - -import android.app.Activity; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentSender.SendIntentException; -import android.content.ServiceConnection; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.RemoteException; -import android.text.TextUtils; -import android.util.Log; - -import com.android.vending.billing.IInAppBillingService; - -import org.json.JSONException; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Provides convenience methods for in-app billing. You can create one instance of this - * class for your application and use it to process in-app billing operations. - * It provides synchronous (blocking) and asynchronous (non-blocking) methods for - * many common in-app billing operations, as well as automatic signature - * verification. - *

- * After instantiating, you must perform setup in order to start using the object. - * To perform setup, call the {@link #startSetup} method and provide a listener; - * that listener will be notified when setup is complete, after which (and not before) - * you may call other methods. - *

- * After setup is complete, you will typically want to request an inventory of owned - * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync} - * and related methods. - *

- * When you are done with this object, don't forget to call {@link #dispose} - * to ensure proper cleanup. This object holds a binding to the in-app billing - * service, which will leak unless you dispose of it correctly. If you created - * the object on an Activity's onCreate method, then the recommended - * place to dispose of it is the Activity's onDestroy method. - *

- * A note about threading: When using this object from a background thread, you may - * call the blocking versions of methods; when using from a UI thread, call - * only the asynchronous versions and handle the results via callbacks. - * Also, notice that you can only call one asynchronous operation at a time; - * attempting to start a second asynchronous operation while the first one - * has not yet completed will result in an exception being thrown. - * - * @author Bruno Oliveira (Google) - */ -public class IabHelper { - // Is debug logging enabled? - boolean mDebugLog = false; - String mDebugTag = "IabHelper"; - - // Is setup done? - boolean mSetupDone = false; - - // Has this object been disposed of? (If so, we should ignore callbacks, etc) - boolean mDisposed = false; - - // Are subscriptions supported? - boolean mSubscriptionsSupported = false; - - // Is an asynchronous operation in progress? - // (only one at a time can be in progress) - boolean mAsyncInProgress = false; - - // (for logging/debugging) - // if mAsyncInProgress == true, what asynchronous operation is in progress? - String mAsyncOperation = ""; - - // Context we were passed during initialization - Context mContext; - - // Connection to the service - IInAppBillingService mService; - ServiceConnection mServiceConn; - - // The request code used to launch purchase flow - int mRequestCode; - - // The item type of the current purchase flow - String mPurchasingItemType; - - // Public key for verifying signature, in base64 encoding - String mSignatureBase64 = null; - - // Billing response codes - public static final int BILLING_RESPONSE_RESULT_OK = 0; - public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1; - public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; - public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4; - public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5; - public static final int BILLING_RESPONSE_RESULT_ERROR = 6; - public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7; - public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8; - - // IAB Helper error codes - public static final int IABHELPER_ERROR_BASE = -1000; - public static final int IABHELPER_REMOTE_EXCEPTION = -1001; - public static final int IABHELPER_BAD_RESPONSE = -1002; - public static final int IABHELPER_VERIFICATION_FAILED = -1003; - public static final int IABHELPER_SEND_INTENT_FAILED = -1004; - public static final int IABHELPER_USER_CANCELLED = -1005; - public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; - public static final int IABHELPER_MISSING_TOKEN = -1007; - public static final int IABHELPER_UNKNOWN_ERROR = -1008; - public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009; - public static final int IABHELPER_INVALID_CONSUMPTION = -1010; - - // Keys for the responses from InAppBillingService - public static final String RESPONSE_CODE = "RESPONSE_CODE"; - public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; - public static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; - public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; - public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; - public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; - public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; - public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; - public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; - - // Item types - public static final String ITEM_TYPE_INAPP = "inapp"; - public static final String ITEM_TYPE_SUBS = "subs"; - - // some fields on the getSkuDetails response bundle - public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; - public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; - - /** - * Creates an instance. After creation, it will not yet be ready to use. You must perform - * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not - * block and is safe to call from a UI thread. - * - * @param ctx Your application or Activity context. Needed to bind to the in-app billing service. - * @param base64PublicKey Your application's public key, encoded in base64. - * This is used for verification of purchase signatures. You can find your app's base64-encoded - * public key in your application's page on Google Play Developer Console. Note that this - * is NOT your "developer public key". - */ - public IabHelper(Context ctx, String base64PublicKey) { - mContext = ctx.getApplicationContext(); - mSignatureBase64 = base64PublicKey; - logDebug("IAB helper created."); - } - - /** - * Enables or disable debug logging through LogCat. - */ - public void enableDebugLogging(boolean enable, String tag) { - checkNotDisposed(); - mDebugLog = enable; - mDebugTag = tag; - } - - public void enableDebugLogging(boolean enable) { - checkNotDisposed(); - mDebugLog = enable; - } - - /** - * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called - * when the setup process is complete. - */ - public interface OnIabSetupFinishedListener { - /** - * Called to notify that setup is complete. - * - * @param result The result of the setup process. - */ - void onIabSetupFinished(IabResult result); - } - - /** - * Starts the setup process. This will start up the setup process asynchronously. - * You will be notified through the listener when the setup process is complete. - * This method is safe to call from a UI thread. - * - * @param listener The listener to notify when the setup process is complete. - */ - public void startSetup(final OnIabSetupFinishedListener listener) { - // If already set up, can't do it again. - checkNotDisposed(); - if (mSetupDone) throw new IllegalStateException("IAB helper is already set up."); - - // Connection to IAB service - logDebug("Starting in-app billing setup."); - mServiceConn = new ServiceConnection() { - @Override - public void onServiceDisconnected(ComponentName name) { - logDebug("Billing service disconnected."); - mService = null; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - if (mDisposed) return; - logDebug("Billing service connected."); - mService = IInAppBillingService.Stub.asInterface(service); - String packageName = mContext.getPackageName(); - try { - logDebug("Checking for in-app billing 3 support."); - - // check for in-app billing v3 support - int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP); - if (response != BILLING_RESPONSE_RESULT_OK) { - if (listener != null) listener.onIabSetupFinished(new IabResult(response, - "Error checking for billing v3 support.")); - - // if in-app purchases aren't supported, neither are subscriptions. - mSubscriptionsSupported = false; - return; - } - logDebug("In-app billing version 3 supported for " + packageName); - - // check for v3 subscriptions support - response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS); - if (response == BILLING_RESPONSE_RESULT_OK) { - logDebug("Subscriptions AVAILABLE."); - mSubscriptionsSupported = true; - } else { - logDebug("Subscriptions NOT AVAILABLE. Response: " + response); - } - - mSetupDone = true; - } catch (RemoteException e) { - if (listener != null) { - listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, - "RemoteException while setting up in-app billing.")); - } - e.printStackTrace(); - return; - } - - if (listener != null) { - listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful.")); - } - } - }; - -// Intent serviceIntent = new Intent("ir.cafebazaar.pardakht.InAppBillingService.BIND"); -// serviceIntent.setPackage("com.farsitel.bazaar"); - Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); - serviceIntent.setPackage("com.android.vending"); - if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) { - // service available to handle that Intent - mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); - } else { - // no service available to handle that Intent - if (listener != null) { - listener.onIabSetupFinished( - new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, - "Billing service unavailable on device.")); - } - } - } - - /** - * Dispose of object, releasing resources. It's very important to call this - * method when you are done with this object. It will release any resources - * used by it such as service connections. Naturally, once the object is - * disposed of, it can't be used again. - */ - public void dispose() { - logDebug("Disposing."); - mSetupDone = false; - if (mServiceConn != null) { - logDebug("Unbinding from service."); - if (mContext != null) mContext.unbindService(mServiceConn); - } - mDisposed = true; - mContext = null; - mServiceConn = null; - mService = null; - mPurchaseListener = null; - } - - private void checkNotDisposed() { - if (mDisposed) - throw new IllegalStateException("IabHelper was disposed of, so it cannot be used."); - } - - /** - * Returns whether subscriptions are supported. - */ - public boolean subscriptionsSupported() { - checkNotDisposed(); - return mSubscriptionsSupported; - } - - - /** - * Callback that notifies when a purchase is finished. - */ - public interface OnIabPurchaseFinishedListener { - /** - * Called to notify that an in-app purchase finished. If the purchase was successful, - * then the sku parameter specifies which item was purchased. If the purchase failed, - * the sku and extraData parameters may or may not be null, depending on how far the purchase - * process went. - * - * @param result The result of the purchase. - * @param info The purchase information (null if purchase failed) - */ - void onIabPurchaseFinished(IabResult result, Purchase info); - } - - // The listener registered on launchPurchaseFlow, which we have to call back when - // the purchase finishes - OnIabPurchaseFinishedListener mPurchaseListener; - - public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) { - launchPurchaseFlow(act, sku, requestCode, listener, ""); - } - - public void launchPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener, String extraData) { - launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); - } - - public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener) { - launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); - } - - public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener, String extraData) { - launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); - } - - /** - * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase, - * which will involve bringing up the Google Play screen. The calling activity will be paused while - * the user interacts with Google Play, and the result will be delivered via the activity's - * {@link android.app.Activity#onActivityResult} method, at which point you must call - * this object's {@link #handleActivityResult} method to continue the purchase flow. This method - * MUST be called from the UI thread of the Activity. - * - * @param act The calling activity. - * @param sku The sku of the item to purchase. - * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS) - * @param requestCode A request code (to differentiate from other responses -- - * as in {@link android.app.Activity#startActivityForResult}). - * @param listener The listener to notify when the purchase process finishes - * @param extraData Extra data (developer payload), which will be returned with the purchase data - * when the purchase completes. This extra data will be permanently bound to that purchase - * and will always be returned when the purchase is queried. - */ - public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode, - OnIabPurchaseFinishedListener listener, String extraData) { - checkNotDisposed(); - checkSetupDone("launchPurchaseFlow"); - flagStartAsync("launchPurchaseFlow"); - IabResult result; - - if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) { - IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, - "Subscriptions are not available."); - flagEndAsync(); - if (listener != null) listener.onIabPurchaseFinished(r, null); - return; - } - - try { - logDebug("Constructing buy intent for " + sku + ", item type: " + itemType); - Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData); - int response = getResponseCodeFromBundle(buyIntentBundle); - if (response != BILLING_RESPONSE_RESULT_OK) { - logError("Unable to buy item, Error response: " + getResponseDesc(response)); - flagEndAsync(); - result = new IabResult(response, "Unable to buy item"); - if (listener != null) listener.onIabPurchaseFinished(result, null); - return; - } - - PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); - logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); - mRequestCode = requestCode; - mPurchaseListener = listener; - mPurchasingItemType = itemType; - act.startIntentSenderForResult(pendingIntent.getIntentSender(), - requestCode, new Intent(), - Integer.valueOf(0), Integer.valueOf(0), - Integer.valueOf(0)); - } catch (SendIntentException e) { - logError("SendIntentException while launching purchase flow for sku " + sku); - e.printStackTrace(); - flagEndAsync(); - - result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent."); - if (listener != null) listener.onIabPurchaseFinished(result, null); - } catch (RemoteException e) { - logError("RemoteException while launching purchase flow for sku " + sku); - e.printStackTrace(); - flagEndAsync(); - - result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow"); - if (listener != null) listener.onIabPurchaseFinished(result, null); - } - } - - /** - * Handles an activity result that's part of the purchase flow in in-app billing. If you - * are calling {@link #launchPurchaseFlow}, then you must call this method from your - * Activity's {@link android.app.Activity@onActivityResult} method. This method - * MUST be called from the UI thread of the Activity. - * - * @param requestCode The requestCode as you received it. - * @param resultCode The resultCode as you received it. - * @param data The data (Intent) as you received it. - * @return Returns true if the result was related to a purchase flow and was handled; - * false if the result was not related to a purchase, in which case you should - * handle it normally. - */ - public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { - IabResult result; - if (requestCode != mRequestCode) return false; - - checkNotDisposed(); - checkSetupDone("handleActivityResult"); - - // end of async purchase operation that started on launchPurchaseFlow - flagEndAsync(); - - if (data == null) { - logError("Null data in IAB activity result."); - result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result"); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); - return true; - } - - int responseCode = getResponseCodeFromIntent(data); - String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); - String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); - - if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) { - logDebug("Successful resultcode from purchase activity."); - logDebug("Purchase data: " + purchaseData); - logDebug("Data signature: " + dataSignature); - logDebug("Extras: " + data.getExtras()); - logDebug("Expected item type: " + mPurchasingItemType); - - if (purchaseData == null || dataSignature == null) { - logError("BUG: either purchaseData or dataSignature is null."); - logDebug("Extras: " + data.getExtras().toString()); - result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature"); - if (mPurchaseListener != null) - mPurchaseListener.onIabPurchaseFinished(result, null); - return true; - } - - Purchase purchase = null; - try { - purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature); - String sku = purchase.getSku(); - - // Verify signature - if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { - logError("Purchase signature verification FAILED for sku " + sku); - result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku); - if (mPurchaseListener != null) - mPurchaseListener.onIabPurchaseFinished(result, purchase); - return true; - } - logDebug("Purchase signature successfully verified."); - } catch (JSONException e) { - logError("Failed to parse purchase data."); - e.printStackTrace(); - result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data."); - if (mPurchaseListener != null) - mPurchaseListener.onIabPurchaseFinished(result, null); - return true; - } - - if (mPurchaseListener != null) { - mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase); - } - } else if (resultCode == Activity.RESULT_OK) { - // result code was OK, but in-app billing response was not OK. - logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode)); - if (mPurchaseListener != null) { - result = new IabResult(responseCode, "Problem purchashing item."); - mPurchaseListener.onIabPurchaseFinished(result, null); - } - } else if (resultCode == Activity.RESULT_CANCELED) { - logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode)); - result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled."); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); - } else { - logError("Purchase failed. Result code: " + Integer.toString(resultCode) - + ". Response: " + getResponseDesc(responseCode)); - result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response."); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); - } - return true; - } - - public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException { - return queryInventory(querySkuDetails, moreSkus, null); - } - - /** - * Queries the inventory. This will query all owned items from the server, as well as - * information on additional skus, if specified. This method may block or take long to execute. - * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}. - * - * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well - * as purchase information. - * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership. - * Ignored if null or if querySkuDetails is false. - * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership. - * Ignored if null or if querySkuDetails is false. - * @throws IabException if a problem occurs while refreshing the inventory. - */ - public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus, - List moreSubsSkus) throws IabException { - checkNotDisposed(); - checkSetupDone("queryInventory"); - try { - Inventory inv = new Inventory(); - int r = queryPurchases(inv, ITEM_TYPE_INAPP); - if (r != BILLING_RESPONSE_RESULT_OK) { - throw new IabException(r, "Error refreshing inventory (querying owned items)."); - } - - if (querySkuDetails) { - r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus); - if (r != BILLING_RESPONSE_RESULT_OK) { - throw new IabException(r, "Error refreshing inventory (querying prices of items)."); - } - } - - // if subscriptions are supported, then also query for subscriptions - if (mSubscriptionsSupported) { - r = queryPurchases(inv, ITEM_TYPE_SUBS); - if (r != BILLING_RESPONSE_RESULT_OK) { - throw new IabException(r, "Error refreshing inventory (querying owned subscriptions)."); - } - - if (querySkuDetails) { - r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus); - if (r != BILLING_RESPONSE_RESULT_OK) { - throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions)."); - } - } - } - - return inv; - } catch (RemoteException e) { - throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e); - } catch (JSONException e) { - throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e); - } - } - - /** - * Listener that notifies when an inventory query operation completes. - */ - public interface QueryInventoryFinishedListener { - /** - * Called to notify that an inventory query operation completed. - * - * @param result The result of the operation. - * @param inv The inventory. - */ - void onQueryInventoryFinished(IabResult result, Inventory inv); - } - - - /** - * Asynchronous wrapper for inventory query. This will perform an inventory - * query as described in {@link #queryInventory}, but will do so asynchronously - * and call back the specified listener upon completion. This method is safe to - * call from a UI thread. - * - * @param querySkuDetails as in {@link #queryInventory} - * @param moreSkus as in {@link #queryInventory} - * @param listener The listener to notify when the refresh operation completes. - */ - public void queryInventoryAsync(final boolean querySkuDetails, - final List moreSkus, - final QueryInventoryFinishedListener listener) { - final Handler handler = new Handler(); - checkNotDisposed(); - checkSetupDone("queryInventory"); - flagStartAsync("refresh inventory"); - (new Thread(new Runnable() { - public void run() { - IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful."); - Inventory inv = null; - try { - inv = queryInventory(querySkuDetails, moreSkus); - } catch (IabException ex) { - result = ex.getResult(); - } - - flagEndAsync(); - - final IabResult result_f = result; - final Inventory inv_f = inv; - if (!mDisposed && listener != null) { - handler.post(new Runnable() { - public void run() { - listener.onQueryInventoryFinished(result_f, inv_f); - } - }); - } - } - })).start(); - } - - public void queryInventoryAsync(QueryInventoryFinishedListener listener) { - queryInventoryAsync(true, null, listener); - } - - public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) { - queryInventoryAsync(querySkuDetails, null, listener); - } - - - /** - * Consumes a given in-app product. Consuming can only be done on an item - * that's owned, and as a result of consumption, the user will no longer own it. - * This method may block or take long to return. Do not call from the UI thread. - * For that, see {@link #consumeAsync}. - * - * @param itemInfo The PurchaseInfo that represents the item to consume. - * @throws IabException if there is a problem during consumption. - */ - void consume(Purchase itemInfo) throws IabException { - checkNotDisposed(); - checkSetupDone("consume"); - - if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) { - throw new IabException(IABHELPER_INVALID_CONSUMPTION, - "Items of type '" + itemInfo.mItemType + "' can't be consumed."); - } - - try { - String token = itemInfo.getToken(); - String sku = itemInfo.getSku(); - if (token == null || token.equals("")) { - logError("Can't consume " + sku + ". No token."); - throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: " - + sku + " " + itemInfo); - } - - logDebug("Consuming sku: " + sku + ", token: " + token); - int response = mService.consumePurchase(3, mContext.getPackageName(), token); - if (response == BILLING_RESPONSE_RESULT_OK) { - logDebug("Successfully consumed sku: " + sku); - } else { - logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response)); - throw new IabException(response, "Error consuming sku " + sku); - } - } catch (RemoteException e) { - throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e); - } - } - - /** - * Callback that notifies when a consumption operation finishes. - */ - public interface OnConsumeFinishedListener { - /** - * Called to notify that a consumption has finished. - * - * @param purchase The purchase that was (or was to be) consumed. - * @param result The result of the consumption operation. - */ - void onConsumeFinished(Purchase purchase, IabResult result); - } - - /** - * Callback that notifies when a multi-item consumption operation finishes. - */ - public interface OnConsumeMultiFinishedListener { - /** - * Called to notify that a consumption of multiple items has finished. - * - * @param purchases The purchases that were (or were to be) consumed. - * @param results The results of each consumption operation, corresponding to each - * sku. - */ - void onConsumeMultiFinished(List purchases, List results); - } - - /** - * Asynchronous wrapper to item consumption. Works like {@link #consume}, but - * performs the consumption in the background and notifies completion through - * the provided listener. This method is safe to call from a UI thread. - * - * @param purchase The purchase to be consumed. - * @param listener The listener to notify when the consumption operation finishes. - */ - public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) { - checkNotDisposed(); - checkSetupDone("consume"); - List purchases = new ArrayList(); - purchases.add(purchase); - consumeAsyncInternal(purchases, listener, null); - } - - /** - * Same as {@link consumeAsync}, but for multiple items at once. - * - * @param purchases The list of PurchaseInfo objects representing the purchases to consume. - * @param listener The listener to notify when the consumption operation finishes. - */ - public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) { - checkNotDisposed(); - checkSetupDone("consume"); - consumeAsyncInternal(purchases, null, listener); - } - - /** - * Returns a human-readable description for the given response code. - * - * @param code The response code - * @return A human-readable string explaining the result code. - * It also includes the result code numerically. - */ - public static String getResponseDesc(int code) { - String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + - "3:Billing Unavailable/4:Item unavailable/" + - "5:Developer Error/6:Error/7:Item Already Owned/" + - "8:Item not owned").split("/"); - String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + - "-1002:Bad response received/" + - "-1003:Purchase signature verification failed/" + - "-1004:Send intent failed/" + - "-1005:User cancelled/" + - "-1006:Unknown purchase response/" + - "-1007:Missing token/" + - "-1008:Unknown error/" + - "-1009:Subscriptions not available/" + - "-1010:Invalid consumption attempt").split("/"); - - if (code <= IABHELPER_ERROR_BASE) { - int index = IABHELPER_ERROR_BASE - code; - if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index]; - else return String.valueOf(code) + ":Unknown IAB Helper Error"; - } else if (code < 0 || code >= iab_msgs.length) - return String.valueOf(code) + ":Unknown"; - else - return iab_msgs[code]; - } - - - // Checks that setup was done; if not, throws an exception. - void checkSetupDone(String operation) { - if (!mSetupDone) { - logError("Illegal state for operation (" + operation + "): IAB helper is not set up."); - throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation); - } - } - - // Workaround to bug where sometimes response codes come as Long instead of Integer - int getResponseCodeFromBundle(Bundle b) { - Object o = b.get(RESPONSE_CODE); - if (o == null) { - logDebug("Bundle with null response code, assuming OK (known issue)"); - return BILLING_RESPONSE_RESULT_OK; - } else if (o instanceof Integer) return ((Integer) o).intValue(); - else if (o instanceof Long) return (int) ((Long) o).longValue(); - else { - logError("Unexpected type for bundle response code."); - logError(o.getClass().getName()); - throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); - } - } - - // Workaround to bug where sometimes response codes come as Long instead of Integer - int getResponseCodeFromIntent(Intent i) { - Object o = i.getExtras().get(RESPONSE_CODE); - if (o == null) { - logError("Intent with no response code, assuming OK (known issue)"); - return BILLING_RESPONSE_RESULT_OK; - } else if (o instanceof Integer) return ((Integer) o).intValue(); - else if (o instanceof Long) return (int) ((Long) o).longValue(); - else { - logError("Unexpected type for intent response code."); - logError(o.getClass().getName()); - throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName()); - } - } - - void flagStartAsync(String operation) { - if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" + - operation + ") because another async operation(" + mAsyncOperation + ") is in progress."); - mAsyncOperation = operation; - mAsyncInProgress = true; - logDebug("Starting async operation: " + operation); - } - - void flagEndAsync() { - logDebug("Ending async operation: " + mAsyncOperation); - mAsyncOperation = ""; - mAsyncInProgress = false; - } - - - int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException { - // Query purchases - logDebug("Querying owned items, item type: " + itemType); - logDebug("Package name: " + mContext.getPackageName()); - boolean verificationFailed = false; - String continueToken = null; - - do { - logDebug("Calling getPurchases with continuation token: " + continueToken); - Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), - itemType, continueToken); - - int response = getResponseCodeFromBundle(ownedItems); - logDebug("Owned items response: " + String.valueOf(response)); - if (response != BILLING_RESPONSE_RESULT_OK) { - logDebug("getPurchases() failed: " + getResponseDesc(response)); - return response; - } - if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) - || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) - || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) { - logError("Bundle returned from getPurchases() doesn't contain required fields."); - return IABHELPER_BAD_RESPONSE; - } - - ArrayList ownedSkus = ownedItems.getStringArrayList( - RESPONSE_INAPP_ITEM_LIST); - ArrayList purchaseDataList = ownedItems.getStringArrayList( - RESPONSE_INAPP_PURCHASE_DATA_LIST); - ArrayList signatureList = ownedItems.getStringArrayList( - RESPONSE_INAPP_SIGNATURE_LIST); - - for (int i = 0; i < purchaseDataList.size(); ++i) { - String purchaseData = purchaseDataList.get(i); - String signature = signatureList.get(i); - String sku = ownedSkus.get(i); - if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) { - logDebug("Sku is owned: " + sku); - Purchase purchase = new Purchase(itemType, purchaseData, signature); - - if (TextUtils.isEmpty(purchase.getToken())) { - logWarn("BUG: empty/null token!"); - logDebug("Purchase data: " + purchaseData); - } - - // Record ownership and token - inv.addPurchase(purchase); - } else { - logWarn("Purchase signature verification **FAILED**. Not adding item."); - logDebug(" Purchase data: " + purchaseData); - logDebug(" Signature: " + signature); - verificationFailed = true; - } - } - - continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); - logDebug("Continuation token: " + continueToken); - } while (!TextUtils.isEmpty(continueToken)); - - return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK; - } - - int querySkuDetails(String itemType, Inventory inv, List moreSkus) - throws RemoteException, JSONException { - logDebug("Querying SKU details."); - ArrayList skuList = new ArrayList(); - skuList.addAll(inv.getAllOwnedSkus(itemType)); - if (moreSkus != null) { - for (String sku : moreSkus) { - if (!skuList.contains(sku)) { - skuList.add(sku); - } - } - } - - if (skuList.size() == 0) { - logDebug("queryPrices: nothing to do because there are no SKUs."); - return BILLING_RESPONSE_RESULT_OK; - } - - Bundle querySkus = new Bundle(); - querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList); - Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), - itemType, querySkus); - - if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { - int response = getResponseCodeFromBundle(skuDetails); - if (response != BILLING_RESPONSE_RESULT_OK) { - logDebug("getSkuDetails() failed: " + getResponseDesc(response)); - return response; - } else { - logError("getSkuDetails() returned a bundle with neither an error nor a detail list."); - return IABHELPER_BAD_RESPONSE; - } - } - - ArrayList responseList = skuDetails.getStringArrayList( - RESPONSE_GET_SKU_DETAILS_LIST); - - for (String thisResponse : responseList) { - SkuDetails d = new SkuDetails(itemType, thisResponse); - logDebug("Got sku details: " + d); - inv.addSkuDetails(d); - } - return BILLING_RESPONSE_RESULT_OK; - } - - - void consumeAsyncInternal(final List purchases, - final OnConsumeFinishedListener singleListener, - final OnConsumeMultiFinishedListener multiListener) { - final Handler handler = new Handler(); - flagStartAsync("consume"); - (new Thread(new Runnable() { - public void run() { - final List results = new ArrayList(); - for (Purchase purchase : purchases) { - try { - consume(purchase); - results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku())); - } catch (IabException ex) { - results.add(ex.getResult()); - } - } - - flagEndAsync(); - if (!mDisposed && singleListener != null) { - handler.post(new Runnable() { - public void run() { - singleListener.onConsumeFinished(purchases.get(0), results.get(0)); - } - }); - } - if (!mDisposed && multiListener != null) { - handler.post(new Runnable() { - public void run() { - multiListener.onConsumeMultiFinished(purchases, results); - } - }); - } - } - })).start(); - } - - void logDebug(String msg) { - if (mDebugLog) Log.d(mDebugTag, msg); - } - - void logError(String msg) { - Log.e(mDebugTag, "In-app billing error: " + msg); - } - - void logWarn(String msg) { - Log.w(mDebugTag, "In-app billing warning: " + msg); - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java deleted file mode 100644 index 0fbe5b58..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/IabResult.java +++ /dev/null @@ -1,45 +0,0 @@ -/* Copyright (c) 2012 Google Inc. - * - * 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.util; - -/** - * Represents the result of an in-app billing operation. - * A result is composed of a response code (an integer) and possibly a - * message (String). You can get those by calling - * {@link #getResponse} and {@link #getMessage()}, respectively. You - * can also inquire whether a result is a success or a failure by - * calling {@link #isSuccess()} and {@link #isFailure()}. - */ -public class IabResult { - int mResponse; - String mMessage; - - public IabResult(int response, String message) { - mResponse = response; - if (message == null || message.trim().length() == 0) { - mMessage = IabHelper.getResponseDesc(response); - } - else { - mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")"; - } - } - public int getResponse() { return mResponse; } - public String getMessage() { return mMessage; } - public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; } - public boolean isFailure() { return !isSuccess(); } - public String toString() { return "IabResult: " + getMessage(); } -} - diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java deleted file mode 100644 index ae13e74f..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Inventory.java +++ /dev/null @@ -1,91 +0,0 @@ -/* Copyright (c) 2012 Google Inc. - * - * 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.util; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Represents a block of information about in-app items. - * An Inventory is returned by such methods as {@link IabHelper#queryInventory}. - */ -public class Inventory { - Map mSkuMap = new HashMap(); - Map mPurchaseMap = new HashMap(); - - Inventory() { } - - /** Returns the listing details for an in-app product. */ - public SkuDetails getSkuDetails(String sku) { - return mSkuMap.get(sku); - } - - /** Returns purchase information for a given product, or null if there is no purchase. */ - public Purchase getPurchase(String sku) { - return mPurchaseMap.get(sku); - } - - /** Returns whether or not there exists a purchase of the given product. */ - public boolean hasPurchase(String sku) { - return mPurchaseMap.containsKey(sku); - } - - /** Return whether or not details about the given product are available. */ - public boolean hasDetails(String sku) { - return mSkuMap.containsKey(sku); - } - - /** - * Erase a purchase (locally) from the inventory, given its product ID. This just - * modifies the Inventory object locally and has no effect on the server! This is - * useful when you have an existing Inventory object which you know to be up to date, - * and you have just consumed an item successfully, which means that erasing its - * purchase data from the Inventory you already have is quicker than querying for - * a new Inventory. - */ - public void erasePurchase(String sku) { - if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku); - } - - /** Returns a list of all owned product IDs. */ - List getAllOwnedSkus() { - return new ArrayList(mPurchaseMap.keySet()); - } - - /** Returns a list of all owned product IDs of a given type */ - List getAllOwnedSkus(String itemType) { - List result = new ArrayList(); - for (Purchase p : mPurchaseMap.values()) { - if (p.getItemType().equals(itemType)) result.add(p.getSku()); - } - return result; - } - - /** Returns a list of all purchases. */ - List getAllPurchases() { - return new ArrayList(mPurchaseMap.values()); - } - - void addSkuDetails(SkuDetails d) { - mSkuMap.put(d.getSku(), d); - } - - void addPurchase(Purchase p) { - mPurchaseMap.put(p.getSku(), p); - } -} 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/LogRecorder.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java deleted file mode 100644 index ad4d54ae..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/LogRecorder.java +++ /dev/null @@ -1,540 +0,0 @@ -package com.v2ray.ang.util; - -import android.content.Context; -import android.os.Environment; -import android.os.Handler; -import android.os.Message; -import android.text.TextUtils; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * Reference to http://blog.csdn.net/way_ping_li/article/details/8487866 - * and improved some features... - */ -public class LogRecorder { - - public static final int LOG_LEVEL_NO_SET = 0; - - public static final int LOG_BUFFER_MAIN = 1; - public static final int LOG_BUFFER_SYSTEM = 1 << 1; - public static final int LOG_BUFFER_RADIO = 1 << 2; - public static final int LOG_BUFFER_EVENTS = 1 << 3; - public static final int LOG_BUFFER_KERNEL = 1 << 4; // not be supported by now - - public static final int LOG_BUFFER_DEFAULT = LOG_BUFFER_MAIN | LOG_BUFFER_SYSTEM; - - public static final int INVALID_PID = -1; - - public String mFileSuffix; - public String mFolderPath; - public int mFileSizeLimitation; - public int mLevel; - public List mFilterTags = new ArrayList<>(); - public int mPID = INVALID_PID; - - public boolean mUseLogcatFileOut = false; - - private LogDumper mLogDumper = null; - - public static final int EVENT_RESTART_LOG = 1001; - - private RestartHandler mHandler; - - private static class RestartHandler extends Handler { - final LogRecorder logRecorder; - public RestartHandler(LogRecorder logRecorder) { - this.logRecorder = logRecorder; - } - - @Override - public void handleMessage(Message msg) { - if (msg.what == EVENT_RESTART_LOG) { - logRecorder.stop(); - logRecorder.start(); - } - } - } - - public LogRecorder() { - mHandler = new RestartHandler(this); - } - - public void start() { - // make sure the out folder exist - // TODO support multi-phase path - File file = new File(mFolderPath); - if (!file.exists()) { - file.mkdirs(); - } - - String cmdStr = collectLogcatCommand(); - - if (mLogDumper != null) { - mLogDumper.stopDumping(); - mLogDumper = null; - } - - mLogDumper = new LogDumper(mFolderPath, mFileSuffix, mFileSizeLimitation, cmdStr, mHandler); - mLogDumper.start(); - } - - public void stop() { - // TODO maybe should clean the log buffer first? - if (mLogDumper != null) { - mLogDumper.stopDumping(); - mLogDumper = null; - } - } - - private String collectLogcatCommand() { - StringBuilder stringBuilder = new StringBuilder(); - final String SPACE = " "; - stringBuilder.append("logcat"); - - // TODO select ring buffer, -b - - // TODO set out format - stringBuilder.append(SPACE); - stringBuilder.append("-v time"); - - // append tag filters - String levelStr = getLevelStr(); - - if (!mFilterTags.isEmpty()) { - stringBuilder.append(SPACE); - stringBuilder.append("-s"); - for (int i = 0; i < mFilterTags.size(); i++) { - String tag = mFilterTags.get(i) + ":" + levelStr; - stringBuilder.append(SPACE); - stringBuilder.append(tag); - } - } else { - if (!TextUtils.isEmpty(levelStr)) { - stringBuilder.append(SPACE); - stringBuilder.append("*:" + levelStr); - } - } - - // logcat -f , but the rotated count default is 4? - // can`t be sure to use that feature - if (mPID != INVALID_PID) { - mUseLogcatFileOut = false; - String pidStr = adjustPIDStr(); - if (!TextUtils.isEmpty(pidStr)) { - stringBuilder.append(SPACE); - stringBuilder.append("|"); - stringBuilder.append(SPACE); - stringBuilder.append("grep (" + pidStr + ")"); - } - } - - return stringBuilder.toString(); - } - - private String getLevelStr() { - switch (mLevel) { - case 2: - return "V"; - case 3: - return "D"; - case 4: - return "I"; - case 5: - return "W"; - case 6: - return "E"; - case 7: - return "F"; - } - - return "V"; - } - - /** - * Android`s user app pid is bigger than 1000. - * - * @return - */ - private String adjustPIDStr() { - if (mPID == INVALID_PID) { - return null; - } - - String pidStr = String.valueOf(mPID); - int length = pidStr.length(); - if (length < 4) { - pidStr = " 0" + pidStr; - } - - if (length == 4) { - pidStr = " " + pidStr; - } - - return pidStr; - } - - - private class LogDumper extends Thread { - final String logPath; - final String logFileSuffix; - final int logFileLimitation; - final String logCmd; - - final RestartHandler restartHandler; - - private Process logcatProc; - private BufferedReader mReader = null; - private FileOutputStream out = null; - - private boolean mRunning = true; - final private Object mRunningLock = new Object(); - - private long currentFileSize; - - public LogDumper(String folderPath, String suffix, - int fileSizeLimitation, String command, - RestartHandler handler) { - logPath = folderPath; - logFileSuffix = suffix; - logFileLimitation = fileSizeLimitation; - logCmd = command; - restartHandler = handler; - - String date = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss") - .format(new Date(System.currentTimeMillis())); - String fileName = (TextUtils.isEmpty(logFileSuffix)) ? date : (logFileSuffix + "-"+ date); - try { - out = new FileOutputStream(new File(logPath, fileName + ".log")); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - } - - public void stopDumping() { - synchronized (mRunningLock) { - mRunning = false; - } - } - - @Override - public void run() { - try { - logcatProc = Runtime.getRuntime().exec(logCmd); - mReader = new BufferedReader(new InputStreamReader( - logcatProc.getInputStream()), 1024); - String line = null; - while (mRunning && (line = mReader.readLine()) != null) { - if (!mRunning) { - break; - } - if (line.length() == 0) { - continue; - } - if (out != null && !line.isEmpty()) { - byte[] data = (line + "\n").getBytes(); - out.write(data); - if (logFileLimitation != 0) { - currentFileSize += data.length; - if (currentFileSize > logFileLimitation*1024) { - restartHandler.sendEmptyMessage(EVENT_RESTART_LOG); - break; - } - } - } - } - - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (logcatProc != null) { - logcatProc.destroy(); - logcatProc = null; - } - if (mReader != null) { - try { - mReader.close(); - mReader = null; - } catch (IOException e) { - e.printStackTrace(); - } - } - if (out != null) { - try { - out.close(); - } catch (IOException e) { - e.printStackTrace(); - } - out = null; - } - } - } - } - - public static class Builder { - - /** - * context object - */ - private Context mContext; - - /** - * the folder name that we save log files to, - * just folder name, not the whole path, - * if set this, will save log files to /sdcard/$mLogFolderName folder, - * use /sdcard/$ApplicationName as default. - */ - private String mLogFolderName; - - /** - * the whole folder path that we save log files to, - * this setting`s priority is bigger than folder name. - */ - private String mLogFolderPath; - - /** - * the log file suffix, - * if this is sot, it will be appended to log file name automatically - */ - private String mLogFileNameSuffix = ""; - - /** - * single log file size limitation, - * in k-bytes, ex. set to 16, is 16KB limitation. - */ - private int mLogFileSizeLimitation = 0; - - /** - * log level, see android.util.Log, 2 - 7, - * if not be set, will use verbose as default - */ - private int mLogLevel = LogRecorder.LOG_LEVEL_NO_SET; - - /** - * can set several filter tags - * logcat -s ActivityManager:V SystemUI:V - */ - private List mLogFilterTags = new ArrayList<>(); - - /** - * filter through pid, by setting this with your APP PID, - * the log recorder will just record the APP`s own log, - * use one call: android.os.Process.myPid(). - */ - private int mPID = LogRecorder.INVALID_PID; - - /** - * which log buffer to catch... - *

- * Request alternate ring buffer, 'main', 'system', 'radio' - * or 'events'. Multiple -b parameters are allowed and the - * results are interleaved. - *

- * The default is -b main -b system. - */ - private int mLogBuffersSelected = LogRecorder.LOG_BUFFER_DEFAULT; - - /** - * log output format, don`t support config yet, use $time format as default. - *

- * Log messages contain a number of metadata fields, in addition to the tag and priority. - * You can modify the output format for messages so that they display a specific metadata - * field. To do so, you use the -v option and specify one of the supported output formats - * listed below. - *

- * brief — Display priority/tag and PID of the process issuing the message. - * process — Display PID only. - * tag — Display the priority/tag only. - * thread - Display the priority, tag, and the PID(process ID) and TID(thread ID) - * of the thread issuing the message. - * raw — Display the raw log message, with no other metadata fields. - * time — Display the date, invocation time, priority/tag, and PID of - * the process issuing the message. - * threadtime — Display the date, invocation time, priority, tag, and the PID(process ID) - * and TID(thread ID) of the thread issuing the message. - * long — Display all metadata fields and separate messages with blank lines. - */ - private int mLogOutFormat; - - /** - * set log out folder name - * - * @param logFolderName folder name - * @return The same Builder. - */ - public Builder setLogFolderName(String logFolderName) { - this.mLogFolderName = logFolderName; - return this; - } - - /** - * set log out folder path - * - * @param logFolderPath out folder absolute path - * @return the same Builder - */ - public Builder setLogFolderPath(String logFolderPath) { - this.mLogFolderPath = logFolderPath; - return this; - } - - /** - * set log file name suffix - * - * @param logFileNameSuffix auto appened suffix - * @return the same Builder - */ - public Builder setLogFileNameSuffix(String logFileNameSuffix) { - this.mLogFileNameSuffix = logFileNameSuffix; - return this; - } - - /** - * set the file size limitation - * - * @param fileSizeLimitation file size limitation in KB - * @return the same Builder - */ - public Builder setLogFileSizeLimitation(int fileSizeLimitation) { - this.mLogFileSizeLimitation = fileSizeLimitation; - return this; - } - - /** - * set the log level - * - * @param logLevel log level, 2-7 - * @return the same Builder - */ - public Builder setLogLevel(int logLevel) { - this.mLogLevel = logLevel; - return this; - } - - /** - * add log filterspec tag name, can add multiple ones, - * they use the same log level set by setLogLevel() - * - * @param tag tag name - * @return the same Builder - */ - public Builder addLogFilterTag(String tag) { - mLogFilterTags.add(tag); - return this; - } - - /** - * which process`s log - * - * @param mPID process id - * @return the same Builder - */ - public Builder setPID(int mPID) { - this.mPID = mPID; - return this; - } - - /** - * -b radio, -b main, -b system, -b events - * -b main -b system as default - * - * @param logBuffersSelected one of - * LOG_BUFFER_MAIN = 1 << 0; - * LOG_BUFFER_SYSTEM = 1 << 1; - * LOG_BUFFER_RADIO = 1 << 2; - * LOG_BUFFER_EVENTS = 1 << 3; - * LOG_BUFFER_KERNEL = 1 << 4; - * @return the same Builder - */ - public Builder setLogBufferSelected(int logBuffersSelected) { - this.mLogBuffersSelected = logBuffersSelected; - return this; - } - - /** - * sets log out format, -v parameter - * - * @param logOutFormat out format, like -v time - * @return the same Builder - */ - public Builder setLogOutFormat(int logOutFormat) { - this.mLogOutFormat = mLogOutFormat; - return this; - } - - public Builder(Context context) { - mContext = context; - } - - /** - * call this only if mLogFolderName and mLogFolderPath not - * be set both. - * - * @return - */ - private void applyAppNameAsOutfolderName() { - try { - String appName = mContext.getPackageName(); - String versionName = mContext.getPackageManager().getPackageInfo( - appName, 0).versionName; - int versionCode = mContext.getPackageManager() - .getPackageInfo(appName, 0).versionCode; - mLogFolderName = appName + "-" + versionName + "-" + versionCode; - mLogFolderPath = applyOutfolderPath(); - } catch (Exception e) { - } - } - - private String applyOutfolderPath() { - String outPath = ""; - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - outPath = Environment.getExternalStorageDirectory() - .getAbsolutePath() + File.separator + mLogFolderName; - } - - return outPath; - } - - /** - * Combine all of the options that have been set and return - * a new {@link LogRecorder} object. - */ - public LogRecorder build() { - LogRecorder logRecorder = new LogRecorder(); - - // no folder name & folder path be set - if (TextUtils.isEmpty(mLogFolderName) - && TextUtils.isEmpty(mLogFolderPath)) { - applyAppNameAsOutfolderName(); - } - - // make sure out path be set - if (TextUtils.isEmpty(mLogFolderPath)) { - mLogFolderPath = applyOutfolderPath(); - } - - logRecorder.mFolderPath = mLogFolderPath; - logRecorder.mFileSuffix = mLogFileNameSuffix; - logRecorder.mFileSizeLimitation = mLogFileSizeLimitation; - logRecorder.mLevel = mLogLevel; - if (!mLogFilterTags.isEmpty()) { - for (int i = 0; i < mLogFilterTags.size(); i++) { - logRecorder.mFilterTags.add(mLogFilterTags.get(i)); - } - } - logRecorder.mPID = mPID; - - return logRecorder; - } - } - -} \ 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/Purchase.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java deleted file mode 100644 index d5e59153..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Purchase.java +++ /dev/null @@ -1,63 +0,0 @@ -/* Copyright (c) 2012 Google Inc. - * - * 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.util; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Represents an in-app billing purchase. - */ -public class Purchase { - String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS - String mOrderId; - String mPackageName; - String mSku; - long mPurchaseTime; - int mPurchaseState; - String mDeveloperPayload; - String mToken; - String mOriginalJson; - String mSignature; - - public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException { - mItemType = itemType; - mOriginalJson = jsonPurchaseInfo; - JSONObject o = new JSONObject(mOriginalJson); - mOrderId = o.optString("orderId"); - mPackageName = o.optString("packageName"); - mSku = o.optString("productId"); - mPurchaseTime = o.optLong("purchaseTime"); - mPurchaseState = o.optInt("purchaseState"); - mDeveloperPayload = o.optString("developerPayload"); - mToken = o.optString("token", o.optString("purchaseToken")); - mSignature = signature; - } - - public String getItemType() { return mItemType; } - public String getOrderId() { return mOrderId; } - public String getPackageName() { return mPackageName; } - public String getSku() { return mSku; } - public long getPurchaseTime() { return mPurchaseTime; } - public int getPurchaseState() { return mPurchaseState; } - public String getDeveloperPayload() { return mDeveloperPayload; } - public String getToken() { return mToken; } - public String getOriginalJson() { return mOriginalJson; } - public String getSignature() { return mSignature; } - - @Override - public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; } -} 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/Security.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java deleted file mode 100644 index 50f02e3c..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Security.java +++ /dev/null @@ -1,119 +0,0 @@ -/* Copyright (c) 2012 Google Inc. - * - * 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.util; - -import android.text.TextUtils; -import android.util.Log; - -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; - -/** - * Security-related methods. For a secure implementation, all of this code - * should be implemented on a server that communicates with the - * application on the device. For the sake of simplicity and clarity of this - * example, this code is included here and is executed on the device. If you - * must verify the purchases on the phone, you should obfuscate this code to - * make it harder for an attacker to replace the code with stubs that treat all - * purchases as verified. - */ -public class Security { - private static final String TAG = "IABUtil/Security"; - - private static final String KEY_FACTORY_ALGORITHM = "RSA"; - private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; - - /** - * Verifies that the data was signed with the given signature, and returns - * the verified purchase. The data is in JSON format and signed - * with a private key. The data also contains the {@link PurchaseState} - * and product ID of the purchase. - * @param base64PublicKey the base64-encoded public key to use for verifying. - * @param signedData the signed JSON string (signed, not encrypted) - * @param signature the signature for the data, signed with the private key - */ - public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) { - if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) || - TextUtils.isEmpty(signature)) { - Log.e(TAG, "Purchase verification failed: missing data."); - return false; - } - - PublicKey key = Security.generatePublicKey(base64PublicKey); - return Security.verify(key, signedData, signature); - } - - /** - * Generates a PublicKey instance from a string containing the - * Base64-encoded public key. - * - * @param encodedPublicKey Base64-encoded public key - * @throws IllegalArgumentException if encodedPublicKey is invalid - */ - public static PublicKey generatePublicKey(String encodedPublicKey) { - try { - byte[] decodedKey = Base64.decode(encodedPublicKey); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); - return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } catch (InvalidKeySpecException e) { - Log.e(TAG, "Invalid key specification."); - throw new IllegalArgumentException(e); - } catch (Base64DecoderException e) { - Log.e(TAG, "Base64 decoding failed."); - throw new IllegalArgumentException(e); - } - } - - /** - * Verifies that the signature from the server matches the computed - * signature on the data. Returns true if the data is correctly signed. - * - * @param publicKey public key associated with the developer account - * @param signedData signed data from server - * @param signature server signature - * @return true if the data and signature match - */ - public static boolean verify(PublicKey publicKey, String signedData, String signature) { - Signature sig; - try { - sig = Signature.getInstance(SIGNATURE_ALGORITHM); - sig.initVerify(publicKey); - sig.update(signedData.getBytes()); - if (!sig.verify(Base64.decode(signature))) { - Log.e(TAG, "Signature verification failed."); - return false; - } - return true; - } catch (NoSuchAlgorithmException e) { - Log.e(TAG, "NoSuchAlgorithmException."); - } catch (InvalidKeyException e) { - Log.e(TAG, "Invalid key specification."); - } catch (SignatureException e) { - Log.e(TAG, "Signature exception."); - } catch (Base64DecoderException e) { - Log.e(TAG, "Base64 decoding failed."); - } - return false; - } -} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java b/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java deleted file mode 100644 index b15cd472..00000000 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/SkuDetails.java +++ /dev/null @@ -1,58 +0,0 @@ -/* Copyright (c) 2012 Google Inc. - * - * 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.util; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Represents an in-app product's listing details. - */ -public class SkuDetails { - String mItemType; - String mSku; - String mType; - String mPrice; - String mTitle; - String mDescription; - String mJson; - - public SkuDetails(String jsonSkuDetails) throws JSONException { - this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails); - } - - public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException { - mItemType = itemType; - mJson = jsonSkuDetails; - JSONObject o = new JSONObject(mJson); - mSku = o.optString("productId"); - mType = o.optString("type"); - mPrice = o.optString("price"); - mTitle = o.optString("title"); - mDescription = o.optString("description"); - } - - public String getSku() { return mSku; } - public String getType() { return mType; } - public String getPrice() { return mPrice; } - public String getTitle() { return mTitle; } - public String getDescription() { return mDescription; } - - @Override - public String toString() { - return "SkuDetails:" + mJson; - } -} 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/kotlin/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt deleted file mode 100644 index 73f816d5..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AngApplication.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.v2ray.ang - -//import com.squareup.leakcanary.LeakCanary -import android.support.multidex.MultiDexApplication -import com.v2ray.ang.util.AngConfigManager -import me.dozen.dpreference.DPreference -import org.jetbrains.anko.defaultSharedPreferences - -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 - - val defaultDPreference by lazy { DPreference(this, packageName + "_preferences") } - - override fun onCreate() { - super.onCreate() - -// LeakCanary.install(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) - AngConfigManager.inject(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 9f2278db..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/AppConfig.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.v2ray.ang - -/** - * - * App Config Const - */ -object AppConfig { - const val ANG_PACKAGE = "com.v2ray.ang" - const val ANG_CONFIG = "ang_config" - const val PREF_CURR_CONFIG = "pref_v2ray_config" - const val PREF_CURR_CONFIG_GUID = "pref_v2ray_config_guid" - const val PREF_CURR_CONFIG_NAME = "pref_v2ray_config_name" - const val PREF_CURR_CONFIG_DOMAIN = "pref_v2ray_config_domain" - const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium" - const val VMESS_PROTOCOL: String = "vmess://" - const val SS_PROTOCOL: String = "ss://" - const val SOCKS_PROTOCOL: String = "socks://" - 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 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 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 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 - - object EConfigType { - val Vmess = 1 - val Custom = 2 - val Shadowsocks = 3 - val Socks = 4 - } - -} \ No newline at end of file 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/V2rayConfig.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt deleted file mode 100644 index ec1a87ec..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/V2rayConfig.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.v2ray.ang.dto - -data class V2rayConfig( - val stats: Any?=null, - val log: LogBean, - val policy: PolicyBean, - val inbounds: ArrayList, - var outbounds: ArrayList, - var dns: DnsBean, - val routing: RoutingBean) { - - data class LogBean(val access: String, - val error: String, - val loglevel: String) - - data class InboundBean( - var tag: String, - var port: Int, - var protocol: String, - var listen: String?=null, - val settings: InSettingsBean, - val sniffing: SniffingBean?) { - - 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: List) - } - - data class OutboundBean(val tag: String, - var protocol: String, - var settings: OutSettingsBean?, - var streamSettings: StreamSettingsBean?, - var mux: MuxBean?) { - - data class OutSettingsBean(var vnext: List?, - var servers: List?, - var response: Response) { - - data class VnextBean(var address: String, - var port: Int, - var users: List) { - - data class UsersBean(var id: String, - var alterId: Int, - var security: String, - var level: Int) - } - - data class ServersBean(var address: String, - var method: String, - var ota: Boolean, - var password: String, - var port: Int, - var level: Int) - - data class Response(var type: String) - } - - data class StreamSettingsBean(var network: String, - var security: String, - var tcpSettings: TcpsettingsBean?, - var kcpsettings: KcpsettingsBean?, - var wssettings: WssettingsBean?, - var httpsettings: HttpsettingsBean?, - var tlssettings: TlssettingsBean?, - var quicsettings: QuicsettingBean? - ) { - - data class TcpsettingsBean(var connectionReuse: Boolean = true, - var header: HeaderBean = HeaderBean()) { - data class HeaderBean(var type: String = "none", - var request: Any? = null, - var response: Any? = null) - } - - data class KcpsettingsBean(var mtu: Int = 1350, - var tti: Int = 20, - var uplinkCapacity: Int = 12, - var downlinkCapacity: Int = 100, - var congestion: Boolean = false, - var readBufferSize: Int = 1, - var writeBufferSize: Int = 1, - var header: HeaderBean = HeaderBean()) { - data class HeaderBean(var type: String = "none") - } - - data class WssettingsBean(var connectionReuse: Boolean = true, - var path: String = "", - var headers: HeadersBean = HeadersBean()) { - data class HeadersBean(var Host: String = "") - } - - data class HttpsettingsBean(var host: List = ArrayList(), var path: String = "") - - data class TlssettingsBean(var allowInsecure: Boolean = true, - var serverName: String = "") - - data class QuicsettingBean(var security: String = "none", - var key: String = "", - var header: HeaderBean = HeaderBean()) { - data class HeaderBean(var type: String = "none") - } - } - - data class MuxBean(var enabled: Boolean) - } - - //data class DnsBean(var servers: List) - data class DnsBean(var servers: List?=null, - var hosts: Map?=null - ) { - data class ServersBean(var address: String = "", - var port: Int = 0, - var domains: List?) - } - - data class RoutingBean(var domainStrategy: String, - var rules: ArrayList) { - - data class RulesBean(var type: String = "", - var ip: ArrayList? = null, - var domain: ArrayList? = null, - var outboundTag: String = "", - var port: String? = null, - var inboundTag: ArrayList? = 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) - } -} \ No newline at end of file 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 30618017..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/VmessQRCode.kt +++ /dev/null @@ -1,13 +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 = "", - var net: String = "", - var type: String = "", - var host: String = "", - var path: String = "", - var tls: String = "") \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt deleted file mode 100644 index f70b5130..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Dialog.kt +++ /dev/null @@ -1,244 +0,0 @@ -package com.v2ray.ang.extension - -import android.app.Fragment -import android.app.ProgressDialog -import android.content.Context -import android.content.DialogInterface -import android.database.Cursor -import android.graphics.drawable.Drawable -import android.support.v7.app.AlertDialog -import android.view.KeyEvent -import android.view.View -import android.widget.ListAdapter - - -fun Context.alertView( - title: String? = null, - view: View, - init: (KAlertDialogBuilder.() -> Unit)? = null -) = KAlertDialogBuilder(this).apply { - if (title != null) title(title) - if (title != null) customView(view) - if (init != null) init() -} - -fun Fragment.alert( - message: String, - title: String? = null, - init: (KAlertDialogBuilder.() -> Unit)? = null -) = activity.alert(message, title, init) - -fun Context.alert( - message: String, - title: String? = null, - init: (KAlertDialogBuilder.() -> Unit)? = null -) = KAlertDialogBuilder(this).apply { - if (title != null) title(title) - message(message) - if (init != null) init() -} - -fun Fragment.alert( - message: Int, - title: Int? = null, - init: (KAlertDialogBuilder.() -> Unit)? = null -) = activity.alert(message, title, init) - -fun Context.alert( - message: Int, - title: Int? = null, - init: (KAlertDialogBuilder.() -> Unit)? = null -) = KAlertDialogBuilder(this).apply { - if (title != null) title(title) - message(message) - if (init != null) init() -} - - -fun Fragment.alert(init: KAlertDialogBuilder.() -> Unit): KAlertDialogBuilder = activity.alert(init) - -fun Context.alert(init: KAlertDialogBuilder.() -> Unit) = KAlertDialogBuilder(this).apply { init() } - -fun Fragment.progressDialog( - message: Int? = null, - title: Int? = null, - init: (ProgressDialog.() -> Unit)? = null -) = activity.progressDialog(message, title, init) - -fun Context.progressDialog( - message: Int? = null, - title: Int? = null, - init: (ProgressDialog.() -> Unit)? = null -) = progressDialog(false, message?.let { getString(it) }, title?.let { getString(it) }, init) - -fun Fragment.indeterminateProgressDialog( - message: Int? = null, - title: Int? = null, - init: (ProgressDialog.() -> Unit)? = null -) = activity.progressDialog(message, title, init) - -fun Context.indeterminateProgressDialog( - message: Int? = null, - title: Int? = null, - init: (ProgressDialog.() -> Unit)? = null -) = progressDialog(true, message?.let { getString(it) }, title?.let { getString(it) }, init) - -fun Fragment.progressDialog( - message: String? = null, - title: String? = null, - init: (ProgressDialog.() -> Unit)? = null -) = activity.progressDialog(message, title, init) - -fun Context.progressDialog( - message: String? = null, - title: String? = null, - init: (ProgressDialog.() -> Unit)? = null -) = progressDialog(false, message, title, init) - -fun Fragment.indeterminateProgressDialog( - message: String? = null, - title: String? = null, - init: (ProgressDialog.() -> Unit)? = null -) = activity.indeterminateProgressDialog(message, title, init) - -fun Context.indeterminateProgressDialog( - message: String? = null, - title: String? = null, - init: (ProgressDialog.() -> Unit)? = null -) = progressDialog(true, message, title, init) - -private fun Context.progressDialog( - indeterminate: Boolean, - message: String? = null, - title: String? = null, - init: (ProgressDialog.() -> Unit)? = null -) = ProgressDialog(this).apply { - isIndeterminate = indeterminate - if (!indeterminate) setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) - if (message != null) setMessage(message) - if (title != null) setTitle(title) - if (init != null) init() - show() -} - -fun Fragment.selector( - title: CharSequence? = null, - items: List, - onClick: (Int) -> Unit -): Unit = activity.selector(title, items, onClick) - -fun Context.selector( - title: CharSequence? = null, - items: List, - onClick: (Int) -> Unit -) { - with(KAlertDialogBuilder(this)) { - if (title != null) title(title) - items(items, onClick) - show() - } -} - -class KAlertDialogBuilder(val ctx: Context) { - - val builder: AlertDialog.Builder = AlertDialog.Builder(ctx) - protected var dialog: AlertDialog? = null - - fun dismiss() { - dialog?.dismiss() - } - - fun show(): KAlertDialogBuilder { - dialog = builder.create() - dialog!!.show() - return this - } - - fun title(title: CharSequence) { - builder.setTitle(title) - } - - fun title(resource: Int) { - builder.setTitle(resource) - } - - fun message(title: CharSequence) { - builder.setMessage(title) - } - - fun message(resource: Int) { - builder.setMessage(resource) - } - - fun icon(icon: Int) { - builder.setIcon(icon) - } - - fun icon(icon: Drawable) { - builder.setIcon(icon) - } - - fun customTitle(title: View) { - builder.setCustomTitle(title) - } - - fun customView(view: View) { - builder.setView(view) - } - - fun cancellable(value: Boolean = true) { - builder.setCancelable(value) - } - - fun onCancel(f: () -> Unit) { - builder.setOnCancelListener { f() } - } - - fun onKey(f: (keyCode: Int, e: KeyEvent) -> Boolean) { - builder.setOnKeyListener({ dialog, keyCode, event -> f(keyCode, event) }) - } - - fun neutralButton(textResource: Int = android.R.string.ok, f: DialogInterface.() -> Unit = { dismiss() }) { - neutralButton(ctx.getString(textResource), f) - } - - fun neutralButton(title: String, f: DialogInterface.() -> Unit = { dismiss() }) { - builder.setNeutralButton(title, { dialog, which -> dialog.f() }) - } - - fun positiveButton(textResource: Int = android.R.string.ok, f: DialogInterface.() -> Unit) { - positiveButton(ctx.getString(textResource), f) - } - - fun positiveButton(title: String, f: DialogInterface.() -> Unit) { - builder.setPositiveButton(title, { dialog, which -> dialog.f() }) - } - - fun negativeButton(textResource: Int = android.R.string.cancel, f: DialogInterface.() -> Unit = { dismiss() }) { - negativeButton(ctx.getString(textResource), f) - } - - fun negativeButton(title: String, f: DialogInterface.() -> Unit = { dismiss() }) { - builder.setNegativeButton(title, { dialog, which -> dialog.f() }) - } - - fun items(itemsId: Int, f: (which: Int) -> Unit) { - items(ctx.resources!!.getTextArray(itemsId), f) - } - - fun items(items: List, f: (which: Int) -> Unit) { - items(items.toTypedArray(), f) - } - - fun items(items: Array, f: (which: Int) -> Unit) { - builder.setItems(items, { dialog, which -> f(which) }) - } - - fun adapter(adapter: ListAdapter, f: (which: Int) -> Unit) { - builder.setAdapter(adapter, { dialog, which -> f(which) }) - } - - fun adapter(cursor: Cursor, labelColumn: String, f: (which: Int) -> Unit) { - builder.setCursor(cursor, { dialog, which -> f(which) }, labelColumn) - } -} \ No newline at end of file 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 fa028c47..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Ext.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.v2ray.ang.extension - -import android.content.Context -import android.os.Build -import com.v2ray.ang.AngApplication -import me.dozen.dpreference.DPreference -import org.json.JSONObject -import java.net.URLConnection - -/** - * Some extensions - */ - -val Context.v2RayApplication: AngApplication - get() = applicationContext as AngApplication - -val Context.defaultDPreference: DPreference - get() = v2RayApplication.defaultDPreference - - -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 < threshold) - return "$this B" - - val kib = this / divisor - if (kib < threshold) - return "${kib.toShortString()} KB" - - val mib = kib / divisor - if (mib < threshold) - return "${mib.toShortString()} MB" - - val gib = mib / divisor - if (gib < threshold) - return "${gib.toShortString()} GB" - - val tib = gib / divisor - if (tib < threshold) - return "${tib.toShortString()} TB" - - val pib = tib / divisor - if (pib < threshold) - return "${pib.toShortString()} PB" - - return "∞" -} - -private fun Float.toShortString(): String { - val s = toString() - 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() diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt deleted file mode 100644 index dcfa4671..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/extension/_Preference.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.v2ray.ang.extension - -import android.preference.Preference - -fun Preference.onClick(listener: () -> Unit) { - setOnPreferenceClickListener { - listener() - true - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt deleted file mode 100644 index dca1dc38..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/TaskerReceiver.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.v2ray.ang.receiver - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.text.TextUtils -import com.google.zxing.WriterException -import com.v2ray.ang.AppConfig - -import com.v2ray.ang.util.Utils - -class TaskerReceiver : BroadcastReceiver() { - 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, "") - - if (switch == null || guid == null || TextUtils.isEmpty(guid)) { - return - } else if (switch) { - if (guid == AppConfig.TASKER_DEFAULT_GUID) { - Utils.startVService(context) - } else { - Utils.startVService(context, guid) - } - } else { - Utils.stopVService(context) - } - } catch (e: WriterException) { - e.printStackTrace() - } - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt deleted file mode 100644 index eddc2146..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/receiver/WidgetProvider.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.v2ray.ang.receiver - -import android.app.PendingIntent -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.widget.RemoteViews -import com.v2ray.ang.R -import com.v2ray.ang.AppConfig -import com.v2ray.ang.util.Utils -import org.jetbrains.anko.toast - -class WidgetProvider : AppWidgetProvider() { - /** - * 每次窗口小部件被更新都调用一次该方法 - */ - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - super.onUpdate(context, appWidgetManager, appWidgetIds) - - val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch) - val intent = Intent(AppConfig.BROADCAST_ACTION_WIDGET_CLICK) - val pendingIntent = PendingIntent.getBroadcast(context, R.id.layout_switch, intent, PendingIntent.FLAG_UPDATE_CURRENT) - remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent) - - for (appWidgetId in appWidgetIds) { - appWidgetManager.updateAppWidget(appWidgetId, remoteViews) - } - } - - /** - * 接收窗口小部件点击时发送的广播 - */ - override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) - if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) { - - val isRunning = Utils.isServiceRun(context, "com.v2ray.ang.service.V2RayVpnService") - if (isRunning) { -// context.toast(R.string.toast_services_stop) - Utils.stopVService(context) - } else { -// context.toast(R.string.toast_services_start) - Utils.startVService(context) - } - } - } - -} 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 3e36ff28..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/service/V2RayVpnService.kt +++ /dev/null @@ -1,497 +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.content.pm.PackageManager -import android.graphics.Color -import android.net.* -import android.net.VpnService -import android.os.* -import android.support.annotation.RequiresApi -import android.support.v4.app.NotificationCompat -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.extension.defaultDPreference -import com.v2ray.ang.extension.toSpeedString -import com.v2ray.ang.ui.MainActivity -import com.v2ray.ang.ui.PerAppProxyActivity -import com.v2ray.ang.ui.SettingsActivity -import com.v2ray.ang.util.MessageUtil -import com.v2ray.ang.util.Utils -import libv2ray.Libv2ray -import libv2ray.V2RayVPNServiceSupportsSet -import rx.Observable -import rx.Subscription -import java.io.File -import java.lang.ref.SoftReference -import android.os.Build -import android.util.Log -import go.Seq -import org.jetbrains.anko.doAsync - -class V2RayVpnService : VpnService() { - companion object { - const val NOTIFICATION_ID = 1 - const val NOTIFICATION_PENDING_INTENT_CONTENT = 0 - const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1 - - fun startV2Ray(context: Context) { - val intent = Intent(context.applicationContext, V2RayVpnService::class.java) - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } - } - } - - private val v2rayPoint = Libv2ray.newV2RayPoint(V2RayCallback()) - private lateinit var configContent: String - private lateinit var mInterface: ParcelFileDescriptor - val fd: Int get() = mInterface.fd - private var mBuilder: NotificationCompat.Builder? = null - private var mSubscription: Subscription? = null - private var mNotificationManager: NotificationManager? = null - - - /** - * 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 - */ - private val defaultNetworkRequest by lazy @RequiresApi(Build.VERSION_CODES.P) { - 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 } - - private val defaultNetworkCallback by lazy @RequiresApi(Build.VERSION_CODES.P) { - 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) - } - } - } - private var listeningForDefaultNetwork = false - - override fun onCreate() { - super.onCreate() - - val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() - StrictMode.setThreadPolicy(policy) - v2rayPoint.packageName = Utils.packagePath(applicationContext) - Seq.setContext(applicationContext) - } - - override fun onRevoke() { - stopV2Ray() - } - - override fun onLowMemory() { - stopV2Ray() - super.onLowMemory() - } - - override fun onDestroy() { - super.onDestroy() - cancelNotification() - } - - fun setup(parameters: String) { - - val prepare = VpnService.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 = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false) - - parameters.split(" ") - .map { it.split(",") } - .forEach { - when (it[0][0]) { - 'm' -> builder.setMtu(java.lang.Short.parseShort(it[1]).toInt()) - 's' -> builder.addSearchDomain(it[1]) - 'a' -> builder.addAddress(it[1], Integer.parseInt(it[2])) - 'r' -> builder.addRoute(it[1], Integer.parseInt(it[2])) - 'd' -> builder.addDnsServer(it[1]) - } - } - - if(!enableLocalDns) { - Utils.getRemoteDnsServers(defaultDPreference) - .forEach { - builder.addDnsServer(it) - } - } - - builder.setSession(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "")) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && - defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false)) { - val apps = defaultDPreference.getPrefStringSet(PerAppProxyActivity.PREF_PER_APP_PROXY_SET, null) - val bypassApps = defaultDPreference.getPrefBoolean(PerAppProxyActivity.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) { - } - - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) - listeningForDefaultNetwork = true - } - - // Create a new interface using the builder and save the parameters. - mInterface = builder.establish() - sendFd() - startSpeedNotification() - } - - fun shutdown() { - stopV2Ray(true) - } - - fun sendFd() { - val fd = mInterface.fileDescriptor - val path = File(Utils.packagePath(applicationContext), "sock_path").absolutePath - - doAsync { - var tries = 0 - while (true) try { - Thread.sleep(50L shl tries) - Log.d(packageName, "sendFd tries: " + tries.toString()) - 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 { - startV2ray() - return START_STICKY - //return super.onStartCommand(intent, flags, startId) - } - - private fun startV2ray() { - if (!v2rayPoint.isRunning) { - - 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) - registerReceiver(mMsgReceive, mFilter) - } catch (e: Exception) { - } - - configContent = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "") - v2rayPoint.configureFileContent = configContent - v2rayPoint.enableLocalDNS = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false) - v2rayPoint.forwardIpv6 = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_FORWARD_IPV6, false) - v2rayPoint.domainName = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, "") - - try { - v2rayPoint.runLoop() - } catch (e: Exception) { - Log.d(packageName, e.toString()) - } - - if (v2rayPoint.isRunning) { - MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_START_SUCCESS, "") - showNotification() - } else { - MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_START_FAILURE, "") - cancelNotification() - } - } - // showNotification() - } - - 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) { - if (listeningForDefaultNetwork) { - connectivity.unregisterNetworkCallback(defaultNetworkCallback) - listeningForDefaultNetwork = false - } - } - if (v2rayPoint.isRunning) { - try { - v2rayPoint.stopLoop() - } catch (e: Exception) { - Log.d(packageName, e.toString()) - } - } - - MessageUtil.sendMsg2UI(this, AppConfig.MSG_STATE_STOP_SUCCESS, "") - cancelNotification() - - if (isForced) { - try { - unregisterReceiver(mMsgReceive) - } catch (e: Exception) { - } - - //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) { - } - - } - } - - private fun showNotification() { - val startMainIntent = Intent(applicationContext, MainActivity::class.java) - val contentPendingIntent = PendingIntent.getActivity(applicationContext, - NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, - PendingIntent.FLAG_UPDATE_CURRENT) - - val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) - stopV2RayIntent.`package` = AppConfig.ANG_PACKAGE - stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP) - - val stopV2RayPendingIntent = PendingIntent.getBroadcast(applicationContext, - NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, - 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(applicationContext, channelId) - .setSmallIcon(R.drawable.ic_v) - .setContentTitle(defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "")) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setOngoing(true) - .setShowWhen(false) - .setOnlyAlertOnce(true) - .setContentIntent(contentPendingIntent) - .addAction(R.drawable.ic_close_grey_800_24dp, - getString(R.string.notification_action_stop_v2ray), - stopV2RayPendingIntent) - //.build() - - //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使 - - 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 - } - - private fun cancelNotification() { - stopForeground(true) - mBuilder = null - mSubscription?.unsubscribe() - mSubscription = null - } - - private fun updateNotification(contentText: String) { - if (mBuilder != null) { - mBuilder?.setContentTitle(contentText) - getNotificationManager().notify(NOTIFICATION_ID, mBuilder?.build()) - } - } - - private fun getNotificationManager(): NotificationManager { - if (mNotificationManager == null) { - mNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - } - return mNotificationManager!! - } - - fun startSpeedNotification() { - if (mSubscription == null && - v2rayPoint.isRunning && - defaultDPreference.getPrefBoolean(SettingsActivity.PREF_SPEED_ENABLED, false)) { - val cf_name = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "") - var last_zero_speed = false - - mSubscription = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS) - .subscribe { - val uplink = v2rayPoint.queryStats("socks", "uplink") - val downlink = v2rayPoint.queryStats("socks", "downlink") - val zero_speed = (uplink == 0L && downlink == 0L) - if (!zero_speed || !last_zero_speed) { - updateNotification("${cf_name} • ${(uplink / 3).toSpeedString()}↑ ${(downlink / 3).toSpeedString()}↓") - } - last_zero_speed = zero_speed - } - } - } - - - fun stopSpeedNotification() { - if (mSubscription != null) { - mSubscription?.unsubscribe() //stop queryStats - mSubscription = null - - val cf_name = defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG_NAME, "") - updateNotification(cf_name) - } - } - - private inner class V2RayCallback : V2RayVPNServiceSupportsSet { - override fun shutdown(): Long { - // called by go - // shutdown the whole vpn service - try { - this@V2RayVpnService.shutdown() - return 0 - } catch (e: Exception) { - Log.d(packageName, e.toString()) - return -1 - } - } - - override fun prepare(): Long { - return 0 - } - - override fun protect(l: Long) = (if (this@V2RayVpnService.protect(l.toInt())) 0 else 1).toLong() - - override fun onEmitStatus(l: Long, s: String?): Long { - //Logger.d(s) - return 0 - } - - override fun setup(s: String): Long { - //Logger.d(s) - try { - this@V2RayVpnService.setup(s) - return 0 - } catch (e: Exception) { - Log.d(packageName, e.toString()) - return -1 - } - } - - override fun sendFd(): Long { - try { - this@V2RayVpnService.sendFd() - } catch (e: Exception) { - Log.d(packageName, e.toString()) - return -1 - } - return 0 - } - } - - private var mMsgReceive = ReceiveMessageHandler(this@V2RayVpnService) - - private class ReceiveMessageHandler(vpnService: V2RayVpnService) : BroadcastReceiver() { - internal var mReference: SoftReference = SoftReference(vpnService) - - override fun onReceive(ctx: Context?, intent: Intent?) { - val vpnService = mReference.get() - when (intent?.getIntExtra("key", 0)) { - AppConfig.MSG_REGISTER_CLIENT -> { - //Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString()) - - val isRunning = vpnService?.v2rayPoint!!.isRunning - && VpnService.prepare(vpnService) == null - if (isRunning) { - MessageUtil.sendMsg2UI(vpnService, AppConfig.MSG_STATE_RUNNING, "") - } else { - MessageUtil.sendMsg2UI(vpnService, AppConfig.MSG_STATE_NOT_RUNNING, "") - } - } - AppConfig.MSG_UNREGISTER_CLIENT -> { -// vpnService?.mMsgSend = null - } - AppConfig.MSG_STATE_START -> { - //nothing to do - } - AppConfig.MSG_STATE_STOP -> { - vpnService?.stopV2Ray() - } - AppConfig.MSG_STATE_RESTART -> { - vpnService?.startV2ray() - } - } - - when (intent?.action) { - Intent.ACTION_SCREEN_OFF -> { - Log.d(AppConfig.ANG_PACKAGE, "SCREEN_OFF, stop querying stats") - vpnService?.stopSpeedNotification() - } - Intent.ACTION_SCREEN_ON -> { - Log.d(AppConfig.ANG_PACKAGE, "SCREEN_ON, start querying stats") - vpnService?.startSpeedNotification() - } - } - } - } -} - 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 8ea4d87c..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 android.support.v7.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) - } -} \ No newline at end of file diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt deleted file mode 100644 index e427c7ed..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/BaseDrawerActivity.kt +++ /dev/null @@ -1,206 +0,0 @@ -package com.v2ray.ang.ui - -import android.app.ActivityOptions -import android.app.FragmentManager -import android.content.Intent -import android.content.res.Configuration -import android.os.Bundle -import android.support.design.widget.NavigationView -import android.support.v4.view.GravityCompat -import android.support.v4.widget.DrawerLayout -import android.support.v7.app.ActionBarDrawerToggle -import android.support.v7.widget.Toolbar -import android.util.Log -import android.view.MenuItem -import android.view.View -//import com.v2ray.ang.InappBuyActivity - -import com.v2ray.ang.R -import org.jetbrains.anko.startActivity - - -abstract class BaseDrawerActivity : BaseActivity() { - companion object { - - private val TAG = "BaseDrawerActivity" - } - - private var mToolbar: Toolbar? = null - - private var mDrawerToggle: ActionBarDrawerToggle? = null - - private var mDrawerLayout: DrawerLayout? = null - - private var mToolbarInitialized: Boolean = false - - private var mItemToOpenWhenDrawerCloses = -1 - - private val backStackChangedListener = FragmentManager.OnBackStackChangedListener { updateDrawerToggle() } - - private val drawerListener = object : DrawerLayout.DrawerListener { - override fun onDrawerSlide(drawerView: View, slideOffset: Float) { - mDrawerToggle!!.onDrawerSlide(drawerView, slideOffset) - } - - override fun onDrawerOpened(drawerView: View) { - mDrawerToggle!!.onDrawerOpened(drawerView) - //supportActionBar!!.setTitle(R.string.app_name) - } - - override fun onDrawerClosed(drawerView: View) { - mDrawerToggle!!.onDrawerClosed(drawerView) - - if (mItemToOpenWhenDrawerCloses >= 0) { - val extras = ActivityOptions.makeCustomAnimation( - this@BaseDrawerActivity, R.anim.fade_in, R.anim.fade_out).toBundle() - var activityClass: Class<*>? = null - when (mItemToOpenWhenDrawerCloses) { - R.id.server_profile -> activityClass = MainActivity::class.java - R.id.sub_setting -> activityClass = SubSettingActivity::class.java - R.id.settings -> activityClass = SettingsActivity::class.java - R.id.logcat -> { - startActivity() - return - } - R.id.donate -> { -// startActivity() - return - } - } - if (activityClass != null) { - startActivity(Intent(this@BaseDrawerActivity, activityClass), extras) - finish() - } - } - } - - override fun onDrawerStateChanged(newState: Int) { - mDrawerToggle!!.onDrawerStateChanged(newState) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Log.d(TAG, "Activity onCreate") - } - - override fun onStart() { - super.onStart() - if (!mToolbarInitialized) { - throw IllegalStateException("You must run super.initializeToolbar at " + "the end of your onCreate method") - } - } - - public override fun onResume() { - super.onResume() - // Whenever the fragment back stack changes, we may need to update the - // action bar toggle: only top level screens show the hamburger-like icon, inner - // screens - either Activities or fragments - show the "Up" icon instead. - fragmentManager.addOnBackStackChangedListener(backStackChangedListener) - } - - public override fun onPause() { - super.onPause() - fragmentManager.removeOnBackStackChangedListener(backStackChangedListener) - } - - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - mDrawerToggle!!.syncState() - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - mDrawerToggle!!.onConfigurationChanged(newConfig) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (mDrawerToggle != null && mDrawerToggle!!.onOptionsItemSelected(item)) { - return true - } - // If not handled by drawerToggle, home needs to be handled by returning to previous - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return super.onOptionsItemSelected(item) - } - - override fun onBackPressed() { - // If the drawer is open, back will close it - if (mDrawerLayout != null && mDrawerLayout!!.isDrawerOpen(GravityCompat.START)) { - mDrawerLayout!!.closeDrawers() - return - } - // Otherwise, it may return to the previous fragment stack - val fragmentManager = fragmentManager - if (fragmentManager.backStackEntryCount > 0) { - fragmentManager.popBackStack() - } else { - // Lastly, it will rely on the system behavior for back - super.onBackPressed() - } - } - - private fun updateDrawerToggle() { - if (mDrawerToggle == null) { - return - } - val isRoot = fragmentManager.backStackEntryCount == 0 - mDrawerToggle!!.isDrawerIndicatorEnabled = isRoot - - supportActionBar!!.setDisplayShowHomeEnabled(!isRoot) - supportActionBar!!.setDisplayHomeAsUpEnabled(!isRoot) - supportActionBar!!.setHomeButtonEnabled(!isRoot) - - if (isRoot) { - mDrawerToggle!!.syncState() - } - } - - protected fun initializeToolbar() { - mToolbar = findViewById(R.id.toolbar) as Toolbar - if (mToolbar == null) { - throw IllegalStateException("Layout is required to include a Toolbar with id " + "'toolbar'") - } - - // mToolbar.inflateMenu(R.menu.main); - - mDrawerLayout = findViewById(R.id.drawer_layout) as DrawerLayout - if (mDrawerLayout != null) { - val navigationView = findViewById(R.id.nav_view) as NavigationView - ?: throw IllegalStateException("Layout requires a NavigationView " + "with id 'nav_view'") - - // Create an ActionBarDrawerToggle that will handle opening/closing of the drawer: - mDrawerToggle = ActionBarDrawerToggle(this, mDrawerLayout, - mToolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) - - mDrawerLayout!!.addDrawerListener(drawerListener) - - populateDrawerItems(navigationView) - setSupportActionBar(mToolbar) - updateDrawerToggle() - } else { - setSupportActionBar(mToolbar) - } - - mToolbarInitialized = true - } - - private fun populateDrawerItems(navigationView: NavigationView) { - navigationView.setNavigationItemSelectedListener { menuItem -> - menuItem.isChecked = true - mItemToOpenWhenDrawerCloses = menuItem.itemId - mDrawerLayout!!.closeDrawers() - true - } - - if (MainActivity::class.java.isAssignableFrom(javaClass)) { - navigationView.setCheckedItem(R.id.server_profile) - } else if (SubSettingActivity::class.java.isAssignableFrom(javaClass)) { - navigationView.setCheckedItem(R.id.sub_setting) - } else if (SettingsActivity::class.java.isAssignableFrom(javaClass)) { - navigationView.setCheckedItem(R.id.settings) - } - } -} diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt deleted file mode 100644 index e4d34e3f..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/FragmentAdapter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.v2ray.ang.ui - - -import android.support.v4.app.Fragment -import android.support.v4.app.FragmentManager -import android.support.v4.app.FragmentStatePagerAdapter - -class FragmentAdapter(fm: FragmentManager, private val mFragments: List, private val mTitles: List) : FragmentStatePagerAdapter(fm) { - - override fun getItem(position: Int): Fragment { - return mFragments[position] - } - - override fun getCount(): Int { - return mFragments.size - } - - override fun getPageTitle(position: Int): CharSequence? { - return mTitles[position] - } -} \ No newline at end of file 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 d56395b8..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/LogcatActivity.kt +++ /dev/null @@ -1,86 +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.R -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.activity_logcat.* -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.toast -import org.jetbrains.anko.uiThread - -import java.io.IOException -import java.util.LinkedHashSet - -class LogcatActivity : BaseActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_logcat) - - title = getString(R.string.title_logcat) - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - logcat(false) - } - - private fun logcat(shouldFlushLog: Boolean) { - - try { - pb_waiting.visibility = View.VISIBLE - - doAsync { - 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,com.v2ray.ang") - 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() } - uiThread { - tv_logcat.text = allText - tv_logcat.movementMethod = ScrollingMovementMethod() - pb_waiting.visibility = View.GONE - Handler(Looper.getMainLooper()).post { sv_logcat.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, tv_logcat.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 045f681e..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainActivity.kt +++ /dev/null @@ -1,573 +0,0 @@ -package com.v2ray.ang.ui - -import android.Manifest -import android.content.* -import android.net.Uri -import android.net.VpnService -import android.support.v7.widget.LinearLayoutManager -import android.view.Menu -import android.view.MenuItem -import com.tbruyelle.rxpermissions.RxPermissions -import com.v2ray.ang.R -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.activity_main.* -import android.os.Bundle -import android.text.TextUtils -import android.view.KeyEvent -import com.v2ray.ang.AppConfig -import com.v2ray.ang.util.MessageUtil -import com.v2ray.ang.util.V2rayConfigUtil -import org.jetbrains.anko.* -import java.lang.ref.SoftReference -import java.net.URL -import android.content.IntentFilter -import android.support.design.widget.NavigationView -import android.support.v4.view.GravityCompat -import android.support.v7.app.ActionBarDrawerToggle -import android.support.v7.widget.helper.ItemTouchHelper -import android.util.Log -//import com.v2ray.ang.InappBuyActivity -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit -import com.v2ray.ang.helper.SimpleItemTouchHelperCallback -import com.v2ray.ang.util.AngConfigManager.configs - -class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { - companion object { - private const val REQUEST_CODE_VPN_PREPARE = 0 - private const val REQUEST_SCAN = 1 - private const val REQUEST_FILE_CHOOSER = 2 - private const val REQUEST_SCAN_URL = 3 - } - - var isRunning = false - set(value) { - field = value - adapter.changeable = !value - if (value) { - fab.imageResource = R.drawable.ic_v - tv_test_state.text = getString(R.string.connection_connected) - } else { - fab.imageResource = R.drawable.ic_v_idle - tv_test_state.text = getString(R.string.connection_not_connected) - } - hideCircle() - } - - private val adapter by lazy { MainRecyclerAdapter(this) } - private var mItemTouchHelper: ItemTouchHelper? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - title = getString(R.string.title_server) - setSupportActionBar(toolbar) - - fab.setOnClickListener { - if (isRunning) { - Utils.stopVService(this) - } else { - val intent = VpnService.prepare(this) - if (intent == null) { - startV2Ray() - } else { - startActivityForResult(intent, REQUEST_CODE_VPN_PREPARE) - } - } - } - layout_test.setOnClickListener { - if (isRunning) { - val socksPort = 10808//Utils.parseInt(defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808")) - - tv_test_state.text = getString(R.string.connection_test_testing) - doAsync { - val result = Utils.testConnection(this@MainActivity, socksPort) - uiThread { - tv_test_state.text = Utils.getEditable(result) - } - } - } else { -// tv_test_state.text = getString(R.string.connection_test_fail) - } - } - - recycler_view.setHasFixedSize(true) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.adapter = adapter - - val callback = SimpleItemTouchHelperCallback(adapter) - mItemTouchHelper = ItemTouchHelper(callback) - mItemTouchHelper?.attachToRecyclerView(recycler_view) - - - val toggle = ActionBarDrawerToggle( - this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) - drawer_layout.addDrawerListener(toggle) - toggle.syncState() - nav_view.setNavigationItemSelectedListener(this) - } - - fun startV2Ray() { - if (AngConfigManager.configs.index < 0) { - return - } - showCircle() -// toast(R.string.toast_services_start) - if (!Utils.startVService(this)) { - hideCircle() - } - } - - override fun onStart() { - super.onStart() - isRunning = false - -// val intent = Intent(this.applicationContext, V2RayVpnService::class.java) -// intent.`package` = AppConfig.ANG_PACKAGE -// bindService(intent, mConnection, BIND_AUTO_CREATE) - - mMsgReceive = ReceiveMessageHandler(this@MainActivity) - registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) - MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") - } - - override fun onStop() { - super.onStop() - if (mMsgReceive != null) { - unregisterReceiver(mMsgReceive) - mMsgReceive = null - } - } - - public override fun onResume() { - super.onResume() - adapter.updateConfigList() - } - - public override fun onPause() { - super.onPause() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - REQUEST_CODE_VPN_PREPARE -> - if (resultCode == RESULT_OK) { - startV2Ray() - } - REQUEST_SCAN -> - if (resultCode == RESULT_OK) { - importBatchConfig(data?.getStringExtra("SCAN_RESULT")) - } - REQUEST_FILE_CHOOSER -> { - if (resultCode == RESULT_OK) { - val uri = data!!.data - readContentFromUri(uri) - } - } - REQUEST_SCAN_URL -> - if (resultCode == RESULT_OK) { - importConfigCustomUrl(data?.getStringExtra("SCAN_RESULT")) - } - } - } - - 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(REQUEST_SCAN) - true - } - R.id.import_clipboard -> { - importClipboard() - true - } - R.id.import_manually_vmess -> { - startActivity("position" to -1, "isRunning" to isRunning) - adapter.updateConfigList() - true - } - R.id.import_manually_ss -> { - startActivity("position" to -1, "isRunning" to isRunning) - adapter.updateConfigList() - true - } - R.id.import_manually_socks -> { - startActivity("position" to -1, "isRunning" to isRunning) - adapter.updateConfigList() - 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(REQUEST_SCAN_URL) - true - } - -// R.id.sub_setting -> { -// startActivity() -// true -// } - - R.id.sub_update -> { - importConfigViaSub() - true - } - - R.id.export_all -> { - if (AngConfigManager.shareAll2Clipboard() == 0) { - //remove toast, otherwise it will block previous warning message - } else { - toast(R.string.toast_failure) - } - true - } - - R.id.ping_all -> { - for (k in 0 until configs.vmess.count()) { - configs.vmess[k].testResult = "" - adapter.updateConfigList() - } - for (k in 0 until configs.vmess.count()) { - if (configs.vmess[k].configType != AppConfig.EConfigType.Custom) { - doAsync { - configs.vmess[k].testResult = Utils.tcping(configs.vmess[k].address, configs.vmess[k].port) - uiThread { - adapter.updateSelectedItem(k) - } - } - } - } - true - } - -// R.id.settings -> { -// startActivity("isRunning" to isRunning) -// true -// } -// R.id.logcat -> { -// startActivity() -// true -// } - else -> super.onOptionsItemSelected(item) - } - - - /** - * import config from qrcode - */ - fun importQRcode(requestCode: Int): 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) - startActivityForResult(requestCode) - else - toast(R.string.toast_permission_denied) - } -// } - return true - } - - /** - * 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 = "") { - val count = AngConfigManager.importBatchConfig(server, subid) - if (count > 0) { - toast(R.string.toast_success) - adapter.updateConfigList() - } 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 - } - doAsync { - val configText = URL(url).readText() - uiThread { - 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) - val subItem = AngConfigManager.configs.subItem - for (k in 0 until subItem.count()) { - if (TextUtils.isEmpty(subItem[k].id) - || TextUtils.isEmpty(subItem[k].remarks) - || TextUtils.isEmpty(subItem[k].url) - ) { - continue - } - val id = subItem[k].id - val url = subItem[k].url - if (!Utils.isValidUrl(url)) { - continue - } - Log.d("Main", url) - doAsync { - val configText = URL(url).readText() - uiThread { - importBatchConfig(Utils.decode(configText), id) - } - } - } - } 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 { - startActivityForResult( - Intent.createChooser(intent, getString(R.string.title_file_chooser)), - REQUEST_FILE_CHOOSER) - } catch (ex: android.content.ActivityNotFoundException) { - toast(R.string.toast_require_file_manager) - } - } - - /** - * read content from uri - */ - private fun readContentFromUri(uri: Uri) { - RxPermissions(this) - .request(Manifest.permission.READ_EXTERNAL_STORAGE) - .subscribe { - if (it) { - try { - val inputStream = contentResolver.openInputStream(uri) - val configText = inputStream.bufferedReader().readText() - importCustomizeConfig(configText) - } catch (e: Exception) { - e.printStackTrace() - } - } else - toast(R.string.toast_permission_denied) - } - } - - /** - * import customize config - */ - fun importCustomizeConfig(server: String?) { - if (server == null) { - return - } - if (!V2rayConfigUtil.isValidConfig(server)) { - toast(R.string.toast_config_file_invalid) - return - } - val resId = AngConfigManager.importCustomizeConfig(server) - if (resId > 0) { - toast(resId) - } else { - toast(R.string.toast_success) - adapter.updateConfigList() - } - } - -// val mConnection = object : ServiceConnection { -// override fun onServiceDisconnected(name: ComponentName?) { -// } -// -// override fun onServiceConnected(name: ComponentName?, service: IBinder?) { -// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "") -// } -// } - - private - var mMsgReceive: BroadcastReceiver? = null - - private class ReceiveMessageHandler(activity: MainActivity) : BroadcastReceiver() { - internal var mReference: SoftReference = SoftReference(activity) - override fun onReceive(ctx: Context?, intent: Intent?) { - val activity = mReference.get() - when (intent?.getIntExtra("key", 0)) { - AppConfig.MSG_STATE_RUNNING -> { - activity?.isRunning = true - } - AppConfig.MSG_STATE_NOT_RUNNING -> { - activity?.isRunning = false - } - AppConfig.MSG_STATE_START_SUCCESS -> { - activity?.toast(R.string.toast_services_success) - activity?.isRunning = true - } - AppConfig.MSG_STATE_START_FAILURE -> { - activity?.toast(R.string.toast_services_failure) - activity?.isRunning = false - } - AppConfig.MSG_STATE_STOP_SUCCESS -> { - activity?.isRunning = false - } - } - } - } - - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - if (keyCode == KeyEvent.KEYCODE_BACK) { - moveTaskToBack(false) - return true - } - return super.onKeyDown(keyCode, event) - } - - fun showCircle() { - fabProgressCircle?.show() - } - - fun hideCircle() { - try { - Observable.timer(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - if (fabProgressCircle.isShown) { - fabProgressCircle.hide() - } - } - } catch (e: Exception) { - } - } - - override fun onBackPressed() { - if (drawer_layout.isDrawerOpen(GravityCompat.START)) { - drawer_layout.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() - } - R.id.settings -> { - startActivity("isRunning" to isRunning) - } - R.id.feedback -> { - Utils.openUri(this, AppConfig.v2rayNGIssues) - } - R.id.promotion -> { - Utils.openUri(this, AppConfig.promotionUrl) - } - R.id.donate -> { -// startActivity() - } - R.id.logcat -> { - startActivity() - } - } - drawer_layout.closeDrawer(GravityCompat.START) - return true - } -} \ No newline at end of file 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 4ae5aadd..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt +++ /dev/null @@ -1,268 +0,0 @@ -package com.v2ray.ang.ui - -import android.graphics.Color -import android.support.v7.widget.RecyclerView -import android.text.TextUtils -import android.view.View -import android.view.ViewGroup -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.dto.AngConfig -import com.v2ray.ang.helper.ItemTouchHelperAdapter -import com.v2ray.ang.helper.ItemTouchHelperViewHolder -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.item_qrcode.view.* -import kotlinx.android.synthetic.main.item_recycler_main.view.* -import org.jetbrains.anko.* -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit -import com.v2ray.ang.extension.defaultDPreference - -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 lateinit var configs: AngConfig - private val share_method: Array by lazy { - mActivity.resources.getStringArray(R.array.share_method) - } - - var changeable: Boolean = true - set(value) { - if (field == value) - return - field = value - notifyDataSetChanged() - } - - init { - updateConfigList() - } - - override fun getItemCount() = configs.vmess.count() + 1 - - override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { - if (holder is MainViewHolder) { - val configType = configs.vmess[position].configType - val remarks = configs.vmess[position].remarks - val subid = configs.vmess[position].subid - val address = configs.vmess[position].address - val port = configs.vmess[position].port - val test_result = configs.vmess[position].testResult - - holder.name.text = remarks - holder.radio.isChecked = (position == configs.index) - holder.itemView.backgroundColor = Color.TRANSPARENT - holder.test_result.text = test_result - - if (TextUtils.isEmpty(subid)) { - holder.subid.text = "" - } else { - holder.subid.text = "S" - } - - if (configType == AppConfig.EConfigType.Vmess) { - holder.type.text = "vmess" - holder.statistics.text = "$address : $port" - holder.layout_share.visibility = View.VISIBLE - } else if (configType == AppConfig.EConfigType.Custom) { - holder.type.text = mActivity.getString(R.string.server_customize_config) - holder.statistics.text = ""//mActivity.getString(R.string.server_customize_config) - holder.layout_share.visibility = View.INVISIBLE - } else if (configType == AppConfig.EConfigType.Shadowsocks) { - holder.type.text = "shadowsocks" - holder.statistics.text = "$address : $port" - holder.layout_share.visibility = View.VISIBLE - } else if (configType == AppConfig.EConfigType.Socks) { - holder.type.text = "socks" - holder.statistics.text = "$address : $port" - holder.layout_share.visibility = View.VISIBLE - } - - holder.layout_share.setOnClickListener { - mActivity.selector(null, share_method.asList()) { dialogInterface, i -> - try { - when (i) { - 0 -> { - val iv = mActivity.layoutInflater.inflate(R.layout.item_qrcode, null) - iv.iv_qcode.setImageBitmap(AngConfigManager.share2QRCode(position)) - - mActivity.alert { - customView { - linearLayout { - addView(iv) - } - } - }.show() - } - 1 -> { - if (AngConfigManager.share2Clipboard(position) == 0) { - mActivity.toast(R.string.toast_success) - } else { - mActivity.toast(R.string.toast_failure) - } - } - 2 -> { - if (AngConfigManager.shareFullContent2Clipboard(position) == 0) { - mActivity.toast(R.string.toast_success) - } else { - mActivity.toast(R.string.toast_failure) - } - } - else -> - mActivity.toast("else") - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - holder.layout_edit.setOnClickListener { - if (configType == AppConfig.EConfigType.Vmess) { - mActivity.startActivity("position" to position, "isRunning" to !changeable) - } else if (configType == AppConfig.EConfigType.Custom) { - mActivity.startActivity("position" to position, "isRunning" to !changeable) - } else if (configType == AppConfig.EConfigType.Shadowsocks) { - mActivity.startActivity("position" to position, "isRunning" to !changeable) - } else if (configType == AppConfig.EConfigType.Socks) { - mActivity.startActivity("position" to position, "isRunning" to !changeable) - } - } - holder.layout_remove.setOnClickListener { - if (configs.index != position) { - if (AngConfigManager.removeServer(position) == 0) { - notifyItemRemoved(position) - updateSelectedItem(position) - } - } - } - - holder.infoContainer.setOnClickListener { - if (changeable) { - AngConfigManager.setActiveServer(position) - } else { - mActivity.showCircle() - Utils.stopVService(mActivity) - AngConfigManager.setActiveServer(position) - Observable.timer(500, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - mActivity.showCircle() - if (!Utils.startVService(mActivity)) { - mActivity.hideCircle() - } - } - - } - notifyDataSetChanged() - } - } - if (holder is FooterViewHolder) { - //if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) { - if (true) { - holder.layout_edit.visibility = View.INVISIBLE - } else { - holder.layout_edit.setOnClickListener { - Utils.openUri(mActivity, AppConfig.promotionUrl) - } - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { - when (viewType) { - VIEW_TYPE_ITEM -> - return MainViewHolder(parent.context.layoutInflater - .inflate(R.layout.item_recycler_main, parent, false)) - else -> - return FooterViewHolder(parent.context.layoutInflater - .inflate(R.layout.item_recycler_footer, parent, false)) - } - } - - fun updateConfigList() { - configs = AngConfigManager.configs - notifyDataSetChanged() - } - -// fun updateSelectedItem() { -// updateSelectedItem(configs.index) -// } - - fun updateSelectedItem(pos: Int) { - //notifyItemChanged(pos) - notifyItemRangeChanged(pos, itemCount - pos) - } - - override fun getItemViewType(position: Int): Int { - if (position == configs.vmess.count()) { - return VIEW_TYPE_FOOTER - } else { - return VIEW_TYPE_ITEM - } - } - - open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) - - class MainViewHolder(itemView: View) : BaseViewHolder(itemView), ItemTouchHelperViewHolder { - val subid = itemView.tv_subid - val radio = itemView.btn_radio!! - val name = itemView.tv_name!! - val test_result = itemView.tv_test_result!! - val type = itemView.tv_type!! - val statistics = itemView.tv_statistics!! - val infoContainer = itemView.info_container!! - val layout_edit = itemView.layout_edit!! - val layout_share = itemView.layout_share - val layout_remove = itemView.layout_remove!! - - override fun onItemSelected() { - itemView.setBackgroundColor(Color.LTGRAY) - } - - override fun onItemClear() { - itemView.setBackgroundColor(0) - } - } - - class FooterViewHolder(itemView: View) : BaseViewHolder(itemView), ItemTouchHelperViewHolder { - val layout_edit = itemView.layout_edit!! - - override fun onItemSelected() { - itemView.setBackgroundColor(Color.LTGRAY) - } - - override fun onItemClear() { - itemView.setBackgroundColor(0) - } - } - - override fun onItemDismiss(position: Int) { - if (configs.index != position) { -// mActivity.alert(R.string.del_config_comfirm) { -// positiveButton(android.R.string.ok) { - if (AngConfigManager.removeServer(position) == 0) { - notifyItemRemoved(position) - } -// } -// show() -// } - } - updateSelectedItem(position) - } - - override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { - AngConfigManager.swapServer(fromPosition, toPosition) - notifyItemMoved(fromPosition, toPosition) - //notifyDataSetChanged() - updateSelectedItem(if (fromPosition < toPosition) fromPosition else toPosition) - return true - } -} 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 3ba3120c..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/PerAppProxyActivity.kt +++ /dev/null @@ -1,279 +0,0 @@ -package com.v2ray.ang.ui - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Context -import android.os.Bundle -import android.support.v7.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.dinuscxj.itemdecoration.LinearDividerItemDecoration -import com.v2ray.ang.R -import com.v2ray.ang.extension.defaultDPreference -import com.v2ray.ang.util.AppManagerUtil -import kotlinx.android.synthetic.main.activity_bypass_list.* -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.dto.AppInfo -import com.v2ray.ang.extension.v2RayApplication -import com.v2ray.ang.util.Utils -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.toast -import org.jetbrains.anko.uiThread -import java.net.URL - -class PerAppProxyActivity : BaseActivity() { - companion object { - const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set" - const val PREF_BYPASS_APPS = "pref_bypass_apps" - } - - private var adapter: PerAppProxyAdapter? = null - private var appsAll: List? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_bypass_list) - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - val dividerItemDecoration = LinearDividerItemDecoration( - this, LinearDividerItemDecoration.LINEAR_DIVIDER_VERTICAL) - recycler_view.addItemDecoration(dividerItemDecoration) - - val blacklist = defaultDPreference.getPrefStringSet(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 = object : Comparator { - override fun compare(p1: AppInfo, p2: AppInfo): Int = 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) - recycler_view.adapter = adapter - pb_waiting.visibility = View.GONE - } - - recycler_view.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) { - header_view.hide() - dst = 0 - } else if (dst < -20) { - header_view.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 - } - }) - - switch_per_app_proxy.setOnCheckedChangeListener { buttonView, isChecked -> - defaultDPreference.setPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, isChecked) - } - switch_per_app_proxy.isChecked = defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PER_APP_PROXY, false) - - switch_bypass_apps.setOnCheckedChangeListener { buttonView, isChecked -> - defaultDPreference.setPrefBoolean(PREF_BYPASS_APPS, isChecked) - } - switch_bypass_apps.isChecked = defaultDPreference.getPrefBoolean(PREF_BYPASS_APPS, false) - - et_search.setOnEditorActionListener { v, actionId, event -> - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - //hide - var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS) - - val key = v.text.toString().toUpperCase() - val apps = ArrayList() - if (TextUtils.isEmpty(key)) { - appsAll?.forEach { - apps.add(it) - } - } else { - appsAll?.forEach { - if (it.appName.toUpperCase().indexOf(key) >= 0) { - apps.add(it) - } - } - } - adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist) - recycler_view.adapter = adapter - adapter?.notifyDataSetChanged() - true - } else { - false - } - } - } - - override fun onPause() { - super.onPause() - adapter?.let { - defaultDPreference.setPrefStringSet(PREF_PER_APP_PROXY_SET, it.blacklist) - } - } - - 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 - doAsync { - val content = URL(url).readText() - uiThread { - Log.d("selectProxyApp", 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 (switch_bypass_apps.isChecked) { - adapter?.let { - it.apps.forEach block@{ - val packageName = it.packageName - Log.d("selectProxyApp2", 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("selectProxyApp3", 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 - } -} \ No newline at end of file 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 43f5ae7a..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsActivity.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.v2ray.ang.ui - -import android.graphics.Color -import android.os.Bundle -import com.v2ray.ang.R -import android.support.v4.app.Fragment -import com.v2ray.ang.AppConfig -import kotlinx.android.synthetic.main.activity_routing_settings.* - - -class RoutingSettingsActivity : BaseActivity() { - private val titles: Array by lazy { - resources.getStringArray(R.array.routing_tag) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_routing_settings) - - 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(supportFragmentManager, fragments, titles.toList()) - viewpager?.adapter = adapter - tablayout.setTabTextColors(Color.BLACK, Color.RED) - tablayout.setupWithViewPager(viewpager) - } -} 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 492829ed..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/RoutingSettingsFragment.kt +++ /dev/null @@ -1,149 +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.support.v4.app.Fragment -import android.text.TextUtils -import android.util.Log -import android.view.* -import com.v2ray.ang.R -import com.v2ray.ang.extension.defaultDPreference -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.fragment_routing_settings.* -import org.jetbrains.anko.toast -import android.view.MenuInflater -import com.tbruyelle.rxpermissions.RxPermissions -import com.v2ray.ang.AppConfig -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.startActivityForResult -import org.jetbrains.anko.support.v4.startActivityForResult -import org.jetbrains.anko.support.v4.toast -import org.jetbrains.anko.uiThread -import java.net.URL - - -class RoutingSettingsFragment : Fragment() { - companion object { - private const val routing_arg = "routing_arg" - private const val REQUEST_SCAN_REPLACE = 11 - private const val REQUEST_SCAN_APPEND = 12 - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - return 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 onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - val content = activity?.defaultDPreference?.getPrefString(arguments!!.getString(routing_arg), "") - et_routing_content.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 = et_routing_content.text.toString() - activity?.defaultDPreference?.setPrefString(arguments!!.getString(routing_arg), content) - activity?.toast(R.string.toast_success) - true - } - R.id.del_routing -> { - et_routing_content.text = null - true - } - R.id.scan_replace -> { - scanQRcode(REQUEST_SCAN_REPLACE) - true - } - R.id.scan_append -> { - scanQRcode(REQUEST_SCAN_APPEND) - true - } - R.id.default_rules -> { - setDefaultRules() - true - } - else -> super.onOptionsItemSelected(item) - } - - fun scanQRcode(requestCode: Int): 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(activity!!) - .request(Manifest.permission.CAMERA) - .subscribe { - if (it) - startActivityForResult(requestCode) - else - activity?.toast(R.string.toast_permission_denied) - } -// } - return true - } - - fun setDefaultRules(): Boolean { - var url = AppConfig.v2rayCustomRoutingListUrl - when (arguments!!.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 - } - } - - toast(R.string.msg_downloading_content) - doAsync { - val content = URL(url).readText() - uiThread { - et_routing_content.text = Utils.getEditable(content!!) - toast(R.string.toast_success) - } - } - return true - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - REQUEST_SCAN_REPLACE -> - if (resultCode == RESULT_OK) { - val content = data?.getStringExtra("SCAN_RESULT") - et_routing_content.text = Utils.getEditable(content!!) - } - REQUEST_SCAN_APPEND -> - if (resultCode == RESULT_OK) { - val content = data?.getStringExtra("SCAN_RESULT") - et_routing_content.text = Utils.getEditable("${et_routing_content.text},$content") - } - } - } - - -} 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 b3cd04bb..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScScannerActivity.kt +++ /dev/null @@ -1,52 +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 org.jetbrains.anko.* - -class ScScannerActivity : BaseActivity() { - companion object { - private const val REQUEST_SCAN = 1 - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_none) - importQRcode(REQUEST_SCAN) - } - - fun importQRcode(requestCode: Int): Boolean { - RxPermissions(this) - .request(Manifest.permission.CAMERA) - .subscribe { - if (it) - startActivityForResult(requestCode) - else - toast(R.string.toast_permission_denied) - } - - return true - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - REQUEST_SCAN -> - if (resultCode == RESULT_OK) { - val count = AngConfigManager.importBatchConfig(data?.getStringExtra("SCAN_RESULT"), "") - if (count > 0) { - toast(R.string.toast_success) - } else { - toast(R.string.toast_failure) - } - startActivity() - } - } - 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/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt deleted file mode 100644 index 5b023055..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScSwitchActivity.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.* -import android.net.VpnService -import com.v2ray.ang.R -import com.v2ray.ang.util.Utils -import android.os.Bundle -import com.v2ray.ang.AppConfig -import com.v2ray.ang.util.MessageUtil -import java.lang.ref.SoftReference -import android.content.IntentFilter -import kotlinx.android.synthetic.main.activity_main.* -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit - -class ScSwitchActivity : BaseActivity() { - companion object { - private const val REQUEST_CODE_VPN_PREPARE = 0 - } - - var isRunning = false - set(value) { - field = value - if (value) { - Utils.stopVService(this) - } else { - val intent = VpnService.prepare(this) - if (intent == null) { - Utils.startVService(this) - } else { - startActivityForResult(intent, REQUEST_CODE_VPN_PREPARE) - } - } - finishActivity() - } - - fun finishActivity() { - try { - Observable.timer(5000, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - finish() - } - } catch (e: Exception) { - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - moveTaskToBack(true) - - setContentView(R.layout.activity_none) - - val isRunning = Utils.isServiceRun(this, "com.v2ray.ang.service.V2RayVpnService") - if (isRunning) { - //Utils.stopVService(this) - mMsgReceive = ReceiveMessageHandler(this@ScSwitchActivity) - registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) - MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") - - } else { - Utils.startVService(this) - finishActivity() - } - } - - override fun onStop() { - super.onStop() - if (mMsgReceive != null) { - unregisterReceiver(mMsgReceive) - mMsgReceive = null - } - } - - private var mMsgReceive: BroadcastReceiver? = null - - private class ReceiveMessageHandler(activity: ScSwitchActivity) : BroadcastReceiver() { - internal var mReference: SoftReference = SoftReference(activity) - override fun onReceive(ctx: Context?, intent: Intent?) { - val activity = mReference.get() - when (intent?.getIntExtra("key", 0)) { - AppConfig.MSG_STATE_RUNNING -> { - activity?.isRunning = true - } - AppConfig.MSG_STATE_NOT_RUNNING -> { - activity?.isRunning = false - } -// AppConfig.MSG_STATE_START_SUCCESS -> { -// activity?.toast(R.string.toast_services_success) -// activity?.isRunning = true -// } -// AppConfig.MSG_STATE_START_FAILURE -> { -// activity?.toast(R.string.toast_services_failure) -// activity?.isRunning = false -// } -// AppConfig.MSG_STATE_STOP_SUCCESS -> { -// activity?.isRunning = false -// } - } - } - } - -} \ No newline at end of file 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 5e019309..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ScannerActivity.kt +++ /dev/null @@ -1,136 +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.icu.util.TimeUnit -import android.view.Menu -import android.view.MenuItem -import com.google.zxing.BarcodeFormat -import com.tbruyelle.rxpermissions.RxPermissions -import com.v2ray.ang.R -import com.v2ray.ang.util.QRCodeDecoder -import org.jetbrains.anko.toast -import rx.Observable -import android.os.SystemClock -import kotlinx.android.synthetic.main.activity_main.* -import rx.Observer -import rx.android.schedulers.AndroidSchedulers -import javax.xml.datatype.DatatypeConstants.SECONDS - - - - -class ScannerActivity : BaseActivity(), ZXingScannerView.ResultHandler { - companion object { - private const val REQUEST_FILE_CHOOSER = 2 - } - - - private var mScannerView: ZXingScannerView? = null - - public override fun onCreate(state: Bundle?) { - super.onCreate(state) - 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) - } - - 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 { - startActivityForResult( - Intent.createChooser(intent, getString(R.string.title_file_chooser)), - REQUEST_FILE_CHOOSER) - } catch (ex: android.content.ActivityNotFoundException) { - toast(R.string.toast_require_file_manager) - } - } - - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - REQUEST_FILE_CHOOSER -> - if (resultCode == RESULT_OK) { - try { - val uri = data!!.data - 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/Server2Activity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server2Activity.kt deleted file mode 100644 index bdfba4c2..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server2Activity.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.text.Editable -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import com.google.gson.Gson -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.extension.defaultDPreference -import com.v2ray.ang.dto.AngConfig -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.activity_server2.* -import org.jetbrains.anko.* -import java.lang.Exception - - -class Server2Activity : BaseActivity() { - companion object { - private const val REQUEST_SCAN = 1 - } - - var del_config: MenuItem? = null - var save_config: MenuItem? = null - - private lateinit var configs: AngConfig - private var edit_index: Int = -1 //当前编辑的服务器 - private var edit_guid: String = "" - private var isRunning: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_server2) - - configs = AngConfigManager.configs - edit_index = intent.getIntExtra("position", -1) - isRunning = intent.getBooleanExtra("isRunning", false) - title = getString(R.string.title_server) - - if (edit_index >= 0) { - edit_guid = configs.vmess[edit_index].guid - bindingServer(configs.vmess[edit_index]) - } else { - clearServer() - } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - /** - * bingding seleced server config - */ - fun bindingServer(vmess: AngConfig.VmessBean): Boolean { - et_remarks.text = Utils.getEditable(vmess.remarks) - tv_content.text = Editable.Factory.getInstance().newEditable(defaultDPreference.getPrefString(AppConfig.ANG_CONFIG + edit_guid, "")) - return true - } - - /** - * clear or init server config - */ - fun clearServer(): Boolean { - et_remarks.text = null - return true - } - - /** - * save server config - */ - fun saveServer(): Boolean { - var saveSuccess: Boolean - val vmess = configs.vmess[edit_index] - - vmess.remarks = et_remarks.text.toString() - - if (TextUtils.isEmpty(vmess.remarks)) { - toast(R.string.server_lab_remarks) - saveSuccess = false - } - - - if (AngConfigManager.addCustomServer(vmess, edit_index) == 0) { - toast(R.string.toast_success) - saveSuccess = true - } else { - toast(R.string.toast_failure) - saveSuccess = false - } - - - try { - Gson().fromJson(tv_content.text.toString(), Object::class.java) - } catch (e: Exception) { - e.printStackTrace() - toast(R.string.toast_malformed_josn) - saveSuccess = false - } - - if (saveSuccess) { - //update config - defaultDPreference.setPrefString(AppConfig.ANG_CONFIG + edit_guid, tv_content.text.toString()) - finish() - return true - } else { - return false - } - } - - /** - * save server config - */ - fun deleteServer(): Boolean { - if (edit_index >= 0) { - alert(R.string.del_config_comfirm) { - positiveButton(android.R.string.ok) { - if (AngConfigManager.removeServer(edit_index) == 0) { - toast(R.string.toast_success) - finish() - } else { - toast(R.string.toast_failure) - } - } - show() - } - } else { - } - 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 (edit_index >= 0) { - if (isRunning) { - if (edit_index == configs.index) { - del_config?.isVisible = false - save_config?.isVisible = false - } - } - } else { - 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/kotlin/com/v2ray/ang/ui/Server3Activity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server3Activity.kt deleted file mode 100644 index 92ee6e2f..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server3Activity.kt +++ /dev/null @@ -1,175 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import com.v2ray.ang.R -import com.v2ray.ang.dto.AngConfig -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.activity_server3.* -import org.jetbrains.anko.* - - -class Server3Activity : BaseActivity() { - companion object { - private const val REQUEST_SCAN = 1 - } - - var del_config: MenuItem? = null - var save_config: MenuItem? = null - - private lateinit var configs: AngConfig - private var edit_index: Int = -1 //当前编辑的服务器 - private var edit_guid: String = "" - private var isRunning: Boolean = false - private val securitys: Array by lazy { - resources.getStringArray(R.array.ss_securitys) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_server3) - - configs = AngConfigManager.configs - edit_index = intent.getIntExtra("position", -1) - isRunning = intent.getBooleanExtra("isRunning", false) - title = getString(R.string.title_server) - - if (edit_index >= 0) { - edit_guid = configs.vmess[edit_index].guid - bindingServer(configs.vmess[edit_index]) - } else { - clearServer() - } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - /** - * bingding seleced server config - */ - fun bindingServer(vmess: AngConfig.VmessBean): Boolean { - et_remarks.text = Utils.getEditable(vmess.remarks) - - et_address.text = Utils.getEditable(vmess.address) - et_port.text = Utils.getEditable(vmess.port.toString()) - et_id.text = Utils.getEditable(vmess.id) - val security = Utils.arrayFind(securitys, vmess.security) - if (security >= 0) { - sp_security.setSelection(security) - } - - return true - } - - /** - * clear or init server config - */ - fun clearServer(): Boolean { - et_remarks.text = null - et_address.text = null - et_port.text = Utils.getEditable("10086") - et_id.text = null - sp_security.setSelection(0) - - return true - } - - /** - * save server config - */ - fun saveServer(): Boolean { - val vmess: AngConfig.VmessBean - if (edit_index >= 0) { - vmess = configs.vmess[edit_index] - } else { - vmess = AngConfig.VmessBean() - } - - vmess.guid = edit_guid - vmess.remarks = et_remarks.text.toString() - vmess.address = et_address.text.toString() - vmess.port = Utils.parseInt(et_port.text.toString()) - vmess.id = et_id.text.toString() - vmess.security = securitys[sp_security.selectedItemPosition] - - if (TextUtils.isEmpty(vmess.remarks)) { - toast(R.string.server_lab_remarks) - return false - } - if (TextUtils.isEmpty(vmess.address)) { - toast(R.string.server_lab_address3) - return false - } - if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) { - toast(R.string.server_lab_port3) - return false - } - if (TextUtils.isEmpty(vmess.id)) { - toast(R.string.server_lab_id3) - return false - } - - if (AngConfigManager.addShadowsocksServer(vmess, edit_index) == 0) { - toast(R.string.toast_success) - finish() - return true - } else { - toast(R.string.toast_failure) - return false - } - } - - /** - * save server config - */ - fun deleteServer(): Boolean { - if (edit_index >= 0) { - alert(R.string.del_config_comfirm) { - positiveButton(android.R.string.ok) { - if (AngConfigManager.removeServer(edit_index) == 0) { - toast(R.string.toast_success) - finish() - } else { - toast(R.string.toast_failure) - } - } - show() - } - } else { - } - 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 (edit_index >= 0) { - if (isRunning) { - if (edit_index == configs.index) { - del_config?.isVisible = false - save_config?.isVisible = false - } - } - } else { - 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/kotlin/com/v2ray/ang/ui/Server4Activity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server4Activity.kt deleted file mode 100644 index 97fb4d63..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/Server4Activity.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import com.v2ray.ang.R -import com.v2ray.ang.dto.AngConfig -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.activity_server4.* -import org.jetbrains.anko.* - - -class Server4Activity : BaseActivity() { - companion object { - private const val REQUEST_SCAN = 1 - } - - var del_config: MenuItem? = null - var save_config: MenuItem? = null - - private lateinit var configs: AngConfig - private var edit_index: Int = -1 //当前编辑的服务器 - private var edit_guid: String = "" - private var isRunning: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_server4) - - configs = AngConfigManager.configs - edit_index = intent.getIntExtra("position", -1) - isRunning = intent.getBooleanExtra("isRunning", false) - title = getString(R.string.title_server) - - if (edit_index >= 0) { - edit_guid = configs.vmess[edit_index].guid - bindingServer(configs.vmess[edit_index]) - } else { - clearServer() - } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - /** - * bingding seleced server config - */ - fun bindingServer(vmess: AngConfig.VmessBean): Boolean { - et_remarks.text = Utils.getEditable(vmess.remarks) - - et_address.text = Utils.getEditable(vmess.address) - et_port.text = Utils.getEditable(vmess.port.toString()) - - return true - } - - /** - * clear or init server config - */ - fun clearServer(): Boolean { - et_remarks.text = null - et_address.text = null - et_port.text = Utils.getEditable("10086") - - return true - } - - /** - * save server config - */ - fun saveServer(): Boolean { - val vmess: AngConfig.VmessBean - if (edit_index >= 0) { - vmess = configs.vmess[edit_index] - } else { - vmess = AngConfig.VmessBean() - } - - vmess.guid = edit_guid - vmess.remarks = et_remarks.text.toString() - vmess.address = et_address.text.toString() - vmess.port = Utils.parseInt(et_port.text.toString()) - - if (TextUtils.isEmpty(vmess.remarks)) { - toast(R.string.server_lab_remarks) - return false - } - if (TextUtils.isEmpty(vmess.address)) { - toast(R.string.server_lab_address3) - return false - } - if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) { - toast(R.string.server_lab_port3) - return false - } - - if (AngConfigManager.addSocksServer(vmess, edit_index) == 0) { - toast(R.string.toast_success) - finish() - return true - } else { - toast(R.string.toast_failure) - return false - } - } - - /** - * save server config - */ - fun deleteServer(): Boolean { - if (edit_index >= 0) { - alert(R.string.del_config_comfirm) { - positiveButton(android.R.string.ok) { - if (AngConfigManager.removeServer(edit_index) == 0) { - toast(R.string.toast_success) - finish() - } else { - toast(R.string.toast_failure) - } - } - show() - } - } else { - } - 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 (edit_index >= 0) { - if (isRunning) { - if (edit_index == configs.index) { - del_config?.isVisible = false - save_config?.isVisible = false - } - } - } else { - 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/kotlin/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt deleted file mode 100644 index d2a14035..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/ServerActivity.kt +++ /dev/null @@ -1,218 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import com.v2ray.ang.R -import com.v2ray.ang.dto.AngConfig -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.activity_server.* -import org.jetbrains.anko.* - - -class ServerActivity : BaseActivity() { - companion object { - private const val REQUEST_SCAN = 1 - } - - var del_config: MenuItem? = null - var save_config: MenuItem? = null - - private lateinit var configs: AngConfig - private var edit_index: Int = -1 //当前编辑的服务器 - private var edit_guid: String = "" - private var isRunning: Boolean = false - private val securitys: Array by lazy { - resources.getStringArray(R.array.securitys) - } - private val networks: Array by lazy { - resources.getStringArray(R.array.networks) - } - private val headertypes: Array by lazy { - resources.getStringArray(R.array.headertypes) - } - private val streamsecuritys: Array by lazy { - resources.getStringArray(R.array.streamsecuritys) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_server) - - configs = AngConfigManager.configs - edit_index = intent.getIntExtra("position", -1) - isRunning = intent.getBooleanExtra("isRunning", false) - title = getString(R.string.title_server) - - if (edit_index >= 0) { - edit_guid = configs.vmess[edit_index].guid - bindingServer(configs.vmess[edit_index]) - } else { - clearServer() - } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - /** - * bingding seleced server config - */ - fun bindingServer(vmess: AngConfig.VmessBean): Boolean { - et_remarks.text = Utils.getEditable(vmess.remarks) - - et_address.text = Utils.getEditable(vmess.address) - et_port.text = Utils.getEditable(vmess.port.toString()) - et_id.text = Utils.getEditable(vmess.id) - et_alterId.text = Utils.getEditable(vmess.alterId.toString()) - - val security = Utils.arrayFind(securitys, vmess.security) - if (security >= 0) { - sp_security.setSelection(security) - } - val network = Utils.arrayFind(networks, vmess.network) - if (network >= 0) { - sp_network.setSelection(network) - } - - val headerType = Utils.arrayFind(headertypes, vmess.headerType) - if (headerType >= 0) { - sp_header_type.setSelection(headerType) - } - et_request_host.text = Utils.getEditable(vmess.requestHost) - et_path.text = Utils.getEditable(vmess.path) - - val streamSecurity = Utils.arrayFind(streamsecuritys, vmess.streamSecurity) - if (streamSecurity >= 0) { - sp_stream_security.setSelection(streamSecurity) - } - return true - } - - /** - * clear or init server config - */ - fun clearServer(): Boolean { - et_remarks.text = null - et_address.text = null - et_port.text = Utils.getEditable("10086") - et_id.text = null - et_alterId.text = Utils.getEditable("64") - 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) - return true - } - - /** - * save server config - */ - fun saveServer(): Boolean { - val vmess: AngConfig.VmessBean - if (edit_index >= 0) { - vmess = configs.vmess[edit_index] - } else { - vmess = AngConfig.VmessBean() - } - - vmess.guid = edit_guid - vmess.remarks = et_remarks.text.toString() - vmess.address = et_address.text.toString() - vmess.port = Utils.parseInt(et_port.text.toString()) - vmess.id = et_id.text.toString() - vmess.alterId = Utils.parseInt(et_alterId.text.toString()) - vmess.security = securitys[sp_security.selectedItemPosition] - vmess.network = networks[sp_network.selectedItemPosition] - - vmess.headerType = headertypes[sp_header_type.selectedItemPosition] - vmess.requestHost = et_request_host.text.toString() - vmess.path = et_path.text.toString() - vmess.streamSecurity = streamsecuritys[sp_stream_security.selectedItemPosition] - - if (TextUtils.isEmpty(vmess.remarks)) { - toast(R.string.server_lab_remarks) - return false - } - if (TextUtils.isEmpty(vmess.address)) { - toast(R.string.server_lab_address) - return false - } - if (TextUtils.isEmpty(vmess.port.toString()) || vmess.port <= 0) { - toast(R.string.server_lab_port) - return false - } - if (TextUtils.isEmpty(vmess.id)) { - toast(R.string.server_lab_id) - return false - } - if (TextUtils.isEmpty(vmess.alterId.toString()) || vmess.alterId < 0) { - toast(R.string.server_lab_alterid) - return false - } - - if (AngConfigManager.addServer(vmess, edit_index) == 0) { - toast(R.string.toast_success) - finish() - return true - } else { - toast(R.string.toast_failure) - return false - } - } - - /** - * save server config - */ - fun deleteServer(): Boolean { - if (edit_index >= 0) { - alert(R.string.del_config_comfirm) { - positiveButton(android.R.string.ok) { - if (AngConfigManager.removeServer(edit_index) == 0) { - toast(R.string.toast_success) - finish() - } else { - toast(R.string.toast_failure) - } - } - show() - } - } else { - } - 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 (edit_index >= 0) { - if (isRunning) { - if (edit_index == configs.index) { - del_config?.isVisible = false - save_config?.isVisible = false - } - } - } else { - 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/kotlin/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt deleted file mode 100644 index f83ae661..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SettingsActivity.kt +++ /dev/null @@ -1,250 +0,0 @@ -package com.v2ray.ang.ui - -import android.content.Intent -import android.content.SharedPreferences -import android.net.Uri -import android.os.Bundle -import android.preference.* -import com.v2ray.ang.AngApplication -import com.v2ray.ang.BuildConfig -//import com.v2ray.ang.InappBuyActivity -import com.v2ray.ang.R -import com.v2ray.ang.AppConfig -import com.v2ray.ang.extension.defaultDPreference -import com.v2ray.ang.extension.onClick -import com.v2ray.ang.util.Utils -import org.jetbrains.anko.act -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.startActivity -import org.jetbrains.anko.toast -import libv2ray.Libv2ray - -class SettingsActivity : BaseActivity() { - companion object { - // const val PREF_BYPASS_MAINLAND = "pref_bypass_mainland" - // const val PREF_START_ON_BOOT = "pref_start_on_boot" - const val PREF_PER_APP_PROXY = "pref_per_app_proxy" -// const val PREF_MUX_ENAimport libv2ray.Libv2rayBLED = "pref_mux_enabled" - 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_REMOTE_DNS = "pref_remote_dns" - const val PREF_DOMESTIC_DNS = "pref_domestic_dns" - -// const val PREF_SOCKS_PORT = "pref_socks_port" -// const val PREF_HTTP_PORT = "pref_http_port" - - const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" - const val PREF_ROUTING_MODE = "pref_routing_mode" - const val PREF_ROUTING_CUSTOM = "pref_routing_custom" -// 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_VERSION = "pref_version" - // const val PREF_AUTO_RESTART = "pref_auto_restart" - const val PREF_FORWARD_IPV6 = "pref_forward_ipv6" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) - - title = getString(R.string.title_settings) - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - class SettingsFragment : PreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { - val perAppProxy by lazy { findPreference(PREF_PER_APP_PROXY) as CheckBoxPreference } - val sppedEnabled by lazy { findPreference(PREF_SPEED_ENABLED) as CheckBoxPreference } - val sniffingEnabled by lazy { findPreference(PREF_SNIFFING_ENABLED) as CheckBoxPreference } - val proxySharing by lazy { findPreference(PREF_PROXY_SHARING) as CheckBoxPreference } - val domainStrategy by lazy { findPreference(PREF_ROUTING_DOMAIN_STRATEGY) as ListPreference } - val routingMode by lazy { findPreference(PREF_ROUTING_MODE) as ListPreference } - - val forwardIpv6 by lazy { findPreference(PREF_FORWARD_IPV6) as CheckBoxPreference } - val enableLocalDns by lazy { findPreference(PREF_LOCAL_DNS_ENABLED) as CheckBoxPreference } - val domesticDns by lazy { findPreference(PREF_DOMESTIC_DNS) as EditTextPreference } - val remoteDns by lazy { findPreference(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 } - - val routingCustom: Preference by lazy { findPreference(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) } - val version: Preference by lazy { findPreference(PREF_VERSION) } - - private fun restartProxy() { - Utils.stopVService(activity) - Utils.startVService(activity) - } - - private fun isRunning(): Boolean { - return Utils.isServiceRun(activity, "com.v2ray.ang.service.V2RayVpnService") - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - addPreferencesFromResource(R.xml.pref_settings) - var app = activity.application as AngApplication - - perAppProxy.setOnPreferenceClickListener { - if (isRunning()) { - Utils.stopVService(activity) - } - startActivity() - perAppProxy.isChecked = true - true - } - sppedEnabled.setOnPreferenceClickListener { - if (isRunning()) - restartProxy() - true - } - sniffingEnabled.setOnPreferenceClickListener { - if (isRunning()) - restartProxy() - true - } - - proxySharing.setOnPreferenceClickListener { - if (proxySharing.isChecked) - toast(R.string.toast_warning_pref_proxysharing) - if (isRunning()) - restartProxy() - true - } - - domainStrategy.setOnPreferenceChangeListener { _, _ -> - if (isRunning()) - restartProxy() - true - } - routingMode.setOnPreferenceChangeListener { _, _ -> - if (isRunning()) - restartProxy() - true - } - - routingCustom.onClick { - if (isRunning()) - Utils.stopVService(activity) - startActivity() - } - - forwardIpv6.setOnPreferenceClickListener { - if (isRunning()) - restartProxy() - true - } - - enableLocalDns.setOnPreferenceClickListener { - if (isRunning()) - restartProxy() - true - } - - - domesticDns.setOnPreferenceChangeListener { preference, 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 { preference, any -> - // remoteDns.summary = any as String - val nval = any as String - remoteDns.summary = if (nval == "") AppConfig.DNS_AGENT else nval - if (isRunning()) - restartProxy() - true - } - -// 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 -// } - - version.summary = "${BuildConfig.VERSION_NAME} (${Libv2ray.checkVersionX()})" - } - - override fun onStart() { - super.onStart() - - perAppProxy.isChecked = defaultSharedPreferences.getBoolean(PREF_PER_APP_PROXY, false) - remoteDns.summary = defaultSharedPreferences.getString(PREF_REMOTE_DNS, "") - domesticDns.summary = defaultSharedPreferences.getString(PREF_DOMESTIC_DNS, "") - - if (remoteDns.summary == "") { - remoteDns.summary = AppConfig.DNS_AGENT - } - - if ( domesticDns.summary == "") { - domesticDns.summary = AppConfig.DNS_DIRECT - } - -// socksPort.summary = defaultSharedPreferences.getString(PREF_SOCKS_PORT, "10808") -// lanconnPort.summary = defaultSharedPreferences.getString(PREF_HTTP_PORT, "") - - defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this) - } - - override fun onStop() { - super.onStop() - defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this) - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { - when (key) { -// PREF_AUTO_RESTART -> -// act.defaultDPreference.setPrefBoolean(key, sharedPreferences.getBoolean(key, false)) - - PREF_PER_APP_PROXY -> - act.defaultDPreference.setPrefBoolean(key, sharedPreferences.getBoolean(key, false)) - } - } - } - -} \ No newline at end of file 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 d8cb7d2e..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubEditActivity.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.v2ray.ang.ui - -import android.os.Bundle -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import com.v2ray.ang.R -import com.v2ray.ang.dto.AngConfig -import com.v2ray.ang.util.AngConfigManager -import com.v2ray.ang.util.Utils -import kotlinx.android.synthetic.main.activity_sub_edit.* -import org.jetbrains.anko.* - - -class SubEditActivity : BaseActivity() { - - var del_config: MenuItem? = null - var save_config: MenuItem? = null - - private lateinit var configs: AngConfig - private var edit_index: Int = -1 //当前编辑的 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_sub_edit) - - configs = AngConfigManager.configs - edit_index = intent.getIntExtra("position", -1) - - title = getString(R.string.title_sub_setting) - - if (edit_index >= 0) { - bindingServer(configs.subItem[edit_index]) - } else { - clearServer() - } - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - /** - * bingding seleced server config - */ - fun bindingServer(subItem: AngConfig.SubItemBean): Boolean { - et_remarks.text = Utils.getEditable(subItem.remarks) - et_url.text = Utils.getEditable(subItem.url) - - return true - } - - /** - * clear or init server config - */ - fun clearServer(): Boolean { - et_remarks.text = null - et_url.text = null - - return true - } - - /** - * save server config - */ - fun saveServer(): Boolean { - val subItem: AngConfig.SubItemBean - if (edit_index >= 0) { - subItem = configs.subItem[edit_index] - } else { - subItem = AngConfig.SubItemBean() - } - - subItem.remarks = et_remarks.text.toString() - subItem.url = et_url.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 - } - - if (AngConfigManager.addSubItem(subItem, edit_index) == 0) { - toast(R.string.toast_success) - finish() - return true - } else { - toast(R.string.toast_failure) - return false - } - } - - /** - * save server config - */ - fun deleteServer(): Boolean { - if (edit_index >= 0) { - alert(R.string.del_config_comfirm) { - positiveButton(android.R.string.ok) { - if (AngConfigManager.removeSubItem(edit_index) == 0) { - toast(R.string.toast_success) - finish() - } else { - toast(R.string.toast_failure) - } - } - show() - } - } else { - } - 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 (edit_index >= 0) { - } else { - 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/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt deleted file mode 100644 index 709ef38b..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingActivity.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.v2ray.ang.ui - -import android.support.v7.widget.LinearLayoutManager -import android.view.Menu -import android.view.MenuItem -import com.v2ray.ang.R -import kotlinx.android.synthetic.main.activity_sub_setting.* -import android.os.Bundle -import org.jetbrains.anko.startActivity - -class SubSettingActivity : BaseActivity() { - - private val adapter by lazy { SubSettingRecyclerAdapter(this) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_sub_setting) - - title = getString(R.string.title_sub_setting) - - recycler_view.setHasFixedSize(true) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.adapter = adapter - - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - - override fun onResume() { - super.onResume() - adapter.updateConfigList() - } - - 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("position" to -1) - adapter.updateConfigList() - true - } - else -> super.onOptionsItemSelected(item) - } - - -} \ No newline at end of file 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 f8cd8824..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.v2ray.ang.ui - -import android.graphics.Color -import android.support.v7.widget.RecyclerView -import android.view.View -import android.view.ViewGroup -import com.v2ray.ang.R -import com.v2ray.ang.dto.AngConfig -import com.v2ray.ang.util.AngConfigManager -import kotlinx.android.synthetic.main.item_recycler_sub_setting.view.* -import org.jetbrains.anko.* - -class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter() { - - private var mActivity: SubSettingActivity = activity - private lateinit var configs: AngConfig - - init { - updateConfigList() - } - - override fun getItemCount() = configs.subItem.count() - - override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { - if (holder is MainViewHolder) { - val remarks = configs.subItem[position].remarks - val url = configs.subItem[position].url - - holder.name.text = remarks - holder.url.text = url - holder.itemView.backgroundColor = Color.TRANSPARENT - - holder.layout_edit.setOnClickListener { - mActivity.startActivity("position" to position) - } - } else { - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { - return MainViewHolder(parent.context.layoutInflater - .inflate(R.layout.item_recycler_sub_setting, parent, false)) - } - - fun updateConfigList() { - configs = AngConfigManager.configs - notifyDataSetChanged() - } - -// fun updateSelectedItem() { -// notifyItemChanged(configs.index) -// } - - open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) - - class MainViewHolder(itemView: View) : BaseViewHolder(itemView) { - val name = itemView.tv_name!! - val url = itemView.tv_url!! - val layout_edit = itemView.layout_edit!! - } - -} 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 217e3573..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/AngConfigManager.kt +++ /dev/null @@ -1,826 +0,0 @@ -package com.v2ray.ang.util - -import android.graphics.Bitmap -import android.text.TextUtils -import android.util.Log -import com.google.gson.Gson -import com.v2ray.ang.AngApplication -import com.v2ray.ang.AppConfig -import com.v2ray.ang.AppConfig.ANG_CONFIG -import com.v2ray.ang.AppConfig.PREF_CURR_CONFIG -import com.v2ray.ang.AppConfig.PREF_CURR_CONFIG_GUID -import com.v2ray.ang.AppConfig.PREF_CURR_CONFIG_NAME -import com.v2ray.ang.AppConfig.SOCKS_PROTOCOL -import com.v2ray.ang.AppConfig.SS_PROTOCOL -import com.v2ray.ang.AppConfig.VMESS_PROTOCOL -import com.v2ray.ang.R -import com.v2ray.ang.dto.AngConfig -import com.v2ray.ang.dto.VmessQRCode -import com.v2ray.ang.extension.defaultDPreference -import org.jetbrains.anko.toast -import java.net.URLDecoder -import java.util.* -import java.net.* -import java.math.BigInteger - -object AngConfigManager { - private lateinit var app: AngApplication - private lateinit var angConfig: AngConfig - val configs: AngConfig get() = angConfig - - fun inject(app: AngApplication) { - this.app = app - if (app.firstRun) { - } - loadConfig() - } - - /** - * loading config - */ - fun loadConfig() { - try { - val context = app.defaultDPreference.getPrefString(ANG_CONFIG, "") - if (!TextUtils.isEmpty(context)) { - angConfig = Gson().fromJson(context, AngConfig::class.java) - } else { - angConfig = AngConfig(0, vmess = arrayListOf(AngConfig.VmessBean()), subItem = arrayListOf(AngConfig.SubItemBean())) - angConfig.index = -1 - angConfig.vmess.clear() - angConfig.subItem.clear() - } - - for (i in angConfig.vmess.indices) { - upgradeServerVersion(angConfig.vmess[i]) - } - - if (configs.subItem == null) { - configs.subItem = arrayListOf(AngConfig.SubItemBean()) - } - - } catch (e: Exception) { - e.printStackTrace() - } - } - - /** - * add or edit server - */ - fun addServer(vmess: AngConfig.VmessBean, index: Int): Int { - try { - vmess.configVersion = 2 - vmess.configType = AppConfig.EConfigType.Vmess - - if (index >= 0) { - //edit - angConfig.vmess[index] = vmess - } else { - //add - vmess.guid = Utils.getUuid() - angConfig.vmess.add(vmess) - if (angConfig.vmess.count() == 1) { - angConfig.index = 0 - } - } - - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * 移除服务器 - */ - fun removeServer(index: Int): Int { - try { - if (index < 0 || index > angConfig.vmess.count() - 1) { - return -1 - } - - //删除 - angConfig.vmess.removeAt(index) - - //移除的是活动的 - if (angConfig.index == index) { - if (angConfig.vmess.count() > 0) { - angConfig.index = 0 - } else { - angConfig.index = -1 - } - } else if (index < angConfig.index)//移除活动之前的 - { - angConfig.index-- - } - - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - fun swapServer(fromPosition: Int, toPosition: Int): Int { - try { - Collections.swap(angConfig.vmess, fromPosition, toPosition) - - val index = angConfig.index - if (index == fromPosition) { - angConfig.index = toPosition - } else if (index == toPosition) { - angConfig.index = fromPosition - } - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * set active server - */ - fun setActiveServer(index: Int): Int { - try { - if (index < 0 || index > angConfig.vmess.count() - 1) { - app.curIndex = -1 - return -1 - } - angConfig.index = index - app.curIndex = index - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - app.curIndex = -1 - return -1 - } - return 0 - } - - /** - * store config to file - */ - fun storeConfigFile() { - try { - val conf = Gson().toJson(angConfig) - app.defaultDPreference.setPrefString(ANG_CONFIG, conf) - } catch (e: Exception) { - e.printStackTrace() - } - } - - /** - * gen and store v2ray config file - */ - fun genStoreV2rayConfig(index: Int): Boolean { - try { - if (angConfig.index < 0 - || angConfig.vmess.count() <= 0 - || angConfig.index > angConfig.vmess.count() - 1 - ) { - return false - } - var index2 = angConfig.index - if (index >= 0) { - index2 = index - } - - val result = V2rayConfigUtil.getV2rayConfig(app, angConfig.vmess[index2]) - if (result.status) { - app.defaultDPreference.setPrefString(PREF_CURR_CONFIG, result.content) - app.defaultDPreference.setPrefString(PREF_CURR_CONFIG_GUID, currConfigGuid()) - app.defaultDPreference.setPrefString(PREF_CURR_CONFIG_NAME, currConfigName()) - return true - } else { - return false - } - } catch (e: Exception) { - e.printStackTrace() - return false - } - } - - fun currGeneratedV2rayConfig(): String { - return app.defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "") - } - - fun currConfigType(): Int { - if (angConfig.index < 0 - || angConfig.vmess.count() <= 0 - || angConfig.index > angConfig.vmess.count() - 1 - ) { - return -1 - } - return angConfig.vmess[angConfig.index].configType - } - - fun currConfigName(): String { - if (angConfig.index < 0 - || angConfig.vmess.count() <= 0 - || angConfig.index > angConfig.vmess.count() - 1 - ) { - return "" - } - return angConfig.vmess[angConfig.index].remarks - } - - fun currConfigGuid(): String { - if (angConfig.index < 0 - || angConfig.vmess.count() <= 0 - || angConfig.index > angConfig.vmess.count() - 1 - ) { - return "" - } - return angConfig.vmess[angConfig.index].guid - } - - /** - * import config form qrcode or... - */ - fun importConfig(server: String?, subid: String): Int { - try { - if (server == null || TextUtils.isEmpty(server)) { - return R.string.toast_none_data - } - - var vmess = AngConfig.VmessBean() - - if (server.startsWith(VMESS_PROTOCOL)) { - - val indexSplit = server.indexOf("?") - if (indexSplit > 0) { - vmess = ResolveVmess4Kitsunebi(server) - } else { - - var result = server.replace(VMESS_PROTOCOL, "") - result = Utils.decode(result) - if (TextUtils.isEmpty(result)) { - return R.string.toast_decoding_failed - } - val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java) - if (TextUtils.isEmpty(vmessQRCode.add) - || TextUtils.isEmpty(vmessQRCode.port) - || TextUtils.isEmpty(vmessQRCode.id) - || TextUtils.isEmpty(vmessQRCode.aid) - || TextUtils.isEmpty(vmessQRCode.net) - ) { - return R.string.toast_incorrect_protocol - } - - vmess.configType = AppConfig.EConfigType.Vmess - vmess.security = "auto" - vmess.network = "tcp" - vmess.headerType = "none" - - vmess.configVersion = Utils.parseInt(vmessQRCode.v) - vmess.remarks = vmessQRCode.ps - vmess.address = vmessQRCode.add - vmess.port = Utils.parseInt(vmessQRCode.port) - vmess.id = vmessQRCode.id - vmess.alterId = Utils.parseInt(vmessQRCode.aid) - vmess.network = vmessQRCode.net - vmess.headerType = vmessQRCode.type - vmess.requestHost = vmessQRCode.host - vmess.path = vmessQRCode.path - vmess.streamSecurity = vmessQRCode.tls - vmess.subid = subid - } - upgradeServerVersion(vmess) - addServer(vmess, -1) - - } else if (server.startsWith(SS_PROTOCOL)) { - var result = server.replace(SS_PROTOCOL, "") - val indexSplit = result.indexOf("#") - if (indexSplit > 0) { - try { - vmess.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) - if (match == null) { - return R.string.toast_incorrect_protocol - } - vmess.security = match.groupValues[1].toLowerCase() - vmess.id = match.groupValues[2] - vmess.address = match.groupValues[3] - if (vmess.address.firstOrNull() == '[' && vmess.address.lastOrNull() == ']') - vmess.address = vmess.address.substring(1, vmess.address.length - 1) - vmess.port = match.groupValues[4].toInt() - vmess.subid = subid - - addShadowsocksServer(vmess, -1) - } else if (server.startsWith(SOCKS_PROTOCOL)) { - var result = server.replace(SOCKS_PROTOCOL, "") - val indexSplit = result.indexOf("#") - if (indexSplit > 0) { - try { - vmess.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) - } - - val legacyPattern = "^(.+?):(\\d+?)$".toRegex() - val match = legacyPattern.matchEntire(result) - if (match == null) { - return R.string.toast_incorrect_protocol - } - vmess.address = match.groupValues[1] - if (vmess.address.firstOrNull() == '[' && vmess.address.lastOrNull() == ']') - vmess.address = vmess.address.substring(1, vmess.address.length - 1) - vmess.port = match.groupValues[2].toInt() - vmess.subid = subid - - addSocksServer(vmess, -1) - } else { - return R.string.toast_incorrect_protocol - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - private fun ResolveVmess4Kitsunebi(server: String): AngConfig.VmessBean { - - val vmess = AngConfig.VmessBean() - - var result = server.replace(VMESS_PROTOCOL, "") - 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 vmess - } - val arr21 = arr1[0].split(':') - val arr22 = arr1[1].split(':') - if (arr21.count() != 2 || arr21.count() != 2) { - return vmess - } - - vmess.address = arr22[0] - vmess.port = Utils.parseInt(arr22[1]) - vmess.security = arr21[0] - vmess.id = arr21[1] - - vmess.security = "chacha20-poly1305" - vmess.network = "tcp" - vmess.headerType = "none" - vmess.remarks = "Alien" - vmess.alterId = 0 - - return vmess - } - - /** - * share config - */ - fun shareConfig(index: Int): String { - try { - if (index < 0 || index > angConfig.vmess.count() - 1) { - return "" - } - - val vmess = angConfig.vmess[index] - if (angConfig.vmess[index].configType == AppConfig.EConfigType.Vmess) { - - val vmessQRCode = VmessQRCode() - vmessQRCode.v = vmess.configVersion.toString() - vmessQRCode.ps = vmess.remarks - vmessQRCode.add = vmess.address - vmessQRCode.port = vmess.port.toString() - vmessQRCode.id = vmess.id - vmessQRCode.aid = vmess.alterId.toString() - vmessQRCode.net = vmess.network - vmessQRCode.type = vmess.headerType - vmessQRCode.host = vmess.requestHost - vmessQRCode.path = vmess.path - vmessQRCode.tls = vmess.streamSecurity - val json = Gson().toJson(vmessQRCode) - val conf = VMESS_PROTOCOL + Utils.encode(json) - - return conf - } else if (angConfig.vmess[index].configType == AppConfig.EConfigType.Shadowsocks) { - val remark = "#" + Utils.urlEncode(vmess.remarks) - val url = String.format("%s:%s@%s:%s", - vmess.security, - vmess.id, - vmess.address, - vmess.port) - return SS_PROTOCOL + Utils.encode(url) + remark - } else if (angConfig.vmess[index].configType == AppConfig.EConfigType.Socks) { - val remark = "#" + Utils.urlEncode(vmess.remarks) - val url = String.format("%s:%s", - vmess.address, - vmess.port) - return SOCKS_PROTOCOL + Utils.encode(url) + remark - } else { - return "" - } - } catch (e: Exception) { - e.printStackTrace() - return "" - } - } - - /** - * share2Clipboard - */ - fun share2Clipboard(index: Int): Int { - try { - val conf = shareConfig(index) - if (TextUtils.isEmpty(conf)) { - return -1 - } - - Utils.setClipboard(app.applicationContext, conf) - - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * share2Clipboard - */ - fun shareAll2Clipboard(): Int { - try { - val sb = StringBuilder() - for (k in 0 until angConfig.vmess.count()) { - val url = shareConfig(k) - if (TextUtils.isEmpty(url)) { - continue - } - sb.append(url) - sb.appendln() - } - if (sb.count() > 0) { - Utils.setClipboard(app.applicationContext, sb.toString()) - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * share2QRCode - */ - fun share2QRCode(index: Int): Bitmap? { - try { - val conf = shareConfig(index) - if (TextUtils.isEmpty(conf)) { - return null - } - val bitmap = Utils.createQRCode(conf) - return bitmap - - } catch (e: Exception) { - e.printStackTrace() - return null - } - } - - /** - * shareFullContent2Clipboard - */ - fun shareFullContent2Clipboard(index: Int): Int { - try { - if (AngConfigManager.genStoreV2rayConfig(index)) { - val configContent = app.defaultDPreference.getPrefString(AppConfig.PREF_CURR_CONFIG, "") - Utils.setClipboard(app.applicationContext, configContent) - } else { - return -1 - } - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * import customize config - */ - fun importCustomizeConfig(server: String?): Int { - try { - if (server == null || TextUtils.isEmpty(server)) { - return R.string.toast_none_data - } - - val guid = System.currentTimeMillis().toString() - app.defaultDPreference.setPrefString(ANG_CONFIG + guid, server) - - //add - val vmess = AngConfig.VmessBean() - vmess.configVersion = 2 - vmess.configType = AppConfig.EConfigType.Custom - vmess.guid = guid - vmess.remarks = vmess.guid - - vmess.security = "" - vmess.network = "" - vmess.headerType = "" - vmess.address = "" - vmess.port = 0 - vmess.id = "" - vmess.alterId = 0 - vmess.network = "" - vmess.headerType = "" - vmess.requestHost = "" - vmess.streamSecurity = "" - - angConfig.vmess.add(vmess) - if (angConfig.vmess.count() == 1) { - angConfig.index = 0 - } - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * getIndexViaGuid - */ - fun getIndexViaGuid(guid: String): Int { - try { - if (TextUtils.isEmpty(guid)) { - return -1 - } - for (i in angConfig.vmess.indices) { - if (angConfig.vmess[i].guid == guid) { - return i - } - } - return -1 - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - } - - /** - * upgrade - */ - fun upgradeServerVersion(vmess: AngConfig.VmessBean): Int { - try { - if (vmess.configVersion == 2) { - return 0 - } - - when (vmess.network) { - "kcp" -> { - } - "ws" -> { - var path = "" - var host = "" - val lstParameter = vmess.requestHost.split(";") - if (lstParameter.size > 0) { - path = lstParameter.get(0).trim() - } - if (lstParameter.size > 1) { - path = lstParameter.get(0).trim() - host = lstParameter.get(1).trim() - } - vmess.path = path - vmess.requestHost = host - } - "h2" -> { - var path = "" - var host = "" - val lstParameter = vmess.requestHost.split(";") - if (lstParameter.size > 0) { - path = lstParameter.get(0).trim() - } - if (lstParameter.size > 1) { - path = lstParameter.get(0).trim() - host = lstParameter.get(1).trim() - } - vmess.path = path - vmess.requestHost = host - } - else -> { - } - } - vmess.configVersion = 2 - return 0 - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - } - - - fun addCustomServer(vmess: AngConfig.VmessBean, index: Int): Int { - try { - vmess.configVersion = 2 - vmess.configType = AppConfig.EConfigType.Custom - - if (index >= 0) { - //edit - angConfig.vmess[index] = vmess - } else { - //add - vmess.guid = System.currentTimeMillis().toString() - angConfig.vmess.add(vmess) - if (angConfig.vmess.count() == 1) { - angConfig.index = 0 - } - } - - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - fun addShadowsocksServer(vmess: AngConfig.VmessBean, index: Int): Int { - try { - vmess.configVersion = 2 - vmess.configType = AppConfig.EConfigType.Shadowsocks - - if (index >= 0) { - //edit - angConfig.vmess[index] = vmess - } else { - //add - vmess.guid = System.currentTimeMillis().toString() - angConfig.vmess.add(vmess) - if (angConfig.vmess.count() == 1) { - angConfig.index = 0 - } - } - - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - fun addSocksServer(vmess: AngConfig.VmessBean, index: Int): Int { - try { - vmess.configVersion = 2 - vmess.configType = AppConfig.EConfigType.Socks - - if (index >= 0) { - //edit - angConfig.vmess[index] = vmess - } else { - //add - vmess.guid = System.currentTimeMillis().toString() - angConfig.vmess.add(vmess) - if (angConfig.vmess.count() == 1) { - angConfig.index = 0 - } - } - - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - fun importBatchConfig(servers: String?, subid: String): Int { - try { - if (servers == null) { - return 0 - } - 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) - if (resId == 0) { - count++ - } - } - return count - } catch (e: Exception) { - e.printStackTrace() - } - return 0 - } - - fun saveSubItem(subItem: ArrayList): Int { - try { - if (subItem.count() <= 0) { - return -1 - } - for (k in 0 until subItem.count()) { - if (TextUtils.isEmpty(subItem[k].id)) { - subItem[k].id = Utils.getUuid() - } - } - angConfig.subItem = subItem - - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - fun removeServerViaSubid(subid: String): Int { - if (TextUtils.isEmpty(subid) || configs.vmess.count() <= 0) { - return -1 - } - - for (k in configs.vmess.count() - 1 downTo 0) { - if (configs.vmess[k].subid.equals(subid)) { - angConfig.vmess.removeAt(k) - } - } - - storeConfigFile() - return 0 - } - - fun addSubItem(subItem: AngConfig.SubItemBean, index: Int): Int { - try { - if (index >= 0) { - //edit - angConfig.subItem[index] = subItem - } else { - //add - angConfig.subItem.add(subItem) - } - - saveSubItem(angConfig.subItem) - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - - /** - * - */ - fun removeSubItem(index: Int): Int { - try { - if (index < 0 || index > angConfig.subItem.count() - 1) { - return -1 - } - - //删除 - angConfig.subItem.removeAt(index) - - storeConfigFile() - } catch (e: Exception) { - e.printStackTrace() - return -1 - } - return 0 - } - -} \ No newline at end of file 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 a806ee5d..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) 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.create { - it.onNext(loadNetworkAppList(ctx)) - } - - val PackageInfo.hasInternetPermission: Boolean - get() { - val permissions = requestedPermissions - return permissions?.any { it == Manifest.permission.INTERNET } ?: false - } -} \ No newline at end of file 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/Utils.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt deleted file mode 100644 index b986add4..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/Utils.kt +++ /dev/null @@ -1,515 +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.app.ActivityManager -import android.content.ClipData -import android.content.Intent -import android.content.res.AssetManager -import android.net.Uri -import android.os.SystemClock -import android.text.TextUtils -import android.text.method.ScrollingMovementMethod -import android.util.Log -import android.util.Patterns -import android.view.View -import android.webkit.URLUtil -import com.v2ray.ang.AngApplication -import com.v2ray.ang.AppConfig -import com.v2ray.ang.R -import com.v2ray.ang.extension.responseLength -import com.v2ray.ang.extension.v2RayApplication -import com.v2ray.ang.service.V2RayVpnService -import com.v2ray.ang.ui.SettingsActivity -import kotlinx.android.synthetic.main.activity_logcat.* -import me.dozen.dpreference.DPreference -import org.jetbrains.anko.toast -import org.jetbrains.anko.uiThread -import java.io.BufferedReader -import java.io.File -import java.io.IOException -import java.io.InputStreamReader -import java.net.* -import java.util.regex.Matcher -import java.util.regex.Pattern -import java.math.BigInteger -import java.util.concurrent.TimeUnit -import libv2ray.Libv2ray - - -object Utils { - - /** - * 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 { - try { - return Integer.parseInt(str) - } catch (e: Exception) { - e.printStackTrace() - return 0 - } - } - - /** - * get text from clipboard - */ - fun getClipboard(context: Context): String { - try { - val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - return cmb.primaryClip?.getItemAt(0)?.text.toString() - } catch (e: Exception) { - e.printStackTrace() - return "" - } - } - - /** - * 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.primaryClip = clipData - } catch (e: Exception) { - e.printStackTrace() - } - } - - /** - * base64 decode - */ - fun decode(text: String): String { - try { - return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8")) - } catch (e: Exception) { - e.printStackTrace() - return "" - } - } - - /** - * base64 encode - */ - fun encode(text: String): String { - try { - return Base64.encodeToString(text.toByteArray(charset("UTF-8")), Base64.NO_WRAP) - } catch (e: Exception) { - e.printStackTrace() - return "" - } - } - - /** - * get remote dns servers from preference - */ - fun getRemoteDnsServers(defaultDPreference: DPreference): ArrayList { - val remoteDns = defaultDPreference.getPrefString(SettingsActivity.PREF_REMOTE_DNS, AppConfig.DNS_AGENT) - val ret = ArrayList() - if (!TextUtils.isEmpty(remoteDns)) { - remoteDns - .split(",") - .forEach { - if (Utils.isPureIpAddress(it)) { - ret.add(it) - } - } - } - if (ret.size == 0) { - ret.add(AppConfig.DNS_AGENT) - } - return ret - } - - /** - * get remote dns servers from preference - */ - fun getDomesticDnsServers(defaultDPreference: DPreference): ArrayList { - val domesticDns = defaultDPreference.getPrefString(SettingsActivity.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) - val ret = ArrayList() - if (!TextUtils.isEmpty(domesticDns)) { - domesticDns - .split(",") - .forEach { - if (Utils.isPureIpAddress(it)) { - ret.add(it) - } - } - } - if (ret.size == 0) { - ret.add(AppConfig.DNS_DIRECT) - } - return ret - } - - /** - * create qrcode using zxing - */ - fun createQRCode(text: String, size: Int = 800): Bitmap? { - try { - val hints = HashMap() - hints.put(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..size - 1) { - for (x in 0..size - 1) { - 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() - var 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) - } - - /** - * is valid url - */ - fun isValidUrl(value: String?): Boolean { - try { - if (Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) { - return true - } - } catch (e: WriterException) { - e.printStackTrace() - return false - } - return false - } - - - /** - * 判断服务是否后台运行 - - * @param context - * * Context - * * - * @param className - * * 判断的服务名字 - * * - * @return true 在运行 false 不在运行 - */ - fun isServiceRun(context: Context, className: String): Boolean { - var isRun = false - val activityManager = context - .getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val serviceList = activityManager - .getRunningServices(999) - val size = serviceList.size - for (i in 0..size - 1) { - if (serviceList[i].service.className == className) { - isRun = true - break - } - } - return isRun - } - - /** - * startVService - */ - fun startVService(context: Context): Boolean { - if (context.v2RayApplication.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PROXY_SHARING, false)) { - context.toast(R.string.toast_warning_pref_proxysharing_short) - }else{ - context.toast(R.string.toast_services_start) - } - if (AngConfigManager.genStoreV2rayConfig(-1)) { - val configContent = AngConfigManager.currGeneratedV2rayConfig() - val configType = AngConfigManager.currConfigType() - if (configType == AppConfig.EConfigType.Custom) { - try { - Libv2ray.testConfig(configContent) - } catch (e: Exception) { - context.toast(e.toString()) - return false - } - } - V2RayVpnService.startV2Ray(context) - return true - } else { - return false - } - } - - /** - * startVService - */ - fun startVService(context: Context, guid: String): Boolean { - val index = AngConfigManager.getIndexViaGuid(guid) - context.v2RayApplication.curIndex=index - return startVService(context, index) - } - - /** - * startVService - */ - fun startVService(context: Context, index: Int): Boolean { - AngConfigManager.setActiveServer(index) - return startVService(context) - } - - /** - * 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 { - try { - return UUID.randomUUID().toString().replace("-", "") - } catch (e: Exception) { - e.printStackTrace() - return "" - } - } - - fun urlDecode(url: String): String { - try { - return URLDecoder.decode(url, "UTF-8") - } catch (e: Exception) { - e.printStackTrace() - return url - } - } - - fun urlEncode(url: String): String { - try { - return URLEncoder.encode(url, "UTF-8") - } catch (e: Exception) { - e.printStackTrace() - return 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(app: AngApplication, fileName: String): String { - val content = app.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 (!TextUtils.isEmpty(allText)) { - val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19) - val temps = tempInfo.split("/".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray() - if (temps.count() > 0 && temps[0].length < 10) { - return temps[0].toFloat().toInt().toString() + "ms" - } - } - } catch (e: Exception) { - e.printStackTrace() - } - return "-1ms" - } - - /** - * tcping - */ - fun tcping(url: String, port: Int): String { - var time = -1L - for (k in 0 until 2) { - val one = socketConnectTime(url, port) - if (one != -1L ) - if(time == -1L || one < time) { - time = one - } - } - return time.toString() + "ms" - } - - fun socketConnectTime(url: String, port: Int): Long { - try { - val start = System.currentTimeMillis() - val socket = Socket(url, port) - val time = System.currentTimeMillis() - start - socket.close() - return time - } catch (e: UnknownHostException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() - } catch (e: Exception) { - e.printStackTrace() - } - return -1 - } -} - 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 559efc4a..00000000 --- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/V2rayConfigUtil.kt +++ /dev/null @@ -1,683 +0,0 @@ -package com.v2ray.ang.util - -import android.text.TextUtils -import android.util.Log -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonArray -import com.v2ray.ang.AngApplication -import com.v2ray.ang.AppConfig -import com.v2ray.ang.dto.AngConfig.VmessBean -import com.v2ray.ang.dto.V2rayConfig -import com.v2ray.ang.extension.defaultDPreference -import com.v2ray.ang.ui.SettingsActivity -import org.json.JSONException -import org.json.JSONObject -import org.json.JSONArray -import com.google.gson.JsonObject - -object V2rayConfigUtil { - private val requestObj: JsonObject by lazy { - Gson().fromJson("""{"version":"1.1","method":"GET","path":["/"],"headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 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"}}""", JsonObject::class.java) - } - -// private val responseObj: JSONObject by lazy { -// JSONObject("""{"version":"1.1","status":"200","reason":"OK","headers":{"Content-Type":["application/octet-stream","video/mpeg"],"Transfer-Encoding":["chunked"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""") -// } - - data class Result(var status: Boolean, var content: String) - - /** - * 生成v2ray的客户端配置文件 - */ - fun getV2rayConfig(app: AngApplication, vmess: VmessBean): Result { - var result = Result(false, "") - try { - //检查设置 -// if (config.index < 0 -// || config.vmess.count() <= 0 -// || config.index > config.vmess.count() - 1 -// ) { -// return result -// } - - if (vmess.configType == AppConfig.EConfigType.Vmess) { - result = getV2rayConfigType1(app, vmess) - } else if (vmess.configType == AppConfig.EConfigType.Custom) { - result = getV2rayConfigType2(app, vmess) - } else if (vmess.configType == AppConfig.EConfigType.Shadowsocks) { - result = getV2rayConfigType1(app, vmess) - } else if (vmess.configType == AppConfig.EConfigType.Socks) { - result = getV2rayConfigType1(app, vmess) - } - - val domainName = parseDomainName(result.content) - if (!TextUtils.isEmpty(domainName)) { - app.defaultDPreference.setPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, domainName) - } - - Log.d("V2rayConfigUtilGoLog", result.content) - return result - } catch (e: Exception) { - e.printStackTrace() - return result - } - } - - /** - * 生成v2ray的客户端配置文件 - */ - private fun getV2rayConfigType1(app: AngApplication, vmess: VmessBean): Result { - val result = Result(false, "") - try { - //取得默认配置 - val assets = Utils.readTextFromAssets(app, "v2ray_config.json") - if (TextUtils.isEmpty(assets)) { - return result - } - - //转成Json - val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result -// if (v2rayConfig == null) { -// return result -// } - - inbounds(vmess, v2rayConfig, app) - - outbounds(vmess, v2rayConfig, app) - - routing(vmess, v2rayConfig, app) - - if (app.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_LOCAL_DNS_ENABLED, false)) { - customLocalDns(vmess, v2rayConfig, app) - } else { - customRemoteDns(vmess, v2rayConfig, app) - } - - val finalConfig = GsonBuilder().setPrettyPrinting().create().toJson(v2rayConfig) - - result.status = true - result.content = finalConfig - return result - - } catch (e: Exception) { - e.printStackTrace() - return result - } - } - - /** - * 生成v2ray的客户端配置文件 - */ - private fun getV2rayConfigType2(app: AngApplication, vmess: VmessBean): Result { - val result = Result(false, "") - try { - val guid = vmess.guid - val jsonConfig = app.defaultDPreference.getPrefString(AppConfig.ANG_CONFIG + guid, "") - result.status = true - result.content = jsonConfig - return result - - } catch (e: Exception) { - e.printStackTrace() - return result - } - } - - /** - * - */ - private fun inbounds(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { - try { - v2rayConfig.inbounds.forEach { curInbound -> - if (!app.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_PROXY_SHARING, false)) { - //bind all inbounds to localhost if the user requests - curInbound.listen = "127.0.0.1" - } - } - v2rayConfig.inbounds[0].port = 10808 -// val socksPort = Utils.parseInt(app.defaultDPreference.getPrefString(SettingsActivity.PREF_SOCKS_PORT, "10808")) -// val lanconnPort = Utils.parseInt(app.defaultDPreference.getPrefString(SettingsActivity.PREF_HTTP_PORT, "")) - -// if (socksPort > 0) { -// v2rayConfig.inbounds[0].port = socksPort -// } -// if (lanconnPort > 0) { -// val httpCopy = v2rayConfig.inbounds[0].copy() -// httpCopy.port = lanconnPort -// httpCopy.protocol = "http" -// v2rayConfig.inbounds.add(httpCopy) -// } - v2rayConfig.inbounds[0].sniffing?.enabled = app.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_SNIFFING_ENABLED, true) - - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - /** - * vmess协议服务器配置 - */ - private fun outbounds(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { - try { - val outbound = v2rayConfig.outbounds[0] - - when (vmess.configType) { - AppConfig.EConfigType.Vmess -> { - outbound.settings?.servers = null - - val vnext = v2rayConfig.outbounds[0].settings?.vnext?.get(0) - vnext?.address = vmess.address - vnext?.port = vmess.port - val user = vnext?.users?.get(0) - user?.id = vmess.id - user?.alterId = vmess.alterId - user?.security = vmess.security - user?.level = 8 - - //Mux - val muxEnabled = false//app.defaultDPreference.getPrefBoolean(SettingsActivity.PREF_MUX_ENABLED, false) - outbound.mux?.enabled = muxEnabled - - //远程服务器底层传输配置 - outbound.streamSettings = boundStreamSettings(vmess) - - outbound.protocol = "vmess" - } - AppConfig.EConfigType.Shadowsocks -> { - outbound.settings?.vnext = null - - val server = outbound.settings?.servers?.get(0) - server?.address = vmess.address - server?.method = vmess.security - server?.ota = false - server?.password = vmess.id - server?.port = vmess.port - server?.level = 8 - - //Mux - outbound.mux?.enabled = false - - outbound.protocol = "shadowsocks" - } - AppConfig.EConfigType.Socks -> { - outbound.settings?.vnext = null - - val server = outbound.settings?.servers?.get(0) - server?.address = vmess.address - server?.port = vmess.port - - //Mux - outbound.mux?.enabled = false - - outbound.protocol = "socks" - } - else -> { - } - } - - var serverDomain: String - if(Utils.isIpv6Address(vmess.address)) { - serverDomain = String.format("[%s]:%s", vmess.address, vmess.port) - } else { - serverDomain = String.format("%s:%s", vmess.address, vmess.port) - } - app.defaultDPreference.setPrefString(AppConfig.PREF_CURR_CONFIG_DOMAIN, serverDomain) - - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - /** - * 远程服务器底层传输配置 - */ - private fun boundStreamSettings(vmess: VmessBean): V2rayConfig.OutboundBean.StreamSettingsBean { - val streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean("", "", null, null, null, null, null, null) - try { - //远程服务器底层传输配置 - streamSettings.network = vmess.network - streamSettings.security = vmess.streamSecurity - - //streamSettings - when (streamSettings.network) { - "kcp" -> { - val kcpsettings = V2rayConfig.OutboundBean.StreamSettingsBean.KcpsettingsBean() - kcpsettings.mtu = 1350 - kcpsettings.tti = 50 - kcpsettings.uplinkCapacity = 12 - kcpsettings.downlinkCapacity = 100 - kcpsettings.congestion = false - kcpsettings.readBufferSize = 1 - kcpsettings.writeBufferSize = 1 - kcpsettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.KcpsettingsBean.HeaderBean() - kcpsettings.header.type = vmess.headerType - streamSettings.kcpsettings = kcpsettings - } - "ws" -> { - val wssettings = V2rayConfig.OutboundBean.StreamSettingsBean.WssettingsBean() - wssettings.connectionReuse = true - val host = vmess.requestHost.trim() - val path = vmess.path.trim() - - if (!TextUtils.isEmpty(host)) { - wssettings.headers = V2rayConfig.OutboundBean.StreamSettingsBean.WssettingsBean.HeadersBean() - wssettings.headers.Host = host - } - if (!TextUtils.isEmpty(path)) { - wssettings.path = path - } - streamSettings.wssettings = wssettings - - val tlssettings = V2rayConfig.OutboundBean.StreamSettingsBean.TlssettingsBean() - tlssettings.allowInsecure = true - if (!TextUtils.isEmpty(host)) { - tlssettings.serverName = host - } - streamSettings.tlssettings = tlssettings - } - "h2" -> { - val httpsettings = V2rayConfig.OutboundBean.StreamSettingsBean.HttpsettingsBean() - val host = vmess.requestHost.trim() - val path = vmess.path.trim() - - if (!TextUtils.isEmpty(host)) { - httpsettings.host = host.split(",").map { it.trim() } - } - httpsettings.path = path - streamSettings.httpsettings = httpsettings - - val tlssettings = V2rayConfig.OutboundBean.StreamSettingsBean.TlssettingsBean() - tlssettings.allowInsecure = true - streamSettings.tlssettings = tlssettings - } - "quic" -> { - val quicsettings = V2rayConfig.OutboundBean.StreamSettingsBean.QuicsettingBean() - val host = vmess.requestHost.trim() - val path = vmess.path.trim() - - quicsettings.security = host - quicsettings.key = path - - quicsettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.QuicsettingBean.HeaderBean() - quicsettings.header.type = vmess.headerType - - streamSettings.quicsettings = quicsettings - } - else -> { - //tcp带http伪装 - if (vmess.headerType == "http") { - val tcpSettings = V2rayConfig.OutboundBean.StreamSettingsBean.TcpsettingsBean() - tcpSettings.connectionReuse = true - tcpSettings.header = V2rayConfig.OutboundBean.StreamSettingsBean.TcpsettingsBean.HeaderBean() - tcpSettings.header.type = vmess.headerType - -// if (requestObj.has("headers") -// || requestObj.optJSONObject("headers").has("Pragma")) { -// val arrHost = ArrayList() -// vmess.requestHost -// .split(",") -// .forEach { -// arrHost.add(it) -// } -// requestObj.optJSONObject("headers") -// .put("Host", arrHost) -// -// } - if (!TextUtils.isEmpty(vmess.requestHost)) { - val arrHost = ArrayList() - vmess.requestHost - .split(",") - .forEach { - arrHost.add("\"$it\"") - } - requestObj.getAsJsonObject("headers") - .add("Host", Gson().fromJson(arrHost.toString(), JsonArray::class.java)) - } - if (!TextUtils.isEmpty(vmess.path)) { - val arrPath = ArrayList() - vmess.path - .split(",") - .forEach { - arrPath.add("\"$it\"") - } - requestObj.add("path", Gson().fromJson(arrPath.toString(), JsonArray::class.java)) - } - tcpSettings.header.request = requestObj - //tcpSettings.header.response = responseObj - streamSettings.tcpSettings = tcpSettings - } - } - } - } catch (e: Exception) { - e.printStackTrace() - return streamSettings - } - return streamSettings - } - - /** - * routing - */ - private fun routing(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { - try { - routingUserRule(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_AGENT, ""), AppConfig.TAG_AGENT, v2rayConfig) - routingUserRule(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_DIRECT, ""), AppConfig.TAG_DIRECT, v2rayConfig) - routingUserRule(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED, ""), AppConfig.TAG_BLOCKED, v2rayConfig) - - v2rayConfig.routing.domainStrategy = app.defaultDPreference.getPrefString(SettingsActivity.PREF_ROUTING_DOMAIN_STRATEGY, "IPIfNonMatch") - val routingMode = app.defaultDPreference.getPrefString(SettingsActivity.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) { - "0" -> { - } - "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.trim().replace("\n", "") - .split(",") - .forEach { - if (Utils.isIpAddress(it) || it.startsWith("geoip:")) { - rulesIP.ip?.add(it) - } else if (it.isNotBlank() || 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.trim().replace("\n", "").split(",").forEach { - if ((it.startsWith("geosite:") || it.startsWith("domain:")) && - it.isNotBlank() && it.isNotEmpty()) { - domain.add(it) - } - } - return domain - } - - /** - * Custom Dns - */ - private fun customLocalDns(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { - try { - val hosts = mutableMapOf() - val servers = ArrayList() - val remoteDns = Utils.getRemoteDnsServers(app.defaultDPreference) - remoteDns.forEach { - servers.add(it) - } - - val domesticDns = Utils.getDomesticDnsServers(app.defaultDPreference) - - val agDomain = userRule2Domian(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_AGENT, "")) - if (agDomain.size > 0) { - servers.add(V2rayConfig.DnsBean.ServersBean(remoteDns.first(), 53, agDomain)) - } - - val dirDomain = userRule2Domian(app.defaultDPreference.getPrefString(AppConfig.PREF_V2RAY_ROUTING_DIRECT, "")) - if (dirDomain.size > 0) { - servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, dirDomain)) - } - - val routingMode = app.defaultDPreference.getPrefString(SettingsActivity.PREF_ROUTING_MODE, "0") - if (routingMode == "2" || routingMode == "3") { - servers.add(V2rayConfig.DnsBean.ServersBean(domesticDns.first(), 53, arrayListOf("geosite:cn"))) - } - - val blkDomain = userRule2Domian(app.defaultDPreference.getPrefString(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.put("domain:googleapis.cn", "googleapis.com") - - // DNS dns对象 - v2rayConfig.dns = V2rayConfig.DnsBean( - servers = servers, - hosts = hosts) - - // DNS inbound对象 - if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) { - val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean( - address = remoteDns.first(), - port = 53, - network = "tcp,udp") - - 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 - v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean( - type = "field", - outboundTag = AppConfig.TAG_DIRECT, - port = "53", - ip = domesticDns, - domain = null) - ) - - v2rayConfig.routing.rules.add(0, V2rayConfig.RoutingBean.RulesBean( - type = "field", - outboundTag = AppConfig.TAG_AGENT, - port = "53", - ip = remoteDns, - domain = 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 - } - - /** - * Custom Remote Dns - */ - private fun customRemoteDns(vmess: VmessBean, v2rayConfig: V2rayConfig, app: AngApplication): Boolean { - try { - val servers = ArrayList() - - Utils.getRemoteDnsServers(app.defaultDPreference).forEach { - servers.add(it) - } - - v2rayConfig.dns = V2rayConfig.DnsBean(servers = servers) - } catch (e: Exception) { - e.printStackTrace() - return false - } - return true - } - - /** - * is valid config - */ - fun isValidConfig(conf: String): Boolean { - try { - val jObj = JSONObject(conf) - var hasBound = false - //hasBound = (jObj.has("outbounds") and jObj.has("inbounds")) or (jObj.has("outbound") and jObj.has("inbound")) - hasBound = (jObj.has("outbounds")) or (jObj.has("outbound")) - return hasBound - } catch (e: JSONException) { - return false - } - } - - private fun parseDomainName(jsonConfig: String): String { - try { - val jObj = JSONObject(jsonConfig) - var domainName: String - if (jObj.has("outbound")) { - domainName = parseDomainName(jObj.optJSONObject("outbound")) - if (!TextUtils.isEmpty(domainName)) { - return domainName - } - } - if (jObj.has("outbounds")) { - for (i in 0..(jObj.optJSONArray("outbounds").length() - 1)) { - domainName = parseDomainName(jObj.optJSONArray("outbounds").getJSONObject(i)) - if (!TextUtils.isEmpty(domainName)) { - return domainName - } - } - } - if (jObj.has("outboundDetour")) { - for (i in 0..(jObj.optJSONArray("outboundDetour").length() - 1)) { - domainName = parseDomainName(jObj.optJSONArray("outboundDetour").getJSONObject(i)) - if (!TextUtils.isEmpty(domainName)) { - return domainName - } - } - } - } catch (e: Exception) { - e.printStackTrace() - } - return "" - } - - private fun parseDomainName(outbound: JSONObject): String { - try { - if (outbound.has("settings")) { - var vnext: JSONArray? - if (outbound.optJSONObject("settings").has("vnext")) { - // vmess - vnext = outbound.optJSONObject("settings").optJSONArray("vnext") - } else if (outbound.optJSONObject("settings").has("servers")) { - // shadowsocks or socks - vnext = outbound.optJSONObject("settings").optJSONArray("servers") - } else { - return "" - } - for (i in 0..(vnext.length() - 1)) { - val item = vnext.getJSONObject(i) - val address = item.getString("address") - val port = item.getString("port") - if(Utils.isIpv6Address(address)) { - return String.format("[%s]:%s", address, port) - } else { - return String.format("%s:%s", address, port) - } - } - } - } catch (e: Exception) { - e.printStackTrace() - } - return "" - } -} \ No newline at end of file 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/color-v21/color_highlight_material.xml b/V2rayNG/app/src/main/res/color/color_highlight_material.xml similarity index 100% rename from V2rayNG/app/src/main/res/color-v21/color_highlight_material.xml rename to V2rayNG/app/src/main/res/color/color_highlight_material.xml 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-xxxhdpi/ic_stat_direct.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_direct.png new file mode 100644 index 00000000..406a0c76 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_direct.png differ 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-xxxhdpi/ic_stat_proxy.png b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_proxy.png new file mode 100644 index 00000000..daef5970 Binary files /dev/null and b/V2rayNG/app/src/main/res/drawable-xxxhdpi/ic_stat_proxy.png differ 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_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_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 1a6c4b8b..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:padding="@dimen/padding_spacing_dp16"> - + android:layout_height="wrap_content" + android:orientation="vertical"> - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + android:orientation="horizontal"> - - + - + + + + + + + + + + + + + + + + - + + - \ No newline at end of file + + + \ 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 054ba3ba..e5f6964c 100644 --- a/V2rayNG/app/src/main/res/layout/activity_main.xml +++ b/V2rayNG/app/src/main/res/layout/activity_main.xml @@ -1,40 +1,32 @@ - + android:layout_height="match_parent"> - + android:layout_height="wrap_content"> - + android:layout_height="?attr/actionBarSize" /> - + - + android:layout_height="match_parent"> - @@ -44,70 +36,93 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + + + + android:layout_weight="1" + android:nextFocusRight="@+id/fab" + android:scrollbars="vertical" /> + android:layout_height="@dimen/view_height_dp64" + android:clickable="true" + 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"> - - - + + - + 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 15d7d804..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_routing_settings.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server.xml b/V2rayNG/app/src/main/res/layout/activity_server.xml deleted file mode 100644 index 1630abc0..00000000 --- a/V2rayNG/app/src/main/res/layout/activity_server.xml +++ /dev/null @@ -1,252 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/V2rayNG/app/src/main/res/layout/activity_server2.xml b/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml similarity index 75% rename from V2rayNG/app/src/main/res/layout/activity_server2.xml rename to V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml index 7924443c..d6d587b8 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server2.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_custom_config.xml @@ -3,31 +3,32 @@ 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:orientation="vertical"> + android:layout_margin="@dimen/padding_spacing_dp8" + android:orientation="vertical"> + android:textColor="@color/color_secondary" /> @@ -59,24 +60,22 @@ - + android:layout_marginTop="@dimen/padding_spacing_dp16" + android:gravity="top|start" /> - - - \ No newline at end of file + diff --git a/V2rayNG/app/src/main/res/layout/activity_server3.xml b/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml similarity index 62% rename from V2rayNG/app/src/main/res/layout/activity_server3.xml rename to V2rayNG/app/src/main/res/layout/activity_server_hysteria2.xml index 3f0af8fb..86df50ef 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server3.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_hysteria2.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" /> - + android:text="@string/server_obfs_password" /> - - + android:layout_height="wrap_content" + android:inputType="text" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml new file mode 100644 index 00000000..0d9e958f --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server_shadowsocks.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_server_socks.xml b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml new file mode 100644 index 00000000..90c1b883 --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server_socks.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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_server4.xml b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml similarity index 56% rename from V2rayNG/app/src/main/res/layout/activity_server4.xml rename to V2rayNG/app/src/main/res/layout/activity_server_vless.xml index 821d61ed..7d29efd2 100644 --- a/V2rayNG/app/src/main/res/layout/activity_server4.xml +++ b/V2rayNG/app/src/main/res/layout/activity_server_vless.xml @@ -3,41 +3,32 @@ 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_id" /> @@ -45,46 +36,53 @@ - - - - - - - - + android:text="@string/server_lab_flow" /> - + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/padding_spacing_dp8" + android:layout_marginBottom="@dimen/padding_spacing_dp8" + android:entries="@array/flows" /> + + + + + + + + + + + + - \ No newline at end of file + diff --git a/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml new file mode 100644 index 00000000..4e77cbdf --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/activity_server_vmess.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 033da71f..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 f2661896..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" /> - 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 2c4031d9..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,47 +2,47 @@ + android:background="?attr/selectableItemBackground" + android:clickable="true" + 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:focusable="false" + 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 3751efd9..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,178 +1,219 @@ - + 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: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" /> + - - \ No newline at end of file + + + 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 30f93c15..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,97 +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 d1a0d910..00000000 --- a/V2rayNG/app/src/main/res/layout/nav_toolbar.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file 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 77b0d6ca..00000000 --- a/V2rayNG/app/src/main/res/layout/nav_view.xml +++ /dev/null @@ -1,11 +0,0 @@ - - \ No newline at end of file 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 new file mode 100644 index 00000000..6d21827b --- /dev/null +++ b/V2rayNG/app/src/main/res/layout/preference_with_help_link.xml @@ -0,0 +1,11 @@ + +