mirror of
https://github.com/2dust/v2rayNG.git
synced 2025-07-01 06:30:04 +00:00
Compare commits
No commits in common. "1.7.12" and "master" have entirely different histories.
438 changed files with 24331 additions and 11323 deletions
159
.github/workflows/build.yml
vendored
Normal file
159
.github/workflows/build.yml
vendored
Normal file
|
@ -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
|
16
.github/workflows/fastlane.yml
vendored
Normal file
16
.github/workflows/fastlane.yml
vendored
Normal file
|
@ -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
|
67
.gitignore
vendored
67
.gitignore
vendored
|
@ -1,9 +1,66 @@
|
|||
V2rayNG/app/src/main/res/layout/activity_inapp_buy.xml
|
||||
V2rayNG/app/src/main/assets/geoip.dat
|
||||
V2rayNG/app/src/main/assets/geosite.dat
|
||||
V2rayNG/app/src/main/java/com/v2ray/ang/InappBuyActivity.java
|
||||
# Ignore data and key store files
|
||||
*.dat
|
||||
*.jks
|
||||
|
||||
# Ignore output JSON file
|
||||
V2rayNG/app/release/output.json
|
||||
|
||||
# Ignore IDE and build system directories
|
||||
.idea/
|
||||
.gradle/
|
||||
.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
|
||||
|
|
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal file
|
@ -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
|
2
AndroidLibV2rayLite/.gitignore
vendored
2
AndroidLibV2rayLite/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*.jar
|
||||
*.aar
|
|
@ -1,65 +0,0 @@
|
|||
package CoreI
|
||||
|
||||
import (
|
||||
v2core "github.com/xtls/xray-core/core"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
IsRunning bool
|
||||
IsTRunning bool
|
||||
PackageName string
|
||||
PackageCodePath string
|
||||
|
||||
Vpoint v2core.Server
|
||||
}
|
||||
|
||||
func CheckVersion() int {
|
||||
return 23
|
||||
}
|
||||
|
||||
func (v *Status) GetDataDir() string {
|
||||
return v.PackageName
|
||||
}
|
||||
|
||||
func (v *Status) GetApp(name string) string {
|
||||
return v.PackageCodePath + name
|
||||
}
|
||||
|
||||
func (v *Status) GetTun2socksArgs(localDNS bool, enableIPv6 bool) (ret []string) {
|
||||
ret = []string{"--netif-ipaddr",
|
||||
"26.26.26.2",
|
||||
"--netif-netmask",
|
||||
"255.255.255.252",
|
||||
"--socks-server-addr",
|
||||
"127.0.0.1:10808",
|
||||
"--tunmtu",
|
||||
"1500",
|
||||
"--loglevel",
|
||||
"notice",
|
||||
"--enable-udprelay",
|
||||
"--sock-path",
|
||||
v.GetDataDir() + "sock_path",
|
||||
}
|
||||
|
||||
if enableIPv6 {
|
||||
ret = append(ret, "--netif-ip6addr", "da26:2626::2")
|
||||
}
|
||||
|
||||
if localDNS {
|
||||
ret = append(ret, "--dnsgw", "127.0.0.1:10807")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Status) GetVPNSetupArg(localDNS bool, enableIPv6 bool) (ret string) {
|
||||
ret = "m,1500 a,26.26.26.1,30 r,0.0.0.0,0"
|
||||
|
||||
if enableIPv6 {
|
||||
ret += " a,da26:2626::1,126 r,::,0"
|
||||
}
|
||||
if localDNS {
|
||||
ret += " d,26.26.26.2"
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
pb:
|
||||
go get -u github.com/golang/protobuf/protoc-gen-go
|
||||
@echo "pb Start"
|
||||
asset:
|
||||
bash gen_assets.sh download
|
||||
mkdir assets
|
||||
cp -v data/*.dat assets/
|
||||
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geosite.dat > geosite.dat
|
||||
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geoip.dat > geoip.dat
|
||||
|
||||
ANDROID_HOME=$(HOME)/android-sdk-linux
|
||||
export ANDROID_HOME
|
||||
PATH:=$(PATH):$(GOPATH)/bin
|
||||
export PATH
|
||||
downloadGoMobile:
|
||||
sudo apt-get install -qq libstdc++6:i386 lib32z1 expect
|
||||
cd ~ ;curl -L https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/ubuntu-cli-install-android-sdk.sh | sudo bash - > /dev/null
|
||||
ls ~
|
||||
ls ~/android-sdk-linux/
|
||||
|
||||
BuildMobile:
|
||||
gomobile init
|
||||
gomobile bind -v -ldflags='-s -w' github.com/2dust/AndroidLibV2rayLite
|
||||
|
||||
all: asset pb
|
||||
@echo DONE
|
|
@ -1,79 +0,0 @@
|
|||
package Escort
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"log"
|
||||
|
||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
||||
)
|
||||
|
||||
func (v *Escorting) EscortRun(proc string, pt []string, additionalEnv string) {
|
||||
log.Println(proc, pt)
|
||||
count := 0
|
||||
for count <= 42 {
|
||||
cmd := exec.Command(proc, pt...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if len(additionalEnv) > 0 {
|
||||
//additionalEnv := "FOO=bar"
|
||||
newEnv := append(os.Environ(), additionalEnv)
|
||||
cmd.Env = newEnv
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Println("EscortRun cmd.Start err", err)
|
||||
goto CMDERROR
|
||||
}
|
||||
|
||||
if v.escortProcess == nil {
|
||||
log.Println("EscortRun v.escortProcess nil")
|
||||
break
|
||||
}
|
||||
|
||||
*v.escortProcess = append(*v.escortProcess, cmd.Process)
|
||||
log.Println("EscortRun Waiting....")
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Println("EscortRun cmd.Wait err:", err)
|
||||
}
|
||||
|
||||
CMDERROR:
|
||||
if v.Status.IsRunning {
|
||||
log.Println("EscortRun Unexpected Exit, Restart now.")
|
||||
count++
|
||||
} else {
|
||||
log.Println("EscortRun Exit")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Escorting) EscortingUp() {
|
||||
if v.escortProcess != nil {
|
||||
return
|
||||
}
|
||||
v.escortProcess = new([](*os.Process))
|
||||
}
|
||||
|
||||
func (v *Escorting) EscortingDown() {
|
||||
if v.escortProcess == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("EscortingDown() Killing all escorted process ")
|
||||
for _, pr := range *v.escortProcess {
|
||||
pr.Kill()
|
||||
if _, err := pr.Wait(); err != nil {
|
||||
log.Println("EscortingDown pr.Wait err:", err)
|
||||
}
|
||||
}
|
||||
v.escortProcess = nil
|
||||
}
|
||||
|
||||
type Escorting struct {
|
||||
escortProcess *[](*os.Process)
|
||||
Status *CoreI.Status
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
# AndroidLibV2rayLite
|
||||
|
||||
进入`AndroidLibV2rayLite`文件夹
|
||||
|
||||
```
|
||||
go mod download
|
||||
```
|
||||
|
||||
|
||||
|
||||
编译aar
|
||||
|
||||
```
|
||||
gomobile bind -v -o android.aar -target=android ./
|
||||
```
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
package VPN
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
v2net "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/features/dns"
|
||||
"github.com/xtls/xray-core/features/outbound"
|
||||
v2internet "github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
type protectSet interface {
|
||||
Protect(int) int
|
||||
}
|
||||
|
||||
type resolved struct {
|
||||
domain string
|
||||
IPs []net.IP
|
||||
Port int
|
||||
ipIdx uint8
|
||||
ipLock sync.Mutex
|
||||
lastSwitched time.Time
|
||||
}
|
||||
|
||||
// NextIP switch to another resolved result.
|
||||
// there still be race-condition here if multiple err concurently occured
|
||||
// may cause idx keep switching,
|
||||
// but that's an outside error can hardly handled here
|
||||
func (r *resolved) NextIP() {
|
||||
r.ipLock.Lock()
|
||||
defer r.ipLock.Unlock()
|
||||
|
||||
if len(r.IPs) > 1 {
|
||||
|
||||
// throttle, don't switch too quickly
|
||||
now := time.Now()
|
||||
if now.Sub(r.lastSwitched) < time.Second*5 {
|
||||
log.Println("switch too quickly")
|
||||
return
|
||||
}
|
||||
r.lastSwitched = now
|
||||
r.ipIdx++
|
||||
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ipIdx >= uint8(len(r.IPs)) {
|
||||
r.ipIdx = 0
|
||||
}
|
||||
|
||||
cur := r.currentIP()
|
||||
log.Printf("switched to next IP: %s", cur)
|
||||
}
|
||||
|
||||
func (r *resolved) currentIP() net.IP {
|
||||
if len(r.IPs) > 0 {
|
||||
return r.IPs[r.ipIdx]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPreotectedDialer ...
|
||||
func NewPreotectedDialer(p protectSet) *ProtectedDialer {
|
||||
d := &ProtectedDialer{
|
||||
// prefer native lookup on Android
|
||||
resolver: &net.Resolver{PreferGo: false},
|
||||
protectSet: p,
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// ProtectedDialer ...
|
||||
type ProtectedDialer struct {
|
||||
currentServer string
|
||||
resolveChan chan struct{}
|
||||
|
||||
vServer *resolved
|
||||
resolver *net.Resolver
|
||||
|
||||
protectSet
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) IsVServerReady() bool {
|
||||
return (d.vServer != nil)
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) PrepareResolveChan() {
|
||||
d.resolveChan = make(chan struct{})
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) ResolveChan() <-chan struct{} {
|
||||
return d.resolveChan
|
||||
}
|
||||
|
||||
// simplicated version of golang: internetAddrList in src/net/ipsock.go
|
||||
func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) {
|
||||
|
||||
var (
|
||||
err error
|
||||
host, port string
|
||||
portnum int
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if host, port, err = net.SplitHostPort(addr); err != nil {
|
||||
log.Printf("PrepareDomain SplitHostPort Err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if portnum, err = d.resolver.LookupPort(ctx, "tcp", port); err != nil {
|
||||
log.Printf("PrepareDomain LookupPort Err: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addrs, err := d.resolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
return nil, fmt.Errorf("domain %s Failed to resolve", addr)
|
||||
}
|
||||
|
||||
IPs := make([]net.IP, len(addrs))
|
||||
for i, ia := range addrs {
|
||||
IPs[i] = ia.IP
|
||||
}
|
||||
|
||||
rs := &resolved{
|
||||
domain: host,
|
||||
IPs: IPs,
|
||||
Port: portnum,
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// PrepareDomain caches direct v2ray server host
|
||||
func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct{}) {
|
||||
log.Printf("Preparing Domain: %s", domainName)
|
||||
d.currentServer = domainName
|
||||
|
||||
defer close(d.resolveChan)
|
||||
maxRetry := 10
|
||||
for {
|
||||
if maxRetry == 0 {
|
||||
log.Println("PrepareDomain maxRetry reached. exiting.")
|
||||
return
|
||||
}
|
||||
|
||||
resolved, err := d.lookupAddr(domainName)
|
||||
if err != nil {
|
||||
maxRetry--
|
||||
log.Printf("PrepareDomain err: %v\n", err)
|
||||
select {
|
||||
case <-closeCh:
|
||||
log.Printf("PrepareDomain exit due to v2ray closed")
|
||||
return
|
||||
case <-time.After(time.Second * 2):
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
d.vServer = resolved
|
||||
log.Printf("Prepare Result:\n Domain: %s\n Port: %d\n IPs: %v\n",
|
||||
resolved.domain, resolved.Port, resolved.IPs)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) getFd(network v2net.Network) (fd int, err error) {
|
||||
switch network {
|
||||
case v2net.Network_TCP:
|
||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
|
||||
case v2net.Network_UDP:
|
||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
||||
default:
|
||||
err = fmt.Errorf("unknow network")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Init implement internet.SystemDialer
|
||||
func (d *ProtectedDialer) Init(_ dns.Client, _ outbound.Manager) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Dial exported as the protected dial method
|
||||
func (d *ProtectedDialer) Dial(ctx context.Context,
|
||||
src v2net.Address, dest v2net.Destination, sockopt *v2internet.SocketConfig) (net.Conn, error) {
|
||||
|
||||
network := dest.Network.SystemString()
|
||||
Address := dest.NetAddr()
|
||||
|
||||
// v2ray server address,
|
||||
// try to connect fixed IP if multiple IP parsed from domain,
|
||||
// and switch to next IP if error occurred
|
||||
if strings.Compare(Address, d.currentServer) == 0 {
|
||||
if d.vServer == nil {
|
||||
log.Println("Dial pending prepare ...", Address)
|
||||
<-d.resolveChan
|
||||
|
||||
// user may close connection during PrepareDomain,
|
||||
// fast return release resources.
|
||||
if d.vServer == nil {
|
||||
return nil, fmt.Errorf("fail to prepare domain %s", d.currentServer)
|
||||
}
|
||||
}
|
||||
|
||||
fd, err := d.getFd(dest.Network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
curIP := d.vServer.currentIP()
|
||||
conn, err := d.fdConn(ctx, curIP, d.vServer.Port, fd)
|
||||
if err != nil {
|
||||
d.vServer.NextIP()
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Using Prepared: %s", curIP)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// v2ray connecting to "domestic" servers, no caching results
|
||||
log.Printf("Not Using Prepared: %s,%s", network, Address)
|
||||
resolved, err := d.lookupAddr(Address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fd, err := d.getFd(dest.Network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use the first resolved address.
|
||||
// the result IP may vary, eg: IPv6 addrs comes first if client has ipv6 address
|
||||
return d.fdConn(ctx, resolved.IPs[0], resolved.Port, fd)
|
||||
}
|
||||
|
||||
func (d *ProtectedDialer) fdConn(ctx context.Context, ip net.IP, port int, fd int) (net.Conn, error) {
|
||||
|
||||
defer unix.Close(fd)
|
||||
|
||||
// call android VPN service to "protect" the fd connecting straight out
|
||||
d.Protect(fd)
|
||||
|
||||
sa := &unix.SockaddrInet6{
|
||||
Port: port,
|
||||
}
|
||||
copy(sa.Addr[:], ip)
|
||||
|
||||
if err := unix.Connect(fd, sa); err != nil {
|
||||
log.Printf("fdConn unix.Connect err, Close Fd: %d Err: %v", fd, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := os.NewFile(uintptr(fd), "Socket")
|
||||
if file == nil {
|
||||
// returned value will be nil if fd is not a valid file descriptor
|
||||
return nil, errors.New("fdConn fd invalid")
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
//Closing conn does not affect file, and closing file does not affect conn.
|
||||
conn, err := net.FileConn(file)
|
||||
if err != nil {
|
||||
log.Printf("fdConn FileConn Close Fd: %d Err: %v", fd, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
package VPN
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v2net "github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
type fakeSupportSet struct{}
|
||||
|
||||
func (f fakeSupportSet) Protect(int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func TestProtectedDialer_PrepareDomain(t *testing.T) {
|
||||
type args struct {
|
||||
domainName string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{"", args{"baidu.com:80"}},
|
||||
// {"", args{"cloudflare.com:443"}},
|
||||
// {"", args{"apple.com:443"}},
|
||||
// {"", args{"110.110.110.110:443"}},
|
||||
// {"", args{"[2002:1234::1]:443"}},
|
||||
}
|
||||
d := NewPreotectedDialer(fakeSupportSet{})
|
||||
for _, tt := range tests {
|
||||
ch := make(chan struct{})
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
go d.PrepareDomain(tt.args.domainName, ch)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
go d.vServer.NextIP()
|
||||
t.Log(d.vServer.currentIP())
|
||||
})
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
func TestProtectedDialer_Dial(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{"baidu.com:80", false},
|
||||
{"cloudflare.com:80", false},
|
||||
{"172.16.192.11:80", true},
|
||||
// {"172.16.192.10:80", true},
|
||||
// {"[2fff:4322::1]:443", true},
|
||||
// {"[fc00::1]:443", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ch := make(chan struct{})
|
||||
|
||||
d := NewPreotectedDialer(fakeSupportSet{})
|
||||
d.currentServer = tt.name
|
||||
|
||||
go d.PrepareDomain(tt.name, ch)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
dial := func() {
|
||||
defer wg.Done()
|
||||
dest, _ := v2net.ParseDestination("tcp:" + tt.name)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := d.Dial(ctx, nil, dest, nil)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
_host, _, _ := net.SplitHostPort(tt.name)
|
||||
fmt.Fprintf(conn, fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\n\r\n", _host))
|
||||
status, err := bufio.NewReader(conn).ReadString('\n')
|
||||
t.Logf("%#v, %#v\n", status, err)
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
for n := 0; n < 3; n++ {
|
||||
wg.Add(1)
|
||||
go dial()
|
||||
// time.Sleep(time.Millisecond * 10)
|
||||
// d.pendingMap[tt.name] = make(chan struct{})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_resolved_NextIP(t *testing.T) {
|
||||
type fields struct {
|
||||
domain string
|
||||
IPs []net.IP
|
||||
Port int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
{"test1",
|
||||
fields{
|
||||
domain: "www.baidu.com",
|
||||
IPs: []net.IP{
|
||||
net.ParseIP("1.2.3.4"),
|
||||
net.ParseIP("4.3.2.1"),
|
||||
net.ParseIP("1234::1"),
|
||||
net.ParseIP("4321::1"),
|
||||
},
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &resolved{
|
||||
domain: tt.fields.domain,
|
||||
IPs: tt.fields.IPs,
|
||||
Port: tt.fields.Port,
|
||||
}
|
||||
t.Logf("%v", r.IPs)
|
||||
t.Logf("%v", r.currentIP())
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
time.Sleep(3 * time.Second)
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
time.Sleep(5 * time.Second)
|
||||
r.NextIP()
|
||||
t.Logf("%v", r.currentIP())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
# Set magic variables for current file & dir
|
||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
||||
__base="$(basename ${__file} .sh)"
|
||||
|
||||
|
||||
DATADIR=${__dir}/data
|
||||
|
||||
compile_dat () {
|
||||
local TMPDIR=$(mktemp -d)
|
||||
|
||||
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; rm -rf $TMPDIR; trap ERR; exit 1' ERR
|
||||
|
||||
local GEOSITE=${GOPATH}/src/github.com/v2ray/domain-list-community
|
||||
if [[ -d ${GEOSITE} ]]; then
|
||||
cd ${GEOSITE} && git pull
|
||||
else
|
||||
mkdir -p ${GEOSITE}
|
||||
cd ${GEOSITE} && git clone https://github.com/v2ray/domain-list-community.git .
|
||||
fi
|
||||
go run main.go
|
||||
|
||||
if [[ -e dlc.dat ]]; then
|
||||
rm -f $DATADIR/geosite.dat
|
||||
mv dlc.dat $DATADIR/geosite.dat
|
||||
echo "----------> geosite.dat updated."
|
||||
else
|
||||
echo "----------> geosite.dat failed to update."
|
||||
fi
|
||||
|
||||
|
||||
if [[ ! -x $GOPATH/bin/geoip ]]; then
|
||||
go get -v -u github.com/v2ray/geoip
|
||||
fi
|
||||
|
||||
cd $TMPDIR
|
||||
curl -L -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country-CSV.zip
|
||||
unzip -q GeoLite2-Country-CSV.zip
|
||||
mkdir geoip && find . -name '*.csv' -exec mv -t ./geoip {} +
|
||||
$GOPATH/bin/geoip \
|
||||
--country=./geoip/GeoLite2-Country-Locations-en.csv \
|
||||
--ipv4=./geoip/GeoLite2-Country-Blocks-IPv4.csv \
|
||||
--ipv6=./geoip/GeoLite2-Country-Blocks-IPv6.csv
|
||||
|
||||
if [[ -e geoip.dat ]]; then
|
||||
rm -f $DATADIR/geoip.dat
|
||||
mv ./geoip.dat $DATADIR/geoip.dat
|
||||
echo "----------> geoip.dat updated."
|
||||
else
|
||||
echo "----------> geoip.dat failed to update."
|
||||
fi
|
||||
trap ERR
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
download_dat () {
|
||||
wget -qO - https://api.github.com/repos/v2ray/geoip/releases/latest \
|
||||
| grep browser_download_url | cut -d '"' -f 4 \
|
||||
| wget -i - -O $DATADIR/geoip.dat
|
||||
|
||||
wget -qO - https://api.github.com/repos/v2ray/domain-list-community/releases/latest \
|
||||
| grep browser_download_url | cut -d '"' -f 4 \
|
||||
| wget -i - -O $DATADIR/geosite.dat
|
||||
}
|
||||
|
||||
ACTION="${1:-}"
|
||||
if [[ -z $ACTION ]]; then
|
||||
ACTION=download
|
||||
fi
|
||||
|
||||
case $ACTION in
|
||||
"download") download_dat;;
|
||||
"compile") compile_dat;;
|
||||
esac
|
|
@ -1,49 +0,0 @@
|
|||
module github.com/2dust/AndroidLibV2rayLite
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/xtls/xray-core v1.5.7
|
||||
golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.27.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pires/go-proxyproto v0.6.2 // indirect
|
||||
github.com/refraction-networking/utls v1.1.0 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/sagernet/sing v0.0.0-20220605012533-e0f722558141 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220605012719-1e22882ea853 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect
|
||||
go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.11-0.20220325154526-54af36eca237 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
|
||||
google.golang.org/grpc v1.47.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
lukechampine.com/blake3 v1.1.7 // indirect
|
||||
)
|
|
@ -1,408 +0,0 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucas-clemente/quic-go v0.27.1 h1:sOw+4kFSVrdWOYmUjufQ9GBVPqZ+tu+jMtXxXNmRJyk=
|
||||
github.com/lucas-clemente/quic-go v0.27.1/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.1 h1:DQjHPq+aOzUeh9/lixAGunn6rIOQyWChPSI4+hgW7jc=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.1 h1:qp7p7XXUFL7fpBvSS1sWD+uSqPvzNQK43DH+/qEkj0Y=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
|
||||
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
|
||||
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/refraction-networking/utls v1.1.0 h1:dKXJwSqni/t5csYJ+aQcEgqB7AMWYi6EUc9u3bEmjX0=
|
||||
github.com/refraction-networking/utls v1.1.0/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sagernet/sing v0.0.0-20220605012533-e0f722558141 h1:YN9EeHRIFYKei1woz2OucLTSpiNrULAHH+jl6upDemQ=
|
||||
github.com/sagernet/sing v0.0.0-20220605012533-e0f722558141/go.mod h1:w2HnJzXKHpD6F5Z/9XlSD4qbcpHY2RSZuQnFzqgELMg=
|
||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220605012719-1e22882ea853 h1:t1pn8v3kPvlaST/fY7GsPV8yIUFJDXzNcmSYQ+nrbEM=
|
||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220605012719-1e22882ea853/go.mod h1:Afu8UIFlEwxTnlidTMSWqWHIduAWOrwY0tFr2LxBACQ=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 h1:4mkzGhKqt3JO1BWYjtD3iRFyAx4ow67hmSqOcGjuxqQ=
|
||||
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672/go.mod h1:YGGVbz9cOxyKFUmhW7LGaLZaMA0cPlHJinvAmVxEMSU=
|
||||
github.com/xtls/xray-core v1.5.7 h1:xQqUpRtzVHL/to2sDmvgn9GelD0jIwC0I8B36hbDgDo=
|
||||
github.com/xtls/xray-core v1.5.7/go.mod h1:H4q5qoCwtsYeF06Zsb0tk8oV8UuaQ9NdnOaPv49Cj7I=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd h1:Uo/x0Ir5vQJ+683GXB9Ug+4fcjsbp7z7Ul8UaZbhsRM=
|
||||
go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd h1:x1GptNaTtxPAlTVIAJk61fuXg0y17h09DTxyb+VNC/k=
|
||||
golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.11-0.20220325154526-54af36eca237 h1:mAhaIX1KEgotq+ju3XYdXUHvll7bzJDTgiDzIAKDdPc=
|
||||
golang.org/x/tools v0.1.11-0.20220325154526-54af36eca237/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
|
||||
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
|
||||
lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
|
@ -1,260 +0,0 @@
|
|||
package libv2ray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
||||
"github.com/2dust/AndroidLibV2rayLite/Process/Escort"
|
||||
"github.com/2dust/AndroidLibV2rayLite/VPN"
|
||||
mobasset "golang.org/x/mobile/asset"
|
||||
|
||||
v2core "github.com/xtls/xray-core/core"
|
||||
v2filesystem "github.com/xtls/xray-core/common/platform/filesystem"
|
||||
v2stats "github.com/xtls/xray-core/features/stats"
|
||||
v2serial "github.com/xtls/xray-core/infra/conf/serial"
|
||||
_ "github.com/xtls/xray-core/main/distro/all"
|
||||
v2internet "github.com/xtls/xray-core/transport/internet"
|
||||
|
||||
v2applog "github.com/xtls/xray-core/app/log"
|
||||
v2commlog "github.com/xtls/xray-core/common/log"
|
||||
)
|
||||
|
||||
const (
|
||||
v2Assert = "xray.location.asset"
|
||||
assetperfix = "/dev/libv2rayfs0/asset"
|
||||
)
|
||||
|
||||
/*V2RayPoint V2Ray Point Server
|
||||
This is territory of Go, so no getter and setters!
|
||||
*/
|
||||
type V2RayPoint struct {
|
||||
SupportSet V2RayVPNServiceSupportsSet
|
||||
statsManager v2stats.Manager
|
||||
|
||||
dialer *VPN.ProtectedDialer
|
||||
status *CoreI.Status
|
||||
escorter *Escort.Escorting
|
||||
v2rayOP *sync.Mutex
|
||||
closeChan chan struct{}
|
||||
|
||||
PackageName string
|
||||
PackageCodePath string
|
||||
DomainName string
|
||||
ConfigureFileContent string
|
||||
EnableLocalDNS bool
|
||||
ForwardIpv6 bool
|
||||
ProxyOnly bool
|
||||
}
|
||||
|
||||
/*V2RayVPNServiceSupportsSet To support Android VPN mode*/
|
||||
type V2RayVPNServiceSupportsSet interface {
|
||||
Setup(Conf string) int
|
||||
Prepare() int
|
||||
Shutdown() int
|
||||
Protect(int) int
|
||||
OnEmitStatus(int, string) int
|
||||
}
|
||||
|
||||
/*RunLoop Run V2Ray main loop
|
||||
*/
|
||||
func (v *V2RayPoint) RunLoop() (err error) {
|
||||
v.v2rayOP.Lock()
|
||||
defer v.v2rayOP.Unlock()
|
||||
//Construct Context
|
||||
v.status.PackageName = v.PackageName
|
||||
v.status.PackageCodePath = v.PackageCodePath
|
||||
|
||||
if !v.status.IsRunning {
|
||||
v.closeChan = make(chan struct{})
|
||||
v.dialer.PrepareResolveChan()
|
||||
go v.dialer.PrepareDomain(v.DomainName, v.closeChan)
|
||||
go func() {
|
||||
select {
|
||||
// wait until resolved
|
||||
case <-v.dialer.ResolveChan():
|
||||
// shutdown VPNService if server name can not reolved
|
||||
if !v.dialer.IsVServerReady() {
|
||||
log.Println("vServer cannot resolved, shutdown")
|
||||
v.StopLoop()
|
||||
}
|
||||
|
||||
// stop waiting if manually closed
|
||||
case <-v.closeChan:
|
||||
}
|
||||
}()
|
||||
|
||||
err = v.pointloop()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*StopLoop Stop V2Ray main loop
|
||||
*/
|
||||
func (v *V2RayPoint) StopLoop() (err error) {
|
||||
v.v2rayOP.Lock()
|
||||
defer v.v2rayOP.Unlock()
|
||||
if v.status.IsRunning {
|
||||
v.shutdownInit()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Delegate Funcation
|
||||
func (v *V2RayPoint) GetIsRunning() bool {
|
||||
return v.status.IsRunning
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) GetIsTRunning() bool {
|
||||
return v.status.IsTRunning
|
||||
}
|
||||
|
||||
//Delegate Funcation
|
||||
func (v V2RayPoint) QueryStats(tag string, direct string) int64 {
|
||||
if v.statsManager == nil {
|
||||
return 0
|
||||
}
|
||||
counter := v.statsManager.GetCounter(fmt.Sprintf("outbound>>>%s>>>traffic>>>%s", tag, direct))
|
||||
if counter == nil {
|
||||
return 0
|
||||
}
|
||||
return counter.Set(0)
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) shutdownInit() {
|
||||
close(v.closeChan)
|
||||
v.statsManager = nil
|
||||
v.status.Vpoint.Close()
|
||||
v.status.Vpoint = nil
|
||||
v.status.IsRunning = false
|
||||
|
||||
v.escorter.EscortingDown()
|
||||
|
||||
v.SupportSet.Shutdown()
|
||||
v.SupportSet.OnEmitStatus(0, "Closed")
|
||||
}
|
||||
|
||||
func (v *V2RayPoint) pointloop() error {
|
||||
log.Println("loading v2ray config")
|
||||
config, err := v2serial.LoadJSONConfig(strings.NewReader(v.ConfigureFileContent))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("new v2ray core")
|
||||
inst, err := v2core.New(config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
v.status.Vpoint = inst
|
||||
v.statsManager = inst.GetFeature(v2stats.ManagerType()).(v2stats.Manager)
|
||||
|
||||
log.Println("start v2ray core")
|
||||
v.status.IsRunning = true
|
||||
if err := v.status.Vpoint.Start(); err != nil {
|
||||
v.status.IsRunning = false
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
v.SupportSet.Prepare()
|
||||
v.SupportSet.Setup(v.status.GetVPNSetupArg(v.EnableLocalDNS, v.ForwardIpv6))
|
||||
v.SupportSet.OnEmitStatus(0, "Running")
|
||||
|
||||
v.status.IsTRunning = false
|
||||
if !v.ProxyOnly {
|
||||
if err := v.runTun2socks(); err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
v.status.IsTRunning = true
|
||||
|
||||
log.Printf("EnableLocalDNS: %v\nForwardIpv6: %v\nDomainName: %s",
|
||||
v.EnableLocalDNS,
|
||||
v.ForwardIpv6,
|
||||
v.DomainName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initV2Env() {
|
||||
if os.Getenv(v2Assert) != "" {
|
||||
return
|
||||
}
|
||||
//Initialize asset API, Since Raymond Will not let notify the asset location inside Process,
|
||||
//We need to set location outside V2Ray
|
||||
os.Setenv(v2Assert, assetperfix)
|
||||
//Now we handle read
|
||||
v2filesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
|
||||
if strings.HasPrefix(path, assetperfix) {
|
||||
p := path[len(assetperfix)+1:]
|
||||
//is it overridden?
|
||||
//by, ok := overridedAssets[p]
|
||||
//if ok {
|
||||
// return os.Open(by)
|
||||
//}
|
||||
return mobasset.Open(p)
|
||||
}
|
||||
return os.Open(path)
|
||||
}
|
||||
}
|
||||
|
||||
//Delegate Funcation
|
||||
func TestConfig(ConfigureFileContent string) error {
|
||||
initV2Env()
|
||||
_, err := v2serial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
||||
return err
|
||||
}
|
||||
|
||||
/*NewV2RayPoint new V2RayPoint*/
|
||||
func NewV2RayPoint(s V2RayVPNServiceSupportsSet) *V2RayPoint {
|
||||
initV2Env()
|
||||
|
||||
// inject our own log writer
|
||||
v2applog.RegisterHandlerCreator(v2applog.LogType_Console,
|
||||
func(lt v2applog.LogType,
|
||||
options v2applog.HandlerCreatorOptions) (v2commlog.Handler, error) {
|
||||
return v2commlog.NewLogger(createStdoutLogWriter()), nil
|
||||
})
|
||||
|
||||
dialer := VPN.NewPreotectedDialer(s)
|
||||
v2internet.UseAlternativeSystemDialer(dialer)
|
||||
status := &CoreI.Status{}
|
||||
return &V2RayPoint{
|
||||
SupportSet: s,
|
||||
v2rayOP: new(sync.Mutex),
|
||||
status: status,
|
||||
dialer: dialer,
|
||||
escorter: &Escort.Escorting{Status: status},
|
||||
}
|
||||
}
|
||||
|
||||
func (v V2RayPoint) runTun2socks() error {
|
||||
v.escorter.EscortingUp()
|
||||
go v.escorter.EscortRun(
|
||||
v.status.GetApp("libtun2socks.so"),
|
||||
v.status.GetTun2socksArgs(v.EnableLocalDNS, v.ForwardIpv6), "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*CheckVersion int
|
||||
This func will return libv2ray binding version.
|
||||
*/
|
||||
func CheckVersion() int {
|
||||
return CoreI.CheckVersion()
|
||||
}
|
||||
|
||||
/*CheckVersionX string
|
||||
This func will return libv2ray binding version and V2Ray version used.
|
||||
*/
|
||||
func CheckVersionX() string {
|
||||
return fmt.Sprintf("Libv2rayLite V%d, Core V%s", CheckVersion(), v2core.Version())
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package libv2ray
|
||||
|
||||
//go:generate make all
|
|
@ -1 +0,0 @@
|
|||
readme.txt
|
|
@ -1,79 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Thanks to https://gist.github.com/wenzhixin/43cf3ce909c24948c6e7
|
||||
# Execute this script in your home directory. Lines 17 and 21 will prompt you for a y/n
|
||||
|
||||
# Install Oracle JDK 8
|
||||
apt-get update
|
||||
apt-get install -y openjdk-8-jdk
|
||||
apt-get install -y unzip make expect # NDK stuff
|
||||
|
||||
# Get SDK tools (link from https://developer.android.com/studio/index.html#downloads)
|
||||
wget -q https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
|
||||
mkdir android-sdk-linux
|
||||
unzip sdk*.zip -d android-sdk-linux
|
||||
|
||||
# Get NDK (https://developer.android.com/ndk/downloads/index.html)
|
||||
# wget -q https://dl.google.com/android/repository/android-ndk-r15c-linux-x86_64.zip
|
||||
# unzip android-ndk*.zip >> /dev/null
|
||||
|
||||
ACCEPT_LICENSES_URL=https://gist.githubusercontent.com/xiaokangwang/1489fd223d26581bfec92adb3cb0088e/raw/328eb6925099df5aae3e76790f8232f0fc378f8b/accept-licenses
|
||||
|
||||
ACCEPT_LICENSES_ITEM="android-sdk-license-bcbbd656|intel-android-sysimage-license-1ea702d1|android-sdk-license-2742d1c5"
|
||||
|
||||
# Let it update itself and install some stuff
|
||||
cd android-sdk-linux/tools
|
||||
|
||||
curl -L -o accept-licenses $ACCEPT_LICENSES_URL
|
||||
|
||||
chmod +x accept-licenses
|
||||
|
||||
./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui" $ACCEPT_LICENSES_ITEM >/dev/null
|
||||
|
||||
# Download every build-tools version that has ever existed
|
||||
# This will save you time! Thank me later for this
|
||||
|
||||
#./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui --filter 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27" $ACCEPT_LICENSES_ITEM
|
||||
|
||||
PACKAGE_PARSE_URL=https://gist.githubusercontent.com/xiaokangwang/06268fb23034ed94bc301880e862da09/raw/afd95cbbe2f8c1d9e7b0277b7c5ef39af756a6ee/parse.awk
|
||||
|
||||
reduceout=https://gist.githubusercontent.com/xiaokangwang/4684bdb5c3415b943f52aa4803386480/raw/b46dab1cc60f02c0d87f88f01e27157034218faa/out.awk
|
||||
|
||||
cd bin
|
||||
|
||||
curl -L -o parse.awk $PACKAGE_PARSE_URL
|
||||
|
||||
curl -L -o reduce.awk $reduceout
|
||||
|
||||
sudo apt-get install gawk
|
||||
|
||||
./sdkmanager --verbose --list |awk -f parse.awk > ~/package_to_install
|
||||
|
||||
readarray -t filenames < $HOME/package_to_install
|
||||
|
||||
cat $HOME/package_to_install
|
||||
|
||||
yes|./sdkmanager --verbose "${filenames[@]}" |awk -f reduce.awk
|
||||
|
||||
# If you need additional packages for your app, check available packages with:
|
||||
# ./android list sdk --all
|
||||
|
||||
# install certain packages with:
|
||||
# ./android update sdk --no-ui --all --filter 1,2,3,<...>,N
|
||||
# where N is the number of the package in the list (see previous command)
|
||||
|
||||
./sdkmanager "ndk-bundle"
|
||||
|
||||
# Add the directory containing executables in PATH so that they can be found
|
||||
echo 'export ANDROID_HOME=$HOME/android-sdk-linux' >> ~/.bashrc
|
||||
echo 'export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools' >> ~/.bashrc
|
||||
# echo 'export NDK_HOME=$HOME/android-ndk-r15c' >> ~/.bashrc
|
||||
# echo 'export ANDROID_NDK_HOME=$NDK_HOME' >> ~/.bashrc
|
||||
|
||||
|
||||
source ~/.bashrc
|
||||
|
||||
# Make sure you can execute 32 bit executables if this is 64 bit machine, otherwise skip this
|
||||
dpkg --add-architecture i386
|
||||
apt-get update
|
||||
apt-get install -y libc6:i386 libstdc++6:i386 zlib1g:i386
|
|
@ -1,32 +0,0 @@
|
|||
package libv2ray
|
||||
|
||||
// This struct creates our own log writer without datatime stamp
|
||||
// As Android adds time stamps on each line
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
v2commlog "github.com/xtls/xray-core/common/log"
|
||||
)
|
||||
|
||||
type consoleLogWriter struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func (w *consoleLogWriter) Write(s string) error {
|
||||
w.logger.Print(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *consoleLogWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This logger won't print data/time stamps
|
||||
func createStdoutLogWriter() v2commlog.WriterCreator {
|
||||
return func() v2commlog.Writer {
|
||||
return &consoleLogWriter{
|
||||
logger: log.New(os.Stdout, "", 0)}
|
||||
}
|
||||
}
|
1
AndroidLibXrayLite
Submodule
1
AndroidLibXrayLite
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 8ad3e1ddf165d8d67e488346b2faa9153d3e33a4
|
41
CR.md
41
CR.md
|
@ -1,29 +1,40 @@
|
|||
v2rayNG 隐私条款
|
||||
**v2rayNG 隐私权政策**
|
||||
|
||||
最后更新 2017-11-22
|
||||
本政策自2023年11月17日起施行
|
||||
|
||||
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
||||
2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。
|
||||
|
||||
1. 信息收集
|
||||
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
||||
|
||||
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
|
||||
**信息收集**
|
||||
|
||||
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
||||
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。
|
||||
|
||||
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
||||
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
||||
|
||||
2. 信息共享
|
||||
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
||||
|
||||
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
||||
**信息共享**
|
||||
|
||||
3. 信息存留
|
||||
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
||||
|
||||
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
||||
**信息存留**
|
||||
|
||||
4. 信息泄露
|
||||
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
||||
|
||||
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
||||
**信息泄露**
|
||||
|
||||
5. 条款修改
|
||||
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
||||
|
||||
**儿童隐私**
|
||||
|
||||
这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。
|
||||
|
||||
**条款修改**
|
||||
|
||||
我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。**
|
||||
|
||||
**联系我们**
|
||||
|
||||
如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。
|
||||
|
||||
我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
|
||||
|
|
|
@ -1,228 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_widget_name">Kết nối ngay</string>
|
||||
<string name="app_tile_name">Kết nối ngay</string>
|
||||
<string name="app_tile_first_use">Vui lòng thêm một cấu hình vào v2rayNG để sử dụng.</string>
|
||||
<string name="navigation_drawer_open">Mở menu ứng dụng</string>
|
||||
<string name="navigation_drawer_close">Đóng menu ứng dụng</string>
|
||||
<string name="migration_success">Đã chuyển dữ liệu!</string>
|
||||
<string name="migration_fail">Không thể chuyển dữ liệu!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_action_stop_v2ray">Ngắt kết nối v2rayNG</string>
|
||||
<string name="toast_permission_denied">Vui lòng cấp quyền cần thiết cho v2rayNG. Bạn đã từ chối các quyền cần thiết như Camera hay Bộ nhớ.</string>
|
||||
<string name="notification_action_more">Nhấn để biết thêm</string>
|
||||
<string name="toast_services_start">Đang bắt đầu dịch vụ v2rayNG.</string>
|
||||
<string name="toast_services_stop">Đã dừng dịch vụ v2rayNG.</string>
|
||||
<string name="toast_services_success">Đã bắt đầu dịch vụ v2rayNG.</string>
|
||||
<string name="toast_services_failure">Không thể bắt đầu dịch vụ, hãy thử kiểm tra lại cấu hình hoặc khởi động lại thiết bị.</string>
|
||||
|
||||
<!--ServerActivity-->
|
||||
<string name="title_server">V2RayNG App :3</string>
|
||||
<string name="menu_item_add_config">Thêm cấu hình</string>
|
||||
<string name="menu_item_save_config">Lưu cấu hình</string>
|
||||
<string name="menu_item_del_config">Xoá cấu hình</string>
|
||||
<string name="menu_item_import_config_qrcode">Nhập cấu hình từ mã QR</string>
|
||||
<string name="menu_item_import_config_clipboard">Nhập cấu hình từ bộ nhớ tạm thời</string>
|
||||
<string name="menu_item_import_config_manually_vmess">Nhập thủ công [Vmess]</string>
|
||||
<string name="menu_item_import_config_manually_vless">Nhập thủ công [VLESS]</string>
|
||||
<string name="menu_item_import_config_manually_ss">Nhập thủ công [Shadowsocks]</string>
|
||||
<string name="menu_item_import_config_manually_socks">Nhập thủ công [Socks]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">Nhập thủ công [Trojan]</string>
|
||||
<string name="menu_item_import_config_custom">Nâng cao / Cấu hình tùy chỉnh</string>
|
||||
<string name="menu_item_import_config_custom_clipboard">Nhập cấu hình tùy chỉnh từ bộ nhớ tạm thời</string>
|
||||
<string name="menu_item_import_config_custom_local">Nhập cấu hình tùy chỉnh từ Tệp</string>
|
||||
<string name="menu_item_import_config_custom_url">Nhập cấu hình tùy chỉnh từ URL</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">Nhập cấu hình tùy chỉnh quét URL</string>
|
||||
<string name="del_config_comfirm">Bạn có muốn xóa cấu hình ?</string>
|
||||
<string name="server_lab_remarks">Tên cấu hình</string>
|
||||
<string name="server_lab_address">Địa chỉ</string>
|
||||
<string name="server_lab_port">Cổng</string>
|
||||
<string name="server_lab_id">Địa chỉ ID</string>
|
||||
<string name="server_lab_alterid">alterId</string>
|
||||
<string name="server_lab_security">Bảo mật</string>
|
||||
<string name="server_lab_network">Kiểu kết nối</string>
|
||||
<string name="server_lab_more_function">Nâng cao</string>
|
||||
<string name="server_lab_head_type">Kiểu Head</string>
|
||||
<string name="server_lab_mode_type">Chế độ gRPC</string>
|
||||
<string name="server_lab_request_host">Yêu cầu host(host/ws host/h2 host)/Bảo mật QUIC</string>
|
||||
<string name="server_lab_path">Đường dẫn (ws path/h2 path)/QUIC key/kcp seed/gRPC serviceName</string>
|
||||
<string name="server_lab_stream_security">tls</string>
|
||||
<string name="server_lab_allow_insecure">allowInsecure</string>
|
||||
<string name="server_lab_sni">Địa chỉ SNI</string>
|
||||
<string name="server_lab_address3">Địa chỉ</string>
|
||||
<string name="server_lab_port3">Cổng</string>
|
||||
<string name="server_lab_id3">Mật khẩu</string>
|
||||
<string name="server_lab_security3">Bảo mật</string>
|
||||
<string name="server_lab_id4">Mật khẩu(Bổ sung)</string>
|
||||
<string name="server_lab_security4">Tên người dùng(Bổ sung)</string>
|
||||
<string name="server_lab_encryption">Mã hoá</string>
|
||||
<string name="server_lab_flow">flow</string>
|
||||
<string name="toast_success">Đã thực hiện thành công thao tác của bạn, Nếu có gì đó không ổn, hãy thao tác lại.</string>
|
||||
<string name="toast_failure">Đã xảy ra lỗi, hãy thử kiểm tra lại hoặc thử lại.</string>
|
||||
<string name="toast_none_data">Không có gì ở đây</string>
|
||||
<string name="toast_incorrect_protocol">Không đúng protocol</string>
|
||||
<string name="toast_decoding_failed">Không thể decode</string>
|
||||
<string name="title_file_chooser">Vui lòng chọn tệp cấu hình</string>
|
||||
<string name="toast_require_file_manager">Vui lòng cài đặt trình quản lý tệp để tiếp tục.</string>
|
||||
<string name="server_customize_config">Cấu hình tùy chỉnh</string>
|
||||
<string name="toast_config_file_invalid">Cấu hình không hợp lệ</string>
|
||||
<string name="server_lab_content">Nội dung</string>
|
||||
<string name="toast_none_data_clipboard">Không có dữ liệu nào trong bộ nhớ tạm thời</string>
|
||||
<string name="toast_invalid_url">URL không hợp lệ hoặc không có gì</string>
|
||||
<string name="server_lab_need_inbound">Vui lòng đảm bảo cấu hình tùy chỉnh này không bị lỗi trước khi sử dụng. v2rayNG được Dịch Tiếng Việt bởi CuynuTT😘</string>
|
||||
<string name="toast_malformed_josn">Cấu hình không hợp lệ </string>
|
||||
<string name="server_lab_request_host6">Host(SNI)(Bổ sung)</string>
|
||||
<string name="toast_asset_copy_failed">Không thể sao chép tệp tin, hãy dùng trình quản lý tệp</string>
|
||||
<string name="menu_item_add_file">Thêm tệp</string>
|
||||
<string name="menu_item_download_file">Tải xuống tệp tin</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">Đang tải...</string>
|
||||
<string name="menu_item_search">Tìm kiếm</string>
|
||||
<string name="menu_item_select_all">Chọn tất cả</string>
|
||||
<string name="msg_enter_keywords">Nhập từ khoá</string>
|
||||
<string name="switch_bypass_apps_mode">Bỏ qua kết nối VPN</string>
|
||||
<string name="menu_item_select_proxy_app">Tự động chọn ứng dụng Proxy</string>
|
||||
<string name="msg_downloading_content">Đang tải xuống nội dung...</string>
|
||||
<string name="menu_item_export_proxy_app">Xuất và sao chép</string>
|
||||
<string name="menu_item_import_proxy_app">Nhập từ bộ nhớ tạm thời</string>
|
||||
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">Cài đặt</string>
|
||||
<string name="title_advanced">Cài đặt nâng cao</string>
|
||||
<string name="title_vpn_settings">Cài đặt cho VPN</string>
|
||||
<string name="title_pref_per_app_proxy">Proxy cho ứng dụng</string>
|
||||
<string name="summary_pref_per_app_proxy">Chung: Ứng dụng đã chọn sẽ kết nối Proxy, Chưa lựa chọn sẽ kết nối trực tiếp; \nBỏ qua kết nối: Ứng dụng được chọn sẽ trực tiếp kết nối, không lựa chọn Proxy. \nLựa chọn để tự động chọn ứng dụng Proxy trong Menu.</string>
|
||||
|
||||
<string name="title_pref_mux_enabled">Cho phép Mux</string>
|
||||
<string name="summary_pref_mux_enabled">Bật lên có thể làm tăng tốc độ mạng và chuyển mạng nhanh hơn.</string>
|
||||
|
||||
<string name="title_pref_speed_enabled">Cho phép hiển thị tốc độ mạng</string>
|
||||
<string name="summary_pref_speed_enabled">Hiển thị tốc độ mạng hiện tại trên thanh thông báo.\nBiểu tượng trên thanh trạng thái có thể thay đổi tùy vào mức sử dụng.</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">Cho phép Sniffing</string>
|
||||
<string name="summary_pref_sniffing_enabled">Thử chuyển kết nối hiện tại của bạn qua trung gian để trung gian xử lý kết nối về lại cho bạn (Mặc định là bật, hãy tắt nó nếu kết nối không ổn định.)</string>
|
||||
|
||||
<string name="title_pref_local_dns_enabled">Cho phép DNS cục bộ</string>
|
||||
<string name="summary_pref_local_dns_enabled">DNS được xử lý bởi mô đun của lõi DNS.
|
||||
(Khuyến cáo, nếu cần lộ trình Bẻ khoá LAN và
|
||||
địa chỉ mainland)</string>
|
||||
|
||||
<string name="title_pref_fake_dns_enabled">Cho phép DNS giả</string>
|
||||
<string name="summary_pref_fake_dns_enabled">DNS cục bộ trả về địa chỉ IP giả (Nhanh hơn, nhưng có thể không hoạt động với một số ứng dụng)</string>
|
||||
|
||||
<string name="title_pref_prefer_ipv6">Ưu tiên IPv6</string>
|
||||
<string name="summary_pref_prefer_ipv6">Ưu tiên sử dụng địa chỉ IPv6 cho kết nối và lộ trình.</string>
|
||||
|
||||
<string name="title_pref_routing">Lộ trình</string>
|
||||
<string name="title_pref_routing_domain_strategy">Tùy chọn tên miền</string>
|
||||
<string name="title_pref_routing_mode">Tùy chỉnh quy tắc lộ trình</string>
|
||||
<string name="title_pref_routing_custom">Tùy chỉnh lộ trình</string>
|
||||
|
||||
<string name="title_pref_remote_dns">Điều khiển DNS (Bổ sung)</string>
|
||||
<string name="summary_pref_remote_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (Chỉ IPv4/v6)</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">Domestic DNS (Bổ sung)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">Cho phép kết nối từ mạng LAN</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">Các thiết bị khác có thể kết nối đến proxy bởi địa chỉ IP thông qua socks/http, Chỉ bật khi bạn tin tưởng kết nối để tránh kết nối lạ.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">Cho phép kết nối từ mạng LAN, Đảm bảo rằng bạn tin tưởng kết nối hiện tại.</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">Cho phép đặt lại allowInsecure</string>
|
||||
<string name="summary_pref_allow_insecure">Khi kết nối TLS, đặt cài đặt allowInsecure thành mặc định</string>
|
||||
|
||||
<string name="title_pref_socks_port">Cổng Proxy SOCKS5</string>
|
||||
<string name="summary_pref_socks_port">Cổng Proxy SOCKS5</string>
|
||||
|
||||
<string name="title_pref_http_port">Cổng Proxy HTTP</string>
|
||||
<string name="summary_pref_http_port">Cổng Proxy HTTP</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">Cổng DNS cục bộ</string>
|
||||
<string name="summary_pref_local_dns_port">Cổng DNS cục bộ</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">Hiển thị thông báo xác nhận xoá cấu hình</string>
|
||||
<string name="summary_pref_confirm_remove">Hiển thị thông báo xác nhận xoá cấu hình khi bạn xoá một cấu hình.</string>
|
||||
|
||||
<string name="title_pref_feedback">Phản hồi lỗi</string>
|
||||
<string name="summary_pref_feedback">Phản hồi cải tiến hoặc bug lên GitHub</string>
|
||||
<string name="summary_pref_tg_group">Tham gia nhóm Telegram</string>
|
||||
<string name="toast_tg_app_not_found">Không tìm thấy ứng dụng Telegram</string>
|
||||
|
||||
<string name="title_pref_promotion">Quảng cáo Server</string>
|
||||
<string name="summary_pref_promotion">Quảng cáo,nhấn để biết thêm(Ủng hộ có thể được gỡ bỏ)</string>
|
||||
|
||||
<string name="title_core_loglevel">Mức độ nhật ký</string>
|
||||
<string name="title_mode">Chế độ kết nối</string>
|
||||
<string name="title_mode_help">Nhấn vào đây nếu bạn cần trợ giúp</string>
|
||||
<string name="title_language">Ngôn ngữ ứng dụng</string>
|
||||
|
||||
<string name="title_logcat">Nhật ký hoạt động</string>
|
||||
<string name="logcat_copy">Sao chép nhật ký</string>
|
||||
<string name="logcat_clear">Xoá nhật ký</string>
|
||||
<string name="title_service_restart">Kết nối lại v2rayNG</string>
|
||||
<string name="title_del_all_config">Xoá tất cả cấu hình</string>
|
||||
<string name="title_del_invalid_config">Xoá cấu hình lỗi (Kiểm tra trước)</string>
|
||||
<string name="title_export_all">Xuất và sao chép tất cả cấu hình</string>
|
||||
<string name="title_sub_setting">Các gói đăng ký</string>
|
||||
<string name="sub_setting_remarks">Tên các gói đăng ký</string>
|
||||
<string name="sub_setting_url">URL gói đăng ký</string>
|
||||
<string name="sub_setting_enable">Sử dụng gói đăng ký này</string>
|
||||
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
|
||||
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
|
||||
<string name="title_real_ping_all_server">Kiểm tra máy chủ</string>
|
||||
<string name="title_user_asset_setting">Tệp Geo assets</string>
|
||||
<string name="title_sort_by_test_results">Sắp xếp lại theo lần kiểm tra cuối cùng</string>
|
||||
<string name="title_filter_config">Lọc cấu hình theo các gói đăng ký</string>
|
||||
<string name="filter_config_all">Hiển thị tất cả các gói đăng ký</string>
|
||||
|
||||
<string name="tasker_start_service">Bắt đầu dịch vụ</string>
|
||||
<string name="tasker_setting_confirm">Xác nhận</string>
|
||||
|
||||
<string name="routing_settings_title">Cài đặt lộ trình</string>
|
||||
<string name="routing_settings_tips">Được phân cách bằng dấu chấm phẩy(,),Hãy nhớ nó để lưu lại.</string>
|
||||
<string name="routing_settings_save">Lưu lại</string>
|
||||
<string name="routing_settings_delete">Xoá</string>
|
||||
<string name="routing_settings_scan_replace">Dò và thay thế</string>
|
||||
<string name="routing_settings_scan_append">Dò và nối</string>
|
||||
<string name="routing_settings_default_rules">Đặt luật lệ lộ trình mặc định</string>
|
||||
|
||||
<string name="connection_test_pending">Kiểm tra kết nối</string>
|
||||
<string name="connection_test_testing">Đang kiểm tra kết nối mạng…</string>
|
||||
<string name="connection_test_available">Đã kiểm tra kết nối mạng thành công, Ping hiện tại là %d</string>
|
||||
<string name="connection_test_error">Lỗi kết nối mạng hãy thử đổi cấu hình hoặc kiểm tra lại. Mã lỗi: %s</string>
|
||||
<string name="connection_test_fail">Không có kết nối mạng</string>
|
||||
<string name="connection_test_error_status_code">Mã lỗi: #%d</string>
|
||||
<string name="connection_connected">Đã kết nối, hãy nhấn vào đây để kiểm tra kết nối mạng.</string>
|
||||
<string name="connection_not_connected">Chưa kết nối, hãy thêm một cấu hình để kết nối. Đừng để bị lừa đảo bởi cấu hình mất tiền,Dịch TV bởi CuynuTT😘</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>Xuất ra mã QR (Chụp màn hình để lưu)</item>
|
||||
<item>Sao chép cấu hình này</item>
|
||||
<item>Sao chép thành cấu hình tùy chỉnh</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_tag">
|
||||
<item>proxy URL hoặc IP</item>
|
||||
<item>direct URL hoặc IP</item>
|
||||
<item>URL đã chặn hoặc IP</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_mode">
|
||||
<item>Proxy Global</item>
|
||||
<item>Bẻ khoá địa chỉ LAN rồi proxy</item>
|
||||
<item>Bẻ khoá địa chỉ mainland rồi proxy</item>
|
||||
<item>Bẻ khoá LAN và địa chỉ mainland rồi proxy</item>
|
||||
<item>Trực tiếp Global</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mode_entries">
|
||||
<item>Chế độ VPN</item>
|
||||
<item>Chế độ Proxy</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
|
@ -1,226 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_widget_name">开关</string>
|
||||
<string name="app_tile_name">开关</string>
|
||||
<string name="app_tile_first_use">初次使用此功能请先用APP添加配置</string>
|
||||
<string name="navigation_drawer_open">Open navigation drawer</string>
|
||||
<string name="navigation_drawer_close">Close navigation drawer</string>
|
||||
<string name="migration_success">数据迁移成功!</string>
|
||||
<string name="migration_fail">数据迁移失败啦!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_action_stop_v2ray">停止</string>
|
||||
<string name="toast_permission_denied">无法取得权D:\vssHotel\SourceCode\Hotel.root\Hotel\Clubank.Hotel\FrontCounter\CheckoutListForm.cs限</string>
|
||||
<string name="notification_action_more">点击了解更多</string>
|
||||
<string name="toast_services_start">启动服务中</string>
|
||||
<string name="toast_services_stop">关闭中</string>
|
||||
<string name="toast_services_success">启动服务成功</string>
|
||||
<string name="toast_services_failure">启动服务失败</string>
|
||||
|
||||
<!--ServerActivity-->
|
||||
<string name="title_server">配置文件</string>
|
||||
<string name="menu_item_add_config">添加配置</string>
|
||||
<string name="menu_item_save_config">保存配置</string>
|
||||
<string name="menu_item_del_config">删除配置</string>
|
||||
<string name="menu_item_import_config_qrcode">扫描二维码</string>
|
||||
<string name="menu_item_import_config_clipboard">从剪贴板导入</string>
|
||||
<string name="menu_item_import_config_manually_vmess">手动输入[Vmess]</string>
|
||||
<string name="menu_item_import_config_manually_vless">手动输入[VLESS]</string>
|
||||
<string name="menu_item_import_config_manually_ss">手动输入[Shadowsocks]</string>
|
||||
<string name="menu_item_import_config_manually_socks">手动输入[Socks]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">手动输入[Trojan]</string>
|
||||
<string name="menu_item_import_config_custom">自定义配置</string>
|
||||
<string name="menu_item_import_config_custom_clipboard">从剪贴板导入自定义配置</string>
|
||||
<string name="menu_item_import_config_custom_local">从本地导入自定义配置</string>
|
||||
<string name="menu_item_import_config_custom_url">剪贴板URL导入自定义配置</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">扫描URL导入自定义配置</string>
|
||||
<string name="del_config_comfirm">确认删除?</string>
|
||||
<string name="server_lab_remarks">别名(remarks)</string>
|
||||
<string name="server_lab_address">地址(address)</string>
|
||||
<string name="server_lab_port">端口(port)</string>
|
||||
<string name="server_lab_id">用户ID(id)</string>
|
||||
<string name="server_lab_alterid">额外ID(alterId)</string>
|
||||
<string name="server_lab_security">加密方式(security)</string>
|
||||
<string name="server_lab_network">传输协议(network)</string>
|
||||
<string name="server_lab_more_function">底层传输方式(transport)</string>
|
||||
<string name="server_lab_head_type">伪装类型(type)</string>
|
||||
<string name="server_lab_mode_type">gRPC 传输模式 (mode)</string>
|
||||
<string name="server_lab_request_host">伪装域名(host)(host/ws host/h2 host)/QUIC 加密方式</string>
|
||||
<string name="server_lab_path">path(ws path/h2 path)/QUIC 加密密钥/kcp seed/gRPC serviceName</string>
|
||||
<string name="server_lab_stream_security">传输层安全(tls)</string>
|
||||
<string name="server_lab_allow_insecure">跳过证书验证(allowInsecure)</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">服务器地址</string>
|
||||
<string name="server_lab_port3">服务器端口</string>
|
||||
<string name="server_lab_id3">密码</string>
|
||||
<string name="server_lab_security3">加密方式</string>
|
||||
<string name="server_lab_id4">密码(可选)</string>
|
||||
<string name="server_lab_security4">用户名(可选)</string>
|
||||
<string name="server_lab_encryption">加密(encryption)</string>
|
||||
<string name="server_lab_flow">流控(flow)</string>
|
||||
<string name="toast_success">成功</string>
|
||||
<string name="toast_failure">失败</string>
|
||||
<string name="toast_none_data">没有数据</string>
|
||||
<string name="toast_incorrect_protocol">不正确的协议</string>
|
||||
<string name="toast_decoding_failed">解码失败</string>
|
||||
<string name="title_file_chooser">选择一个配置文件</string>
|
||||
<string name="toast_require_file_manager">请安装一个文件管理器</string>
|
||||
<string name="server_customize_config">自定义配置</string>
|
||||
<string name="toast_config_file_invalid">无效的配置文件</string>
|
||||
<string name="server_lab_content">内容</string>
|
||||
<string name="toast_none_data_clipboard">剪贴板中没有数据</string>
|
||||
<string name="toast_invalid_url">无效的网址</string>
|
||||
<string name="server_lab_need_inbound">确保inbounds port和设置中的一致</string>
|
||||
<string name="toast_malformed_josn">配置格式错误</string>
|
||||
<string name="server_lab_request_host6">Host(SNI)(可选)</string>
|
||||
<string name="toast_asset_copy_failed">失败, 请使用文件管理器</string>
|
||||
<string name="menu_item_add_file">添加文件</string>
|
||||
<string name="menu_item_download_file">下载文件</string>
|
||||
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">正在加载</string>
|
||||
<string name="menu_item_search">搜索</string>
|
||||
<string name="menu_item_select_all">全选</string>
|
||||
<string name="msg_enter_keywords">输入关键字</string>
|
||||
<string name="switch_bypass_apps_mode">绕行模式</string>
|
||||
<string name="menu_item_select_proxy_app">自动选中需代理应用</string>
|
||||
<string name="msg_downloading_content">正在下载内容</string>
|
||||
<string name="menu_item_export_proxy_app">导出至剪贴板</string>
|
||||
<string name="menu_item_import_proxy_app">从剪贴板导入</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">设置</string>
|
||||
<string name="title_advanced">进阶设置</string>
|
||||
<string name="title_vpn_settings">VPN 设置</string>
|
||||
<string name="title_pref_per_app_proxy">分应用代理</string>
|
||||
<string name="summary_pref_per_app_proxy">常规:勾选的App被代理,未勾选的直连;\n绕行模式:勾选的App直连,未勾选的被代理.\n不明白者在菜单中选择自动选中需代理应用</string>
|
||||
|
||||
<string name="title_pref_mux_enabled">启用Mux多路复用</string>
|
||||
<string name="summary_pref_mux_enabled">开启可能会加速,关闭可能会减少断流</string>
|
||||
|
||||
<string name="title_pref_speed_enabled">启用速度显示</string>
|
||||
<string name="summary_pref_speed_enabled">在通知中显示当前速度\n小图标显示流量的路由情况</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">启用流量探测</string>
|
||||
<string name="summary_pref_sniffing_enabled">从流量中探测域名 (默认启用)</string>
|
||||
|
||||
<string name="title_pref_local_dns_enabled">启用本地DNS</string>
|
||||
<string name="summary_pref_local_dns_enabled">DNS 请求导入 core 由 DNS 模块处理(推荐启用 如果需要路由绕过局域网及大陆地址)</string>
|
||||
|
||||
<string name="title_pref_fake_dns_enabled">启用虚拟DNS</string>
|
||||
<string name="summary_pref_fake_dns_enabled">本地返回虚构解析结果 (减低延时 但个别应用可能无法使用)</string>
|
||||
|
||||
<string name="title_pref_prefer_ipv6">IPv6优先</string>
|
||||
<string name="summary_pref_prefer_ipv6">App优先使用IPv6地址连接服务器,同时开启VPN的IPv6路由</string>
|
||||
|
||||
<string name="title_pref_routing">路由设置</string>
|
||||
<string name="title_pref_routing_domain_strategy">域名策略</string>
|
||||
<string name="title_pref_routing_mode">预定义规则</string>
|
||||
<string name="title_pref_routing_custom">自定义规则</string>
|
||||
|
||||
<string name="title_pref_remote_dns">远程DNS (可选)</string>
|
||||
<string name="summary_pref_remote_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (仅支持 IPv4/v6)</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">境内DNS (可选)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">允许来自局域网的连接</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">其他设备可以使用socks/http协议通过您的IP地址连接到代理,仅在受信任的网络中启用以避免未经授权的连接</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">允许来自局域网的连接,请确保处于受信网络</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">跳过证书验证(allowInsecure)</string>
|
||||
<string name="summary_pref_allow_insecure">传输层安全选tls时,默认跳过证书验证(allowInsecure)</string>
|
||||
|
||||
<string name="title_pref_socks_port">SOCKS5代理端口</string>
|
||||
<string name="summary_pref_socks_port">SOCKS5代理端口</string>
|
||||
|
||||
<string name="title_pref_http_port">HTTP代理端口</string>
|
||||
<string name="summary_pref_http_port">HTTP代理端口</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">本地DNS端口</string>
|
||||
<string name="summary_pref_local_dns_port">本地DNS端口</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">删除配置文件确认</string>
|
||||
<string name="summary_pref_confirm_remove">删除配置文件是否需要用户二次确认</string>
|
||||
|
||||
<string name="title_pref_feedback">反馈</string>
|
||||
<string name="summary_pref_feedback">反馈改进或漏洞至 GitHub</string>
|
||||
<string name="summary_pref_tg_group">加入Telegram Group</string>
|
||||
<string name="toast_tg_app_not_found">未找到Telegram app</string>
|
||||
|
||||
<string name="title_pref_promotion">推广</string>
|
||||
<string name="summary_pref_promotion">一些推广,点击查看详情(捐赠可去除)</string>
|
||||
|
||||
<string name="title_core_loglevel">日志级别</string>
|
||||
<string name="title_mode">模式</string>
|
||||
<string name="title_mode_help">点此查看更多帮助</string>
|
||||
<string name="title_language">语言</string>
|
||||
|
||||
<string name="title_logcat">Logcat</string>
|
||||
<string name="logcat_copy">复制</string>
|
||||
<string name="logcat_clear">清除</string>
|
||||
<string name="title_service_restart">服务重启</string>
|
||||
<string name="title_del_all_config">删除全部配置</string>
|
||||
<string name="title_del_invalid_config">删除无效配置(先测试)</string>
|
||||
<string name="title_export_all">导出全部(非自定义)配置至剪贴板</string>
|
||||
<string name="title_sub_setting">订阅分组设置</string>
|
||||
<string name="sub_setting_remarks">备注</string>
|
||||
<string name="sub_setting_url">可选地址(url)</string>
|
||||
<string name="sub_setting_enable">启用更新</string>
|
||||
<string name="title_sub_update">更新订阅</string>
|
||||
<string name="title_ping_all_server">测试全部配置Tcping</string>
|
||||
<string name="title_real_ping_all_server">测试全部配置真连接</string>
|
||||
<string name="title_user_asset_setting">Geo 资源文件</string>
|
||||
<string name="title_sort_by_test_results">按测试结果排序</string>
|
||||
<string name="title_filter_config">过滤配置文件</string>
|
||||
<string name="filter_config_all">所有订阅分组</string>
|
||||
|
||||
<string name="tasker_start_service">启动服务</string>
|
||||
<string name="tasker_setting_confirm">确定</string>
|
||||
|
||||
<string name="routing_settings_title">路由设置</string>
|
||||
<string name="routing_settings_tips">用逗号(,)隔开,可以一行多个,记得保存</string>
|
||||
<string name="routing_settings_save">保存</string>
|
||||
<string name="routing_settings_delete">清空</string>
|
||||
<string name="routing_settings_scan_replace">扫描并替换</string>
|
||||
<string name="routing_settings_scan_append">扫描并追加</string>
|
||||
<string name="routing_settings_default_rules">设置默认路由规则</string>
|
||||
|
||||
<string name="connection_test_pending">"检查网络连接"</string>
|
||||
<string name="connection_test_testing">"测试中…"</string>
|
||||
<string name="connection_test_available">"连接成功:延时 %d 毫秒"</string>
|
||||
<string name="connection_test_error">"失败:%s"</string>
|
||||
<string name="connection_test_fail">"无互联网连接"</string>
|
||||
<string name="connection_test_error_status_code">"状态码无效(#%d)"</string>
|
||||
<string name="connection_connected">"已连接,点击测试连接"</string>
|
||||
<string name="connection_not_connected">"未连接"</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>二维码</item>
|
||||
<item>导出至剪贴板</item>
|
||||
<item>导出完整配置至剪贴板</item>
|
||||
</string-array>
|
||||
share_method
|
||||
<string-array name="routing_tag">
|
||||
<item>代理的网址或IP</item>
|
||||
<item>直连的网址或IP</item>
|
||||
<item>阻止的网址或IP</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_mode">
|
||||
<item>全局代理</item>
|
||||
<item>绕过局域网地址而后代理</item>
|
||||
<item>绕过大陆地址而后代理</item>
|
||||
<item>绕过局域网及大陆地址而后代理</item>
|
||||
<item>全局直连</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mode_entries">
|
||||
<item>VPN</item>
|
||||
<item>仅代理</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
|
@ -1,226 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_widget_name">開關</string>
|
||||
<string name="app_tile_name">開關</string>
|
||||
<string name="app_tile_first_use">首次使用此功能,請使用此應用程式新增伺服器</string>
|
||||
<string name="navigation_drawer_open">開啟導覽匣</string>
|
||||
<string name="navigation_drawer_close">關閉導覽匣</string>
|
||||
<string name="migration_success">資料遷移成功!</string>
|
||||
<string name="migration_fail">資料遷移失敗!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_action_stop_v2ray">停止</string>
|
||||
<string name="toast_permission_denied">無法取得此權限</string>
|
||||
<string name="notification_action_more">瞭解更多</string>
|
||||
<string name="toast_services_start">啟動服務</string>
|
||||
<string name="toast_services_stop">停止服務</string>
|
||||
<string name="toast_services_success">啟動服務成功</string>
|
||||
<string name="toast_services_failure">啟動服務失敗</string>
|
||||
|
||||
<!--ServerActivity-->
|
||||
<string name="title_server">組態檔案</string>
|
||||
<string name="menu_item_add_config">新增組態</string>
|
||||
<string name="menu_item_save_config">儲存組態</string>
|
||||
<string name="menu_item_del_config">刪除組態</string>
|
||||
<string name="menu_item_import_config_qrcode">從 QR Code 匯入組態</string>
|
||||
<string name="menu_item_import_config_clipboard">從剪貼簿匯入組態</string>
|
||||
<string name="menu_item_import_config_manually_vmess">手動鍵入 [Vmess]</string>
|
||||
<string name="menu_item_import_config_manually_vless">手動鍵入 [VLESS]</string>
|
||||
<string name="menu_item_import_config_manually_ss">手動鍵入 [Shadowsocks]</string>
|
||||
<string name="menu_item_import_config_manually_socks">手動鍵入 [Socks]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">手動鍵入 [Trojan]</string>
|
||||
<string name="menu_item_import_config_custom">自訂組態</string>
|
||||
<string name="menu_item_import_config_custom_clipboard">從剪貼簿匯入自訂組態</string>
|
||||
<string name="menu_item_import_config_custom_local">從 URL 匯入自訂組態</string>
|
||||
<string name="menu_item_import_config_custom_url">從 URL 匯入自訂組態</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">掃描 URL 匯入自訂組態</string>
|
||||
<string name="del_config_comfirm">確定刪除?</string>
|
||||
<string name="server_lab_remarks">備註</string>
|
||||
<string name="server_lab_address">位址</string>
|
||||
<string name="server_lab_port">埠</string>
|
||||
<string name="server_lab_id">使用者 ID</string>
|
||||
<string name="server_lab_alterid">alterId</string>
|
||||
<string name="server_lab_security">安全性</string>
|
||||
<string name="server_lab_network">網路</string>
|
||||
<string name="server_lab_more_function">底層傳輸方式 (transport)</string>
|
||||
<string name="server_lab_head_type">標頭類型</string>
|
||||
<string name="server_lab_mode_type">gRPC 傳輸模式 (mode)</string>
|
||||
<string name="server_lab_request_host">要求主機 (host)(host/ws host/h2 host)/QUIC 加密方式</string>
|
||||
<string name="server_lab_path">path(ws path/h2 path)/QUIC 加密金鑰/kcp seed/gRPC serviceName</string>
|
||||
<string name="server_lab_stream_security">傳輸層安全 (tls)</string>
|
||||
<string name="server_lab_allow_insecure">跳過憑證驗證 (allowInsecure)</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">伺服器位址</string>
|
||||
<string name="server_lab_port3">伺服器埠</string>
|
||||
<string name="server_lab_id3">密碼</string>
|
||||
<string name="server_lab_security3">加密方式</string>
|
||||
<string name="server_lab_id4">密碼 (可選)</string>
|
||||
<string name="server_lab_security4">使用者名稱 (可選)</string>
|
||||
<string name="server_lab_encryption">加密 (encryption)</string>
|
||||
<string name="server_lab_flow">流程 (flow)</string>
|
||||
<string name="toast_success">成功</string>
|
||||
<string name="toast_failure">失敗</string>
|
||||
<string name="toast_none_data">無資料</string>
|
||||
<string name="toast_incorrect_protocol">通訊協定不正確</string>
|
||||
<string name="toast_decoding_failed">解碼失敗</string>
|
||||
<string name="title_file_chooser">選取一個組態檔</string>
|
||||
<string name="toast_require_file_manager">請安裝檔案總管。</string>
|
||||
<string name="server_customize_config">自訂組態</string>
|
||||
<string name="toast_config_file_invalid">無效組態</string>
|
||||
<string name="server_lab_content">內容</string>
|
||||
<string name="toast_none_data_clipboard">剪貼簿內無資料</string>
|
||||
<string name="toast_invalid_url">URL 無效</string>
|
||||
<string name="server_lab_need_inbound">確保 inbounds port 和設定中的一致</string>
|
||||
<string name="toast_malformed_josn">組態格式不正確</string>
|
||||
<string name="server_lab_request_host6">Host(SNI)(可選)</string>
|
||||
<string name="toast_asset_copy_failed">失敗,請使用檔案總管</string>
|
||||
<string name="menu_item_add_file">新增檔案</string>
|
||||
<string name="menu_item_download_file">下載檔案</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">載入</string>
|
||||
<string name="menu_item_search">搜尋</string>
|
||||
<string name="menu_item_select_all">全選</string>
|
||||
<string name="msg_enter_keywords">輸入關鍵字</string>
|
||||
<string name="switch_bypass_apps_mode">略過模式</string>
|
||||
<string name="menu_item_select_proxy_app">自動選中需 Proxy 應用</string>
|
||||
<string name="msg_downloading_content">正在下載內容</string>
|
||||
<string name="menu_item_export_proxy_app">匯出至剪貼簿</string>
|
||||
<string name="menu_item_import_proxy_app">從剪貼簿匯入</string>
|
||||
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">設定</string>
|
||||
<string name="title_advanced">進階</string>
|
||||
<string name="title_vpn_settings">VPN 設定</string>
|
||||
<string name="title_pref_per_app_proxy">Proxy 個別應用程式</string>
|
||||
<string name="summary_pref_per_app_proxy">常規:勾選的 App 啟用 Proxy,未勾選的直接連線;\n繞行模式:勾選的 App 直接連線,未勾選的啟用 Proxy。\n可在選單中選擇自動選中需 Proxy 應用</string>
|
||||
|
||||
<string name="title_pref_mux_enabled">啟用 Mux</string>
|
||||
<string name="summary_pref_mux_enabled">啟用或許會加快網路速度,切換或許會閃爍</string>
|
||||
|
||||
<string name="title_pref_speed_enabled">啟用速度顯示</string>
|
||||
<string name="summary_pref_speed_enabled">在通知中顯示當前速度\n小圖示顯示流量的轉送狀況</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">啟用流量監聽</string>
|
||||
<string name="summary_pref_sniffing_enabled">從流量中監聽網域 (預設啟用)</string>
|
||||
|
||||
<string name="title_pref_local_dns_enabled">啟用本機 DNS</string>
|
||||
<string name="summary_pref_local_dns_enabled">DNS 請求匯入 core 由 DNS 模塊處理 (建議啟用,如果需要轉送略過區域網路及中國大陸)</string>
|
||||
|
||||
<string name="title_pref_fake_dns_enabled">啟用假 DNS</string>
|
||||
<string name="summary_pref_fake_dns_enabled">本機退回假解析結果 (減低延時,但個別應用可能無法使用)</string>
|
||||
|
||||
<string name="title_pref_prefer_ipv6">IPv6 偏好</string>
|
||||
<string name="summary_pref_prefer_ipv6">App 優先使用 IPv6 位址連線伺服器,同时開啟 VPN 的 IPv6 路由</string>
|
||||
|
||||
<string name="title_pref_routing">轉送設定</string>
|
||||
<string name="title_pref_routing_domain_strategy">網域策略</string>
|
||||
<string name="title_pref_routing_mode">轉送模式</string>
|
||||
<string name="title_pref_routing_custom">自訂轉送</string>
|
||||
|
||||
<string name="title_pref_remote_dns">遠端 DNS (可選)</string>
|
||||
<string name="summary_pref_remote_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (僅支援 IPv4/v6)</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">國內 DNS (可選)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">允許來自區域網路的連線</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">其他裝置可以使用 socks/http 協定透過您的 IP 位址連線到 Proxy,僅在受信任的網路中啟用以避免未經授權的連線</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">允許來自區域網路的連線,請確保處於受信網路</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">跳過憑證驗證 (allowInsecure)</string>
|
||||
<string name="summary_pref_allow_insecure">傳輸層安全選 tls 時,預設跳過憑證驗證 (allowInsecure)</string>
|
||||
|
||||
<string name="title_pref_socks_port">SOCKS5 Proxy 埠</string>
|
||||
<string name="summary_pref_socks_port">SOCKS5 Proxy 埠</string>
|
||||
|
||||
<string name="title_pref_http_port">HTTP Proxy 埠</string>
|
||||
<string name="summary_pref_http_port">HTTP Proxy 埠</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">本機 DNS 埠</string>
|
||||
<string name="summary_pref_local_dns_port">本機 DNS 埠</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">刪除配置文件確認</string>
|
||||
<string name="summary_pref_confirm_remove">刪除配置文件是否需要用戶二次確認</string>
|
||||
|
||||
<string name="title_pref_feedback">意見回饋</string>
|
||||
<string name="summary_pref_feedback">前往 GitHub 回報錯誤</string>
|
||||
<string name="summary_pref_tg_group">加入 Telegram 群組</string>
|
||||
<string name="toast_tg_app_not_found">未找到 Telegram 應用程式</string>
|
||||
|
||||
<string name="title_pref_promotion">推廣</string>
|
||||
<string name="summary_pref_promotion">一些推廣,輕觸以檢視 (捐贈可去除)</string>
|
||||
|
||||
<string name="title_core_loglevel">記錄層級</string>
|
||||
<string name="title_mode">模式</string>
|
||||
<string name="title_mode_help">輕觸以檢視說明</string>
|
||||
<string name="title_language">語言</string>
|
||||
|
||||
<string name="title_logcat">Logcat</string>
|
||||
<string name="logcat_copy">複製</string>
|
||||
<string name="logcat_clear">清除</string>
|
||||
<string name="title_service_restart">服務重啟</string>
|
||||
<string name="title_del_all_config">刪除全部組態</string>
|
||||
<string name="title_del_invalid_config">刪除無效組態 (先偵測)</string>
|
||||
<string name="title_export_all">匯出全部 (非自訂) 組態至剪貼簿</string>
|
||||
<string name="title_sub_setting">訂閱分組設定</string>
|
||||
<string name="sub_setting_remarks">備註</string>
|
||||
<string name="sub_setting_url">Optional URL</string>
|
||||
<string name="sub_setting_enable">啟用更新</string>
|
||||
<string name="title_sub_update">更新訂閱</string>
|
||||
<string name="title_ping_all_server">偵測所有組態 Tcping</string>
|
||||
<string name="title_real_ping_all_server">偵測所有組態真延遲</string>
|
||||
<string name="title_user_asset_setting">Geo 資源檔案</string>
|
||||
<string name="title_sort_by_test_results">依偵測結果排序</string>
|
||||
<string name="title_filter_config">過濾組態</string>
|
||||
<string name="filter_config_all">所有訂閱分組</string>
|
||||
|
||||
<string name="tasker_start_service">啟動服務</string>
|
||||
<string name="tasker_setting_confirm">確定</string>
|
||||
|
||||
<string name="routing_settings_title">轉送設定</string>
|
||||
<string name="routing_settings_tips">以半形逗號「,」分隔,並手動儲存</string>
|
||||
<string name="routing_settings_save">儲存</string>
|
||||
<string name="routing_settings_delete">清除</string>
|
||||
<string name="routing_settings_scan_replace">掃描並取代</string>
|
||||
<string name="routing_settings_scan_append">掃描並附加</string>
|
||||
<string name="routing_settings_default_rules">設定預設轉送規則</string>
|
||||
|
||||
<string name="connection_test_pending">"測試連線能力"</string>
|
||||
<string name="connection_test_testing">"測試中……"</string>
|
||||
<string name="connection_test_available">"成功:%d ms延遲"</string>
|
||||
<string name="connection_test_error">"測試網際網路連線失敗:%s"</string>
|
||||
<string name="connection_test_fail">"無法使用網際網路"</string>
|
||||
<string name="connection_test_error_status_code">"錯誤碼:(#%d)"</string>
|
||||
<string name="connection_connected">"已連線,輕觸以檢查連線能力"</string>
|
||||
<string name="connection_not_connected">"未連線"</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>QR Code</item>
|
||||
<item>匯出至剪貼簿</item>
|
||||
<item>匯出完整組態至剪貼簿</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_tag">
|
||||
<item>Proxy URL 或 IP</item>
|
||||
<item>直接連線 URL 或 IP</item>
|
||||
<item>已封鎖的 URL 或 IP</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_mode">
|
||||
<item>全域 Proxy</item>
|
||||
<item>略過區域網路的 Proxy</item>
|
||||
<item>略過中國大陸的 Proxy</item>
|
||||
<item>略過區域網路及中國大陸的 Proxy</item>
|
||||
<item>直接連線</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mode_entries">
|
||||
<item>VPN</item>
|
||||
<item>僅 Proxy</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
|
@ -1,229 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">v2rayNG</string>
|
||||
<string name="app_widget_name">Switch</string>
|
||||
<string name="app_tile_name">Switch</string>
|
||||
<string name="app_tile_first_use">First use of this feature, please use the app to add server</string>
|
||||
<string name="navigation_drawer_open">Open navigation drawer</string>
|
||||
<string name="navigation_drawer_close">Close navigation drawer</string>
|
||||
<string name="migration_success">Data migration success!</string>
|
||||
<string name="migration_fail">Data migration failed!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_action_stop_v2ray">Stop</string>
|
||||
<string name="toast_permission_denied">Unable to obtain the permission</string>
|
||||
<string name="notification_action_more">click for more</string>
|
||||
<string name="toast_services_start">Start Services</string>
|
||||
<string name="toast_services_stop">Stop Services</string>
|
||||
<string name="toast_services_success">Start Services Success</string>
|
||||
<string name="toast_services_failure">Start Services Failure</string>
|
||||
|
||||
<!--ServerActivity-->
|
||||
<string name="title_server">Configuration file</string>
|
||||
<string name="menu_item_add_config">Add config</string>
|
||||
<string name="menu_item_save_config">Save config</string>
|
||||
<string name="menu_item_del_config">Delete config</string>
|
||||
<string name="menu_item_import_config_qrcode">Import config from QRcode</string>
|
||||
<string name="menu_item_import_config_clipboard">Import config from Clipboard</string>
|
||||
<string name="menu_item_import_config_manually_vmess">Type manually[Vmess]</string>
|
||||
<string name="menu_item_import_config_manually_vless">Type manually[VLESS]</string>
|
||||
<string name="menu_item_import_config_manually_ss">Type manually[Shadowsocks]</string>
|
||||
<string name="menu_item_import_config_manually_socks">Type manually[Socks]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">Type manually[Trojan]</string>
|
||||
<string name="menu_item_import_config_custom">custom config</string>
|
||||
<string name="menu_item_import_config_custom_clipboard">Import custom config from Clipboard</string>
|
||||
<string name="menu_item_import_config_custom_local">Import custom config from locally</string>
|
||||
<string name="menu_item_import_config_custom_url">Import custom config from URL</string>
|
||||
<string name="menu_item_import_config_custom_url_scan">Import custom config scan URL</string>
|
||||
<string name="del_config_comfirm">Confirm delete?</string>
|
||||
<string name="server_lab_remarks">remarks</string>
|
||||
<string name="server_lab_address">address</string>
|
||||
<string name="server_lab_port">port</string>
|
||||
<string name="server_lab_id">id</string>
|
||||
<string name="server_lab_alterid">alterId</string>
|
||||
<string name="server_lab_security">security</string>
|
||||
<string name="server_lab_network">Network</string>
|
||||
<string name="server_lab_more_function">Transport</string>
|
||||
<string name="server_lab_head_type">head type</string>
|
||||
<string name="server_lab_mode_type">gRPC mode</string>
|
||||
<string name="server_lab_request_host">request host(host/ws host/h2 host)/QUIC security</string>
|
||||
<string name="server_lab_path">path(ws path/h2 path)/QUIC key/kcp seed/gRPC serviceName</string>
|
||||
<string name="server_lab_stream_security">tls</string>
|
||||
<string name="server_lab_allow_insecure">allowInsecure</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
<string name="server_lab_address3">address</string>
|
||||
<string name="server_lab_port3">port</string>
|
||||
<string name="server_lab_id3">password</string>
|
||||
<string name="server_lab_security3">security</string>
|
||||
<string name="server_lab_id4">Password(Optional)</string>
|
||||
<string name="server_lab_security4">User(Optional)</string>
|
||||
<string name="server_lab_encryption">encryption</string>
|
||||
<string name="server_lab_flow">flow</string>
|
||||
<string name="toast_success">Success</string>
|
||||
<string name="toast_failure">Failure</string>
|
||||
<string name="toast_none_data">There is nothing</string>
|
||||
<string name="toast_incorrect_protocol">Incorrect protocol</string>
|
||||
<string name="toast_decoding_failed">Decoding failed</string>
|
||||
<string name="title_file_chooser">Select a Config File</string>
|
||||
<string name="toast_require_file_manager">Please install a File Manager.</string>
|
||||
<string name="server_customize_config">Customize Config</string>
|
||||
<string name="toast_config_file_invalid">Invalid Config</string>
|
||||
<string name="server_lab_content">Content</string>
|
||||
<string name="toast_none_data_clipboard">There is no data in the clipboard</string>
|
||||
<string name="toast_invalid_url">Invalid URL</string>
|
||||
<string name="server_lab_need_inbound">Ensure inbounds port is consistent with the settings</string>
|
||||
<string name="toast_malformed_josn">Config malformed</string>
|
||||
<string name="server_lab_request_host6">Host(SNI)(Optional)</string>
|
||||
<string name="toast_asset_copy_failed">File copy failed, please use File Manager</string>
|
||||
<string name="menu_item_add_file">Add files</string>
|
||||
<string name="menu_item_download_file">Download files</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">Loading</string>
|
||||
<string name="menu_item_search">Search</string>
|
||||
<string name="menu_item_select_all">Select all</string>
|
||||
<string name="msg_enter_keywords">Enter keywords</string>
|
||||
<string name="switch_bypass_apps_mode">Bypass Mode</string>
|
||||
<string name="menu_item_select_proxy_app">Auto select proxy app</string>
|
||||
<string name="msg_downloading_content">Downloading content</string>
|
||||
<string name="menu_item_export_proxy_app">Export to Clipboard</string>
|
||||
<string name="menu_item_import_proxy_app">Import from Clipboard</string>
|
||||
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">Settings</string>
|
||||
<string name="title_advanced">Advanced Settings</string>
|
||||
<string name="title_vpn_settings">VPN Settings</string>
|
||||
<string name="title_pref_per_app_proxy">Per-app proxy</string>
|
||||
<string name="summary_pref_per_app_proxy">General: Checked App is proxy, unchecked direct connection; \nbypass mode: checked app directly connected, unchecked proxy. \nThe option to automatically select the proxy application in the menu</string>
|
||||
|
||||
<string name="title_pref_mux_enabled">Enable Mux</string>
|
||||
<string name="summary_pref_mux_enabled">Enable maybe speed up network and switch network maybe flash</string>
|
||||
|
||||
<string name="title_pref_speed_enabled">Enable speed display</string>
|
||||
<string name="summary_pref_speed_enabled">Display current speed in the notification.\nNotification icon would change based on
|
||||
usage.</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">Enable Sniffing</string>
|
||||
<string name="summary_pref_sniffing_enabled">Try sniff domain from the packet (default on)</string>
|
||||
|
||||
<string name="title_pref_local_dns_enabled">Enable local DNS</string>
|
||||
<string name="summary_pref_local_dns_enabled">DNS processed by core‘s DNS module (Recommended, if need routing Bypassing LAN and
|
||||
mainland address)</string>
|
||||
|
||||
<string name="title_pref_fake_dns_enabled">Enable fake DNS</string>
|
||||
<string name="summary_pref_fake_dns_enabled">local DNS returns fake IP address (faster, but it may not work for some apps)</string>
|
||||
|
||||
<string name="title_pref_prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="summary_pref_prefer_ipv6">Prefer IPv6 address and routes</string>
|
||||
|
||||
<string name="title_pref_routing">Routing</string>
|
||||
<string name="title_pref_routing_domain_strategy">Domain strategy</string>
|
||||
<string name="title_pref_routing_mode">Predefined rules</string>
|
||||
<string name="title_pref_routing_custom">Custom rules</string>
|
||||
|
||||
<string name="title_pref_remote_dns">Remote DNS (Optional)</string>
|
||||
<string name="summary_pref_remote_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (only IPv4/v6)</string>
|
||||
|
||||
<string name="title_pref_domestic_dns">Domestic DNS (Optional)</string>
|
||||
<string name="summary_pref_domestic_dns">DNS</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">Allow connections from the LAN</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">Other devices can connect to proxy by your ip address through socks/http, Only enable in trusted network to avoid unauthorized connection</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">Allow connections from the LAN, Make sure you are in a trusted network</string>
|
||||
|
||||
<string name="title_pref_allow_insecure">allowInsecure</string>
|
||||
<string name="summary_pref_allow_insecure">When TLS, the default allowInsecure</string>
|
||||
|
||||
<string name="title_pref_socks_port">SOCKS5 proxy port</string>
|
||||
<string name="summary_pref_socks_port">SOCKS5 proxy port</string>
|
||||
|
||||
<string name="title_pref_http_port">HTTP proxy port</string>
|
||||
<string name="summary_pref_http_port">HTTP proxy port</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">Local DNS port</string>
|
||||
<string name="summary_pref_local_dns_port">Local DNS port</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">Delete configuration file confirmation</string>
|
||||
<string name="summary_pref_confirm_remove">Whether to delete the configuration file requires a second confirmation by the user</string>
|
||||
|
||||
<string name="title_pref_feedback">Feedback</string>
|
||||
<string name="summary_pref_feedback">Feedback enhancements or bugs to GitHub</string>
|
||||
<string name="summary_pref_tg_group">Join Telegram Group</string>
|
||||
<string name="toast_tg_app_not_found">Telegram app not found</string>
|
||||
|
||||
<string name="title_pref_promotion">Promotion</string>
|
||||
<string name="summary_pref_promotion">Promotion,click for details(Donation can be removed)</string>
|
||||
|
||||
<string name="title_core_loglevel">Log Level</string>
|
||||
<string name="title_mode">Mode</string>
|
||||
<string name="title_mode_help">Click me for more help</string>
|
||||
<string name="title_language">Language</string>
|
||||
|
||||
<string name="title_logcat">Logcat</string>
|
||||
<string name="logcat_copy">Copy</string>
|
||||
<string name="logcat_clear">Clear</string>
|
||||
<string name="title_service_restart">Service restart</string>
|
||||
<string name="title_del_all_config">Delete all config</string>
|
||||
<string name="title_del_invalid_config">Delete invalid config(Test first)</string>
|
||||
<string name="title_export_all">Export non-custom configs to clipboard</string>
|
||||
<string name="title_sub_setting">Subscription group setting</string>
|
||||
<string name="sub_setting_remarks">remarks</string>
|
||||
<string name="sub_setting_url">Optional URL</string>
|
||||
<string name="sub_setting_enable">enable update</string>
|
||||
<string name="title_sub_update">Update subscription</string>
|
||||
<string name="title_ping_all_server">Tcping all configuration</string>
|
||||
<string name="title_real_ping_all_server">Real delay all configuration</string>
|
||||
<string name="title_user_asset_setting">Geo asset files</string>
|
||||
<string name="title_sort_by_test_results">Sorting by test results</string>
|
||||
<string name="title_filter_config">Filter configuration file</string>
|
||||
<string name="filter_config_all">All subscription groups</string>
|
||||
|
||||
<string name="tasker_start_service">Start Service</string>
|
||||
<string name="tasker_setting_confirm">Confirm</string>
|
||||
|
||||
<string name="routing_settings_title">Routing Settings</string>
|
||||
<string name="routing_settings_tips">Separated by commas(,),remember to save</string>
|
||||
<string name="routing_settings_save">Save</string>
|
||||
<string name="routing_settings_delete">Clear</string>
|
||||
<string name="routing_settings_scan_replace">Scan and replace</string>
|
||||
<string name="routing_settings_scan_append">Scan and append</string>
|
||||
<string name="routing_settings_default_rules"> set default routing rules</string>
|
||||
|
||||
<string name="connection_test_pending">Check Connectivity</string>
|
||||
<string name="connection_test_testing">Testing…</string>
|
||||
<string name="connection_test_available">Success: HTTP connection took %dms</string>
|
||||
<string name="connection_test_error">Fail to detect internet connection: %s</string>
|
||||
<string name="connection_test_fail">Internet Unavailable</string>
|
||||
<string name="connection_test_error_status_code">Error code: #%d</string>
|
||||
<string name="connection_connected">Connected, tap to check connection</string>
|
||||
<string name="connection_not_connected">Not connected</string>
|
||||
|
||||
<string-array name="share_method">
|
||||
<item>QRcode</item>
|
||||
<item>Export to clipboard</item>
|
||||
<item>Export full configuration to clipboard</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_tag">
|
||||
<item>proxy URL or IP</item>
|
||||
<item>direct URL or IP</item>
|
||||
<item>blocked URL or IP</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="routing_mode">
|
||||
<item>Global proxy</item>
|
||||
<item>Bypassing the LAN address then proxy</item>
|
||||
<item>Bypass mainland address then proxy</item>
|
||||
<item>Bypassing LAN and mainland address then proxy</item>
|
||||
<item>Global direct</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mode_entries">
|
||||
<item>VPN</item>
|
||||
<item>Proxy only</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
13
README.md
13
README.md
|
@ -3,22 +3,21 @@
|
|||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||
|
||||
[](https://developer.android.com/about/versions/lollipop)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://kotlinlang.org)
|
||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||
[](https://github.com/2dust/v2rayNG/releases)
|
||||
[](https://t.me/v2rayn)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
||||
</a>
|
||||
### Telegram Channel
|
||||
[github_2dust](https://t.me/github_2dust)
|
||||
|
||||
### Usage
|
||||
|
||||
#### Geoip and Geosite
|
||||
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
||||
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
||||
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
|
||||
- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually
|
||||
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
|
||||
|
||||
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||
|
@ -26,8 +25,8 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
|
|||
### Development guide
|
||||
|
||||
Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
|
||||
The aar can be compiled from the Golang project under AndroidLibV2rayLite folder. For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile)
|
||||
and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
|
||||
The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
|
||||
For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
|
||||
|
||||
v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via
|
||||
`appops set [package name] ACTIVATE_VPN allow`
|
||||
|
|
9
V2rayNG/.gitignore
vendored
9
V2rayNG/.gitignore
vendored
|
@ -1,9 +0,0 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
*.apk
|
||||
signing.properties
|
2
V2rayNG/app/.gitignore
vendored
2
V2rayNG/app/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
/build
|
||||
/google-services.json
|
|
@ -1,121 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion Integer.parseInt("$compileSdkVer")
|
||||
buildToolsVersion buildToolsVer
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility = "8"
|
||||
sourceCompatibility = "8"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.v2ray.ang"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
||||
multiDexEnabled true
|
||||
versionCode 212
|
||||
versionName "1.0.2"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
zipAlignEnabled false
|
||||
shrinkResources false
|
||||
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
zipAlignEnabled false
|
||||
shrinkResources false
|
||||
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
|
||||
universalApk true //generate an additional APK that contains all the ABIs
|
||||
}
|
||||
}
|
||||
|
||||
// map for the version code
|
||||
project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
// assign different version code for each output
|
||||
variant.outputs.each { output ->
|
||||
output.outputFileName = "v2rayNG_" + variant.versionName + "_" + output.getFilter(com.android.build.OutputFile.ABI) + ".apk"
|
||||
|
||||
output.versionCodeOverride =
|
||||
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
|
||||
1000000 + android.defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
// Androidx
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation "androidx.legacy:legacy-support-v4:1.0.0"
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "com.google.android.material:material:1.5.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.preference:preference:1.0.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
|
||||
// Androidx ktx
|
||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
|
||||
|
||||
//kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
||||
|
||||
implementation 'com.tencent:mmkv-static:1.2.12'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'io.reactivex:rxjava:1.3.4'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
|
||||
implementation 'me.dm7.barcodescanner:core:1.9.8'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
|
||||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||
implementation 'com.blacksquircle.ui:editorkit:2.1.1'
|
||||
implementation 'com.blacksquircle.ui:language-base:2.1.1'
|
||||
implementation 'com.blacksquircle.ui:language-json:2.1.1'
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://maven.google.com' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
jcenter()
|
||||
}
|
||||
}
|
191
V2rayNG/app/build.gradle.kts
Normal file
191
V2rayNG/app/build.gradle.kts
Normal file
|
@ -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)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite
|
Binary file not shown.
53
V2rayNG/app/proguard-rules.pro
vendored
53
V2rayNG/app/proguard-rules.pro
vendored
|
@ -1,14 +1,10 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in G:\android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
|
@ -16,43 +12,10 @@
|
|||
# public *;
|
||||
#}
|
||||
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
|
||||
-dontwarn org.apache.commons.**
|
||||
-keep class org.apache.commons.** { *;}
|
||||
|
||||
# Disable debug info output
|
||||
-assumenosideeffects class android.util.Log {
|
||||
public static boolean isLoggable(java.lang.String,int);
|
||||
public static int v(...);
|
||||
public static int i(...);
|
||||
public static int w(...);
|
||||
public static int d(...);
|
||||
public static int e(...);
|
||||
}
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
||||
static void throwUninitializedPropertyAccessException(java.lang.String);
|
||||
}
|
||||
|
||||
-dontwarn rx.internal.util.unsafe.**
|
||||
-keep class rx.internal.util.unsafe.** { *;}
|
||||
|
||||
-dontwarn app.dinus.**
|
||||
-keep class app.dinus.** { *;}
|
||||
|
||||
-keepclassmembers class ** {
|
||||
@com.hwangjr.rxbus.annotation.Subscribe public *;
|
||||
@com.hwangjr.rxbus.annotation.Produce public *;
|
||||
}
|
||||
|
||||
-keep class libv2ray.** { *;}
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -1,13 +0,0 @@
|
|||
package com.v2ray.ang;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||
public ApplicationTest() {
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
4
V2rayNG/app/src/dev/res/values/strings.xml
Normal file
4
V2rayNG/app/src/dev/res/values/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="app_name" type="string">v2rayNG (DEV)</item>
|
||||
</resources>
|
|
@ -1,107 +1,161 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.v2ray.ang">
|
||||
tools:ignore="MissingLeanbackLauncher">
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:smallScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||
<uses-sdk
|
||||
android:minSdkVersion="21"
|
||||
tools:overrideLibrary="com.blacksquircle.ui.editorkit" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
android:minSdkVersion="34" />
|
||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:name=".AngApplication"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/AppThemeDayNight"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="m">
|
||||
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:launchMode="singleTask">
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppThemeDayNight.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.ServerActivity"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
<activity
|
||||
android:name=".ui.ServerCustomConfigActivity"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
<activity
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.PerAppProxyActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.ScannerActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.LogcatActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.RoutingSettingActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.RoutingEditActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.SubSettingActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.UserAssetActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.UserAssetUrlActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.SubEditActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.ScScannerActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.ScSwitchActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:process=":RunSoLibV2RayDaemon"
|
||||
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.UrlSchemeActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="v2rayng" />
|
||||
<data android:host="install-config" />
|
||||
<data android:host="install-sub" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ServerActivity"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
android:name=".ui.CheckUpdateActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ServerCustomConfigActivity"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.SettingsActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.PerAppProxyActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ScannerActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.LogcatActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.RoutingSettingsActivity"
|
||||
android:windowSoftInputMode="stateUnchanged" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.SubSettingActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.SubEditActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ScScannerActivity" />
|
||||
<activity
|
||||
android:exported="false"
|
||||
android:name=".ui.ScSwitchActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:process=":RunSoLibV2RayDaemon"
|
||||
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||
android:name=".ui.AboutActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".service.V2RayVpnService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
|
@ -111,59 +165,107 @@
|
|||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true" />
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
|
||||
<service android:name=".service.V2RayProxyOnlyService"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<service
|
||||
android:name=".service.V2RayProxyOnlyService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:label="@string/app_name"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="proxy" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.V2RayTestService"
|
||||
android:exported="false"
|
||||
android:process=":RunSoLibV2RayDaemon" />
|
||||
|
||||
<receiver
|
||||
android:exported="true"
|
||||
android:name=".receiver.WidgetProvider"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
android:name=".receiver.WidgetProvider"
|
||||
android:exported="true"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/app_widget_provider" />
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/app_widget_provider" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="com.v2ray.ang.action.widget.click" />
|
||||
<action android:name="com.v2ray.ang.action.activity" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".receiver.BootReceiver"
|
||||
android:exported="true"
|
||||
android:label="BootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:exported="true"
|
||||
android:name=".service.QSTileService"
|
||||
android:icon="@drawable/ic_v"
|
||||
android:label="@string/app_tile_name"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
android:name=".service.QSTileService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:label="@string/app_tile_name"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:process=":RunSoLibV2RayDaemon"
|
||||
tools:targetApi="24">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="tile" />
|
||||
</service>
|
||||
<!-- =====================Tasker===================== -->
|
||||
<activity
|
||||
android:exported="true"
|
||||
android:name=".ui.TaskerActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:exported="true"
|
||||
android:name=".receiver.TaskerReceiver"
|
||||
android:process=":RunSoLibV2RayDaemon">
|
||||
android:exported="true"
|
||||
android:process=":RunSoLibV2RayDaemon"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- =====================Tasker===================== -->
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.cache"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/cache_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
|
||||
* This service provides the following features:
|
||||
* 1. Provides a new API to get details of in-app items published for the app including
|
||||
* price, type, title and description.
|
||||
* 2. The purchase flow is synchronous and purchase information is available immediately
|
||||
* after it completes.
|
||||
* 3. Purchase information of in-app purchases is maintained within the Google Play system
|
||||
* till the purchase is consumed.
|
||||
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
|
||||
* in-app items are consumable and thereafter can be purchased again.
|
||||
* 5. An API to get current purchases of the user immediately. This will not contain any
|
||||
* consumed purchases.
|
||||
*
|
||||
* All calls will give a response code with the following possible values
|
||||
* RESULT_OK = 0 - success
|
||||
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
|
||||
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
|
||||
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
|
||||
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
|
||||
* RESULT_ERROR = 6 - Fatal error during the API action
|
||||
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
|
||||
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
|
||||
*/
|
||||
interface IInAppBillingService {
|
||||
/**
|
||||
* Checks support for the requested billing API version, package and in-app type.
|
||||
* Minimum API version supported by this interface is 3.
|
||||
* @param apiVersion the billing version which the app is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @param type type of the in-app item being purchased "inapp" for one-time purchases
|
||||
* and "subs" for subscription.
|
||||
* @return RESULT_OK(0) on success, corresponding result code on failures
|
||||
*/
|
||||
int isBillingSupported(int apiVersion, String packageName, String type);
|
||||
|
||||
/**
|
||||
* Provides details of a list of SKUs
|
||||
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
|
||||
* with a list JSON strings containing the productId, price, title and description.
|
||||
* This API can be called with a maximum of 20 SKUs.
|
||||
* @param apiVersion billing API version that the Third-party is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
||||
* in JSON format similar to:
|
||||
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
|
||||
* "title : "Example Title", "description" : "This is an example description" }'
|
||||
*/
|
||||
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
|
||||
|
||||
/**
|
||||
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
|
||||
* the type, a unique purchase token and an optional developer payload.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param sku the SKU of the in-app item as published in the developer console
|
||||
* @param type the type of the in-app item ("inapp" for one-time purchases
|
||||
* and "subs" for subscription).
|
||||
* @param developerPayload optional argument to be sent back with the purchase information
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
||||
*
|
||||
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
||||
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
||||
* If the purchase is successful, the result data will contain the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
||||
* '{"orderId":"12999763169054705758.1371079406387615",
|
||||
* "packageName":"com.example.app",
|
||||
* "productId":"exampleSku",
|
||||
* "purchaseTime":1345678900000,
|
||||
* "purchaseToken" : "122333444455555",
|
||||
* "developerPayload":"example developer payload" }'
|
||||
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
||||
* was signed with the private key of the developer
|
||||
* TODO: change this to app-specific keys.
|
||||
*/
|
||||
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
|
||||
String developerPayload);
|
||||
|
||||
/**
|
||||
* Returns the current SKUs owned by the user of the type and package name specified along with
|
||||
* purchase information and a signature of the data to be validated.
|
||||
* This will return all SKUs that have been purchased in V3 and managed items purchased using
|
||||
* V1 and V2 that have not been consumed.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param type the type of the in-app items being requested
|
||||
* ("inapp" for one-time purchases and "subs" for subscription).
|
||||
* @param continuationToken to be set as null for the first call, if the number of owned
|
||||
* skus are too many, a continuationToken is returned in the response bundle.
|
||||
* This method can be called again with the continuation token to get the next set of
|
||||
* owned skus.
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
|
||||
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
|
||||
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
|
||||
* of the purchase information
|
||||
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
|
||||
* next set of in-app purchases. Only set if the
|
||||
* user has more owned skus than the current list.
|
||||
*/
|
||||
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
|
||||
|
||||
/**
|
||||
* Consume the last purchase of the given SKU. This will result in this item being removed
|
||||
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param purchaseToken token in the purchase information JSON that identifies the purchase
|
||||
* to be consumed
|
||||
* @return 0 if consumption succeeded. Appropriate error values for failures.
|
||||
*/
|
||||
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
|
||||
}
|
142
V2rayNG/app/src/main/assets/custom_routing_black
Normal file
142
V2rayNG/app/src/main/assets/custom_routing_black
Normal file
|
@ -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"
|
||||
}
|
||||
]
|
27
V2rayNG/app/src/main/assets/custom_routing_global
Normal file
27
V2rayNG/app/src/main/assets/custom_routing_global
Normal file
|
@ -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"
|
||||
}
|
||||
]
|
96
V2rayNG/app/src/main/assets/custom_routing_white
Normal file
96
V2rayNG/app/src/main/assets/custom_routing_white
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
37
V2rayNG/app/src/main/assets/custom_routing_white_iran
Normal file
37
V2rayNG/app/src/main/assets/custom_routing_white_iran
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
1285
V2rayNG/app/src/main/assets/open_source_licenses.html
Normal file
1285
V2rayNG/app/src/main/assets/open_source_licenses.html
Normal file
File diff suppressed because it is too large
Load diff
|
@ -4,6 +4,7 @@ au.com.shiftyjelly.pocketcasts
|
|||
bbc.mobile.news.ww
|
||||
be.mygod.vpnhotspot
|
||||
ch.protonmail.android
|
||||
cm.aptoide.pt
|
||||
co.wanqu.android
|
||||
com.alphainventor.filemanager
|
||||
com.amazon.kindle
|
||||
|
@ -34,7 +35,9 @@ com.chrome.canary
|
|||
com.chrome.dev
|
||||
com.cl.newt66y
|
||||
com.cradle.iitc_mobile
|
||||
org.exarhteam.iitc_mobile
|
||||
com.cygames.shadowverse
|
||||
com.dcard.freedom
|
||||
com.devhd.feedly
|
||||
com.devolver.reigns2
|
||||
com.discord
|
||||
|
@ -108,6 +111,7 @@ com.ifttt.ifttt
|
|||
com.imgur.mobile
|
||||
com.innologica.inoreader
|
||||
com.instagram.android
|
||||
com.instagram.lite
|
||||
com.instapaper.android
|
||||
com.jarvanh.vpntether
|
||||
com.kapp.youtube.final
|
||||
|
@ -115,6 +119,7 @@ com.klinker.android.twitter_l
|
|||
com.lastpass.lpandroid
|
||||
com.linecorp.linelite
|
||||
com.lingodeer
|
||||
com.ltnnews.news
|
||||
com.mediapods.tumbpods
|
||||
com.mgoogle.android.gms
|
||||
com.microsoft.emmx
|
||||
|
@ -159,6 +164,7 @@ com.slack
|
|||
com.snaptube.premium
|
||||
com.sololearn
|
||||
com.sonelli.juicessh
|
||||
com.sparkslab.dcardreader
|
||||
com.spotify.music
|
||||
com.tencent.huatuo
|
||||
com.termux
|
||||
|
@ -173,10 +179,13 @@ com.twitter.android
|
|||
com.u91porn
|
||||
com.u9porn
|
||||
com.ubisoft.dance.justdance2015companion
|
||||
com.udn.news
|
||||
com.utopia.pxview
|
||||
com.valvesoftware.android.steam.communimunity
|
||||
com.valvesoftware.android.steam.community
|
||||
com.vanced.manager
|
||||
com.vanced.android.youtube
|
||||
com.vanced.android.apps.youtube.music
|
||||
com.mgoogle.android.gms
|
||||
com.vimeo.android.videoapp
|
||||
com.vivaldi.browser
|
||||
com.vivaldi.browser.snapshot
|
||||
|
@ -186,10 +195,12 @@ com.wire
|
|||
com.wuxiangai.refactor
|
||||
com.xda.labs
|
||||
com.xvideos.app
|
||||
com.yahoo.mobile.client.android.superapp
|
||||
com.yandex.browser
|
||||
com.yandex.browser.beta
|
||||
com.yandex.browser.alpha
|
||||
com.z28j.feel
|
||||
com.zhiliaoapp.musically
|
||||
con.medium.reader
|
||||
de.apkgrabber
|
||||
de.robv.android.xposed.installer
|
||||
|
@ -210,6 +221,7 @@ jp.bokete.app.android
|
|||
jp.naver.line.android
|
||||
jp.pxv.android
|
||||
luo.speedometergpspro
|
||||
m.cna.com.tw.App
|
||||
mark.via.gp
|
||||
me.tshine.easymark
|
||||
net.teeha.android.url_shortener
|
||||
|
@ -226,6 +238,7 @@ org.mozilla.firefox_beta
|
|||
org.mozilla.focus
|
||||
org.schabi.newpipe
|
||||
org.telegram.messenger
|
||||
org.telegram.messenger.web
|
||||
org.telegram.multi
|
||||
org.telegram.plus
|
||||
org.thunderdog.challegram
|
||||
|
@ -239,3 +252,162 @@ tw.com.gamer.android.activecenter
|
|||
videodownloader.downloadvideo.downloader
|
||||
uk.co.bbc.learningenglish
|
||||
com.ted.android
|
||||
de.danoeh.antennapod
|
||||
com.kiwibrowser.browser
|
||||
nekox.messenger
|
||||
com.nextcloud.client
|
||||
com.aurora.store
|
||||
com.aurora.adroid
|
||||
chat.simplex.app
|
||||
im.vector.app
|
||||
network.loki.messenger
|
||||
eu.siacs.conversations
|
||||
xyz.nextalone.nagram
|
||||
net.programmierecke.radiodroid2
|
||||
im.fdx.v2ex
|
||||
ml.docilealligator.infinityforreddit
|
||||
com.bytemyth.ama
|
||||
app.vanadium.browser
|
||||
com.cakewallet.cake_wallet
|
||||
org.purplei2p.i2pd
|
||||
dk.tacit.android.foldersync.lite
|
||||
com.nononsenseapps.feeder
|
||||
com.m2049r.xmrwallet
|
||||
com.paypal.android.p2pmobile
|
||||
com.google.android.apps.googlevoice
|
||||
com.readdle.spark
|
||||
org.torproject.torbrowser
|
||||
com.deepl.mobiletranslator
|
||||
com.microsoft.bing
|
||||
com.keylesspalace.tusky
|
||||
com.ottplay.ottplay
|
||||
ru.iptvremote.android.iptv.pro
|
||||
jp.naver.line.android
|
||||
com.xmflsct.app.tooot
|
||||
com.forem.android
|
||||
app.revanced.android.youtube
|
||||
com.mgoogle.android.gms
|
||||
com.pionex.client
|
||||
vip.mytokenpocket
|
||||
im.token.app
|
||||
com.linekong.mars24
|
||||
com.feixiaohao
|
||||
com.aicoin.appandroid
|
||||
com.binance.dev
|
||||
com.kraken.trade
|
||||
com.okinc.okex.gp
|
||||
com.authy.authy
|
||||
air.com.rosettastone.mobile.CoursePlayer
|
||||
com.blizzard.bma
|
||||
com.amazon.kindle
|
||||
com.google.android.apps.fitness
|
||||
net.tsapps.appsales
|
||||
com.wemesh.android
|
||||
com.google.android.apps.googleassistant
|
||||
allen.town.focus.reader
|
||||
me.hyliu.fluent_reader_lite
|
||||
com.aljazeera.mobile
|
||||
com.ft.news
|
||||
de.marmaro.krt.ffupdater
|
||||
myradio.radio.fmradio.liveradio.radiostation
|
||||
com.google.earth
|
||||
eu.kanade.tachiyomi.j2k
|
||||
com.audials
|
||||
com.microsoft.skydrive
|
||||
com.mb.android.tg
|
||||
com.melodis.midomiMusicIdentifier.freemium
|
||||
com.foxnews.android
|
||||
ch.threema.app
|
||||
com.briarproject.briar.android
|
||||
foundation.e.apps
|
||||
com.valvesoftware.android.steam.friendsui
|
||||
com.imback.yeetalk
|
||||
so.onekey.app.wallet
|
||||
com.xc3fff0e.xmanager
|
||||
meditofoundation.medito
|
||||
com.picol.client
|
||||
com.streetwriters.notesnook
|
||||
shanghai.panewsApp.com
|
||||
org.coursera.android
|
||||
com.positron_it.zlib
|
||||
com.blizzard.messenger
|
||||
com.javdb.javrocket
|
||||
com.picacomic.fregata
|
||||
com.fxl.chacha
|
||||
me.proton.android.drive
|
||||
com.lastpass.lpandroid
|
||||
com.tradingview.tradingviewapp
|
||||
com.deviantart.android.damobile
|
||||
com.fusionmedia.investing
|
||||
com.ewa.ewaapp
|
||||
com.duolingo
|
||||
com.hellotalk
|
||||
io.github.huskydg.magisk
|
||||
com.jsy.xpgbox
|
||||
com.hostloc.app.hostloc
|
||||
com.dena.pokota
|
||||
com.vitorpamplona.amethyst
|
||||
com.zhiliaoapp.musically
|
||||
us.spotco.fennec_dos
|
||||
com.fongmi.android.tv
|
||||
com.pocketprep.android.itcybersecurity
|
||||
com.cloudtv
|
||||
com.glassdoor.app
|
||||
com.indeed.android.jobsearch
|
||||
com.linkedin.android
|
||||
com.github.tvbox.osc.bh
|
||||
com.example.douban
|
||||
com.sipnetic.app
|
||||
com.microsoft.rdc.androidx
|
||||
org.zwanoo.android.speedtest
|
||||
com.sonelli.juicessh
|
||||
com.scmp.newspulse
|
||||
org.lsposed.manager
|
||||
mnn.Android
|
||||
com.thomsonretuers.reuters
|
||||
com.guardian
|
||||
com.ttxapps.onesyncv2
|
||||
org.fcitx.fcitx5.android.updater
|
||||
com.tailscale.ipn
|
||||
tw.nekomimi.nekogram
|
||||
com.nexon.kartdrift
|
||||
io.syncapps.lemmy_sync
|
||||
com.seazon.feedme
|
||||
com.readwise
|
||||
de.spiritcroc.riotx
|
||||
com.openai.chatgpt
|
||||
io.changenow.changenow
|
||||
com.poe.android
|
||||
com.twingate
|
||||
com.blinkslabs.blinkist.android
|
||||
com.ichi2.anki
|
||||
md.obsidian
|
||||
com.musixmatch.android.lyrify
|
||||
com.cyber.turbo
|
||||
com.offsec.nethunter
|
||||
me.ghui.v2er
|
||||
com.samruston.twitter
|
||||
org.adaway
|
||||
org.swiftapps.swiftbackup
|
||||
com.zerotier.one
|
||||
com.quietmobile
|
||||
com.instagram.barcelona
|
||||
im.molly.app
|
||||
com.rvx.android.youtube
|
||||
com.deepl.mobiletranslator
|
||||
com.qingsong.yingmi
|
||||
com.lemurbrowser.exts
|
||||
com.silverdev.dnartdroid
|
||||
me.ash.reader
|
||||
de.tutao.tutanota
|
||||
dev.imranr.obtainium
|
||||
com.getsomeheadspace.android
|
||||
org.cromite.cromite
|
||||
com.nutomic.syncthingandroid
|
||||
com.bumble.app
|
||||
com.cnn.mobile.android.phone
|
||||
com.google.android.apps.authenticator2
|
||||
com.microsoft.copilot
|
||||
com.netflix.NGP.Storyteller
|
||||
com.Slack
|
||||
com.server.auditor.ssh.client
|
|
@ -54,6 +54,7 @@
|
|||
"users": [
|
||||
{
|
||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||
"alterId": 0,
|
||||
"security": "auto",
|
||||
"level": 8
|
||||
}
|
||||
|
@ -80,7 +81,9 @@
|
|||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {},
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
|
@ -94,7 +97,7 @@
|
|||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": []
|
||||
},
|
||||
"dns": {
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 12 KiB |
47
V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt
Normal file
47
V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
248
V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt
Normal file
248
V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt
Normal file
|
@ -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"
|
||||
)
|
||||
|
||||
}
|
11
V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt
Normal file
11
V2rayNG/app/src/main/java/com/v2ray/ang/dto/AppInfo.kt
Normal file
|
@ -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
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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,
|
||||
)
|
||||
|
22
V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt
Normal file
22
V2rayNG/app/src/main/java/com/v2ray/ang/dto/EConfigType.kt
Normal file
|
@ -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 }
|
||||
}
|
||||
}
|
23
V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
Normal file
23
V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
Normal file
|
@ -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<Asset>,
|
||||
@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
|
||||
)
|
||||
}
|
46
V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt
Normal file
46
V2rayNG/app/src/main/java/com/v2ray/ang/dto/Hysteria2Bean.kt
Normal file
|
@ -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?,
|
||||
)
|
||||
}
|
12
V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
Normal file
12
V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
Normal file
|
@ -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
|
||||
)
|
20
V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt
Normal file
20
V2rayNG/app/src/main/java/com/v2ray/ang/dto/Language.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
18
V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt
Normal file
18
V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
120
V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt
Normal file
120
V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt
Normal file
|
@ -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<String> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
20
V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt
Normal file
20
V2rayNG/app/src/main/java/com/v2ray/ang/dto/RoutingType.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt
Normal file
13
V2rayNG/app/src/main/java/com/v2ray/ang/dto/RulesetItem.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package com.v2ray.ang.dto
|
||||
|
||||
data class RulesetItem(
|
||||
var remarks: String? = "",
|
||||
var ip: List<String>? = null,
|
||||
var domain: List<String>? = null,
|
||||
var outboundTag: String = "",
|
||||
var port: String? = null,
|
||||
var network: String? = null,
|
||||
var protocol: List<String>? = null,
|
||||
var enabled: Boolean = true,
|
||||
var locked: Boolean? = false,
|
||||
)
|
86
V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt
Normal file
86
V2rayNG/app/src/main/java/com/v2ray/ang/dto/ServerConfig.kt
Normal file
|
@ -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<String> {
|
||||
if (configType != EConfigType.CUSTOM) {
|
||||
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
||||
}
|
||||
fullConfig?.let { config ->
|
||||
return config.outbounds.map { it.tag }.toMutableList()
|
||||
}
|
||||
return mutableListOf()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.v2ray.ang.dto
|
||||
|
||||
data class ServersCache(
|
||||
val guid: String,
|
||||
val profile: ProfileItem
|
||||
)
|
|
@ -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,
|
||||
)
|
||||
|
556
V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt
Normal file
556
V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt
Normal file
|
@ -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<InboundBean>,
|
||||
var outbounds: ArrayList<OutboundBean>,
|
||||
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<String>,
|
||||
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<VnextBean>? = null,
|
||||
var fragment: FragmentBean? = null,
|
||||
var noises: List<NoiseBean>? = null,
|
||||
var servers: List<ServersBean>? = 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<WireGuardBean>? = null,
|
||||
var reserved: List<Int>? = null,
|
||||
var mtu: Int? = null,
|
||||
var obfsPassword: String? = null,
|
||||
) {
|
||||
|
||||
data class VnextBean(
|
||||
var address: String = "",
|
||||
var port: Int = AppConfig.DEFAULT_PORT,
|
||||
var users: List<UsersBean>
|
||||
) {
|
||||
|
||||
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<SocksUsersBean>? = 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<String> = ArrayList(),
|
||||
var headers: HeadersBean = HeadersBean(),
|
||||
val version: String? = null,
|
||||
val method: String? = null
|
||||
) {
|
||||
data class HeadersBean(
|
||||
var Host: List<String>? = ArrayList(),
|
||||
@SerializedName("User-Agent")
|
||||
val userAgent: List<String>? = null,
|
||||
@SerializedName("Accept-Encoding")
|
||||
val acceptEncoding: List<String>? = null,
|
||||
val Connection: List<String>? = 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<String> = 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<String>? = null,
|
||||
val minVersion: String? = null,
|
||||
val maxVersion: String? = null,
|
||||
val preferServerCipherSuites: Boolean? = null,
|
||||
val cipherSuites: String? = null,
|
||||
val fingerprint: String? = null,
|
||||
val certificates: List<Any>? = 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<String?>? {
|
||||
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<Any>? = null,
|
||||
var hosts: Map<String, Any>? = 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<String>? = null,
|
||||
var expectIPs: List<String>? = null,
|
||||
val clientIp: String? = null,
|
||||
val skipFallback: Boolean? = null,
|
||||
)
|
||||
}
|
||||
|
||||
data class RoutingBean(
|
||||
var domainStrategy: String,
|
||||
var domainMatcher: String? = null,
|
||||
var rules: ArrayList<RulesBean>,
|
||||
val balancers: List<Any>? = null
|
||||
) {
|
||||
|
||||
data class RulesBean(
|
||||
var type: String = "field",
|
||||
var ip: ArrayList<String>? = null,
|
||||
var domain: ArrayList<String>? = null,
|
||||
var outboundTag: String = "",
|
||||
var balancerTag: String? = null,
|
||||
var port: String? = null,
|
||||
val sourcePort: String? = null,
|
||||
val network: String? = null,
|
||||
val source: List<String>? = null,
|
||||
val user: List<String>? = null,
|
||||
var inboundTag: List<String>? = null,
|
||||
val protocol: List<String>? = null,
|
||||
val attrs: String? = null,
|
||||
val domainMatcher: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
data class PolicyBean(
|
||||
var levels: Map<String, LevelBean>,
|
||||
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<OutboundBean> {
|
||||
return outbounds.filter { outbound ->
|
||||
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
19
V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt
Normal file
19
V2rayNG/app/src/main/java/com/v2ray/ang/dto/VmessQRCode.kt
Normal file
|
@ -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 = ""
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
212
V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt
Normal file
212
V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt
Normal file
|
@ -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<String, Any?>) {
|
||||
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<String, Any?>) {
|
||||
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 <reified T : Serializable> 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 <reified T : Serializable> 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()
|
||||
}
|
27
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt
Normal file
27
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/CustomFmt.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
170
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt
Normal file
170
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt
Normal file
|
@ -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, String>?): 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<String, String> {
|
||||
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<String, String>, 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<String, String> {
|
||||
val dicQuery = HashMap<String, String>()
|
||||
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()
|
||||
}
|
||||
}
|
32
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt
Normal file
32
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
151
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt
Normal file
151
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt
Normal file
|
@ -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<String, String>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
154
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
Normal file
154
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
Normal file
|
@ -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<String, String>()
|
||||
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
|
||||
}
|
||||
}
|
79
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt
Normal file
79
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
83
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt
Normal file
83
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
80
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt
Normal file
80
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
192
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt
Normal file
192
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
}
|
149
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt
Normal file
149
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt
Normal file
|
@ -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<String, String> = mutableMapOf()
|
||||
val peerParams: MutableMap<String, String> = 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<String, String>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<String>): 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<Int, Int> {
|
||||
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<Any> =
|
||||
JsonUtil.fromJson(server, Array<Any>::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<String, SubscriptionItem>): 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
588
V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt
Normal file
588
V2rayNG/app/src/main/java/com/v2ray/ang/handler/MmkvManager.kt
Normal file
|
@ -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<String>) {
|
||||
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the server list.
|
||||
*
|
||||
* @return The list of server GUIDs.
|
||||
*/
|
||||
fun decodeServerList(): MutableList<String> {
|
||||
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
||||
return if (json.isNullOrBlank()) {
|
||||
mutableListOf()
|
||||
} else {
|
||||
JsonUtil.fromJson(json, Array<String>::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<String>?) {
|
||||
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<Pair<String, SubscriptionItem>> {
|
||||
initSubsList()
|
||||
|
||||
val subscriptions = mutableListOf<Pair<String, SubscriptionItem>>()
|
||||
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<String>) {
|
||||
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the subscription list.
|
||||
*
|
||||
* @return The list of subscription IDs.
|
||||
*/
|
||||
fun decodeSubsList(): MutableList<String> {
|
||||
val json = mainStorage.decodeString(KEY_SUB_IDS)
|
||||
return if (json.isNullOrBlank()) {
|
||||
mutableListOf()
|
||||
} else {
|
||||
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Asset
|
||||
|
||||
/**
|
||||
* Decodes the asset URLs.
|
||||
*
|
||||
* @return The list of asset URLs.
|
||||
*/
|
||||
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
||||
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||
assetStorage.allKeys()?.forEach { key ->
|
||||
val json = assetStorage.decodeString(key)
|
||||
if (!json.isNullOrBlank()) {
|
||||
assetUrlItems.add(Pair(key, 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<RulesetItem>? {
|
||||
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
||||
if (ruleset.isNullOrEmpty()) return null
|
||||
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the routing rulesets.
|
||||
*
|
||||
* @param rulesetList The list of routing rulesets.
|
||||
*/
|
||||
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
|
||||
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<String>): 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<String>? {
|
||||
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
|
||||
|
||||
}
|
|
@ -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<RulesetItem>? {
|
||||
val fileName = RoutingType.fromIndex(index).fileName
|
||||
val assets = Utils.readTextFromAssets(context, fileName)
|
||||
if (TextUtils.isEmpty(assets)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return JsonUtil.fromJson(assets, Array<RulesetItem>::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<RulesetItem>::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<RulesetItem>) {
|
||||
val rulesetNew: MutableList<RulesetItem> = 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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<Socket?>()
|
||||
|
||||
/**
|
||||
* 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<Long, String> {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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<GitHubRelease>::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")
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,48 +13,41 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
package com.v2ray.ang.helper
|
||||
|
||||
/**
|
||||
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
||||
* Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
|
||||
*
|
||||
* @author Paul Burke (ipaulpro)
|
||||
*/
|
||||
public interface ItemTouchHelperAdapter {
|
||||
|
||||
interface ItemTouchHelperAdapter {
|
||||
/**
|
||||
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
||||
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
|
||||
* <br/>
|
||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
|
||||
* an item is shifted, and **not** at the end of a "drop" event.<br></br>
|
||||
* <br></br>
|
||||
* Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
|
||||
* adjusting the underlying data to reflect this move.
|
||||
*
|
||||
* @param fromPosition The start position of the moved item.
|
||||
* @param toPosition Then resolved position of the moved item.
|
||||
* @return True if the item was moved to the new adapter position.
|
||||
*
|
||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||
* @see RecyclerView.getAdapterPositionFor
|
||||
* @see RecyclerView.ViewHolder.getAdapterPosition
|
||||
*/
|
||||
boolean onItemMove(int fromPosition, int toPosition);
|
||||
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
|
||||
|
||||
|
||||
void onItemMoveCompleted();
|
||||
fun onItemMoveCompleted()
|
||||
|
||||
/**
|
||||
* Called when an item has been dismissed by a swipe.<br/>
|
||||
* <br/>
|
||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
|
||||
* Called when an item has been dismissed by a swipe.<br></br>
|
||||
* <br></br>
|
||||
* 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)
|
||||
}
|
|
@ -13,29 +13,26 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.v2ray.ang.helper
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
|
||||
/**
|
||||
* Interface to notify an item ViewHolder of relevant callbacks from {@link
|
||||
* ItemTouchHelper.Callback}.
|
||||
* Interface to notify an item ViewHolder of relevant callbacks from [ ].
|
||||
*
|
||||
* @author Paul Burke (ipaulpro)
|
||||
*/
|
||||
public interface ItemTouchHelperViewHolder {
|
||||
|
||||
interface ItemTouchHelperViewHolder {
|
||||
/**
|
||||
* Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
|
||||
* Called when the [ItemTouchHelper] first registers an item as being moved or swiped.
|
||||
* Implementations should update the item view to indicate it's active state.
|
||||
*/
|
||||
void onItemSelected();
|
||||
fun onItemSelected()
|
||||
|
||||
|
||||
/**
|
||||
* Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
|
||||
* Called when the [ItemTouchHelper] has completed the move or swipe, and the active item
|
||||
* state should be cleared.
|
||||
*/
|
||||
void onItemClear();
|
||||
fun onItemClear()
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Paul Burke
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Listener for manual initiation of a drag.
|
||||
*/
|
||||
public interface OnStartDragListener {
|
||||
|
||||
/**
|
||||
* Called when a view is requesting a start of a drag.
|
||||
*
|
||||
* @param viewHolder The holder of the view to drag.
|
||||
*/
|
||||
void onStartDrag(RecyclerView.ViewHolder viewHolder);
|
||||
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Paul Burke
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.v2ray.ang.helper;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
||||
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
|
||||
* </br/>
|
||||
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
|
||||
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement
|
||||
* {@link ItemTouchHelperViewHolder}.
|
||||
*
|
||||
* @author Paul Burke (ipaulpro)
|
||||
*/
|
||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
public static final float ALPHA_FULL = 1.0f;
|
||||
|
||||
private final ItemTouchHelperAdapter mAdapter;
|
||||
|
||||
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
||||
mAdapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
||||
// Set movement flags based on the layout manager
|
||||
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||
final int swipeFlags = 0;
|
||||
return makeMovementFlags(dragFlags, swipeFlags);
|
||||
} else {
|
||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
||||
return makeMovementFlags(dragFlags, swipeFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
||||
if (source.getItemViewType() != target.getItemViewType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Notify the adapter of the move
|
||||
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
|
||||
// Notify the adapter of the dismissal
|
||||
mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX,
|
||||
float dY, int actionState, boolean isCurrentlyActive) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||
// Fade out the view as it is swiped out of the parent's bounds
|
||||
final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
|
||||
viewHolder.itemView.setAlpha(alpha);
|
||||
viewHolder.itemView.setTranslationX(dX);
|
||||
} else {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||
// We only want the active item to change
|
||||
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||
// Let the view holder know that this item is being moved or dragged
|
||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||
itemViewHolder.onItemSelected();
|
||||
}
|
||||
}
|
||||
|
||||
super.onSelectedChanged(viewHolder, actionState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
|
||||
mAdapter.onItemMoveCompleted();
|
||||
|
||||
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
||||
|
||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||
// Tell the view holder it's time to restore the idle state
|
||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||
itemViewHolder.onItemClear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.<br></br>
|
||||
*
|
||||
* 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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
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!!
|
||||
}
|
43
V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt
Normal file
43
V2rayNG/app/src/main/java/com/v2ray/ang/plugin/Plugin.kt
Normal file
|
@ -0,0 +1,43 @@
|
|||
/******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
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()
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
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"
|
||||
}
|
54
V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt
Normal file
54
V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginList.kt
Normal file
|
@ -0,0 +1,54 @@
|
|||
/******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
package com.v2ray.ang.plugin
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import com.v2ray.ang.AngApplication
|
||||
|
||||
class PluginList : ArrayList<Plugin>() {
|
||||
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<String, Plugin>().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))
|
||||
}
|
||||
}
|
||||
}
|
233
V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt
Normal file
233
V2rayNG/app/src/main/java/com/v2ray/ang/plugin/PluginManager.kt
Normal file
|
@ -0,0 +1,233 @@
|
|||
/******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2021 by nekohasekai <contact-AngApplication@sekai.icu> *
|
||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* 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 <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
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
|
||||
)!!
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -4,38 +4,38 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.TextUtils
|
||||
import com.google.zxing.WriterException
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
class TaskerReceiver : BroadcastReceiver() {
|
||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||
|
||||
/**
|
||||
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
||||
* It retrieves the bundle from the intent and checks the switch and guid values.
|
||||
* Depending on the switch value, it starts or stops the V2Ray service.
|
||||
*
|
||||
* @param context The Context in which the receiver is running.
|
||||
* @param intent The Intent being received.
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
|
||||
try {
|
||||
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
|
||||
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
|
||||
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
|
||||
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty()
|
||||
|
||||
if (switch == null || guid == null || TextUtils.isEmpty(guid)) {
|
||||
if (switch == null || TextUtils.isEmpty(guid)) {
|
||||
return
|
||||
} else if (switch) {
|
||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||
Utils.startVServiceFromToggle(context)
|
||||
V2RayServiceManager.startVServiceFromToggle(context)
|
||||
} else {
|
||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
||||
V2RayServiceManager.startV2Ray(context)
|
||||
V2RayServiceManager.startVService(context, guid)
|
||||
}
|
||||
} else {
|
||||
Utils.stopVService(context)
|
||||
V2RayServiceManager.stopVService(context)
|
||||
}
|
||||
} catch (e: WriterException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,21 +8,32 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.service.V2RayServiceManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
class WidgetProvider : AppWidgetProvider() {
|
||||
/**
|
||||
* 每次窗口小部件被更新都调用一次该方法
|
||||
* This method is called every time the widget is updated.
|
||||
* It updates the widget background based on the V2Ray service running state.
|
||||
*
|
||||
* @param context The Context in which the receiver is running.
|
||||
* @param appWidgetManager The AppWidgetManager instance.
|
||||
* @param appWidgetIds The appWidgetIds for which an update is needed.
|
||||
*/
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning)
|
||||
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.isRunning())
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the widget background based on whether the V2Ray service is running.
|
||||
*
|
||||
* @param context The Context in which the receiver is running.
|
||||
* @param appWidgetManager The AppWidgetManager instance.
|
||||
* @param appWidgetIds The appWidgetIds for which an update is needed.
|
||||
* @param isRunning Boolean indicating if the V2Ray service is running.
|
||||
*/
|
||||
private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
|
||||
val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
|
||||
val intent = Intent(context, WidgetProvider::class.java)
|
||||
|
@ -35,20 +46,15 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
})
|
||||
}
|
||||
)
|
||||
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
|
||||
if (isRunning) {
|
||||
remoteViews.setInt(
|
||||
R.id.layout_switch,
|
||||
"setBackgroundResource",
|
||||
R.drawable.ic_rounded_corner_theme
|
||||
)
|
||||
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
|
||||
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_active)
|
||||
} else {
|
||||
remoteViews.setInt(
|
||||
R.id.layout_switch,
|
||||
"setBackgroundResource",
|
||||
R.drawable.ic_rounded_corner_grey
|
||||
)
|
||||
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_play_24dp)
|
||||
remoteViews.setInt(R.id.layout_background, "setBackgroundResource", R.drawable.ic_rounded_corner_inactive)
|
||||
}
|
||||
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
|
@ -57,26 +63,35 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
}
|
||||
|
||||
/**
|
||||
* 接收窗口小部件发送的广播
|
||||
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
||||
* It handles widget click actions and updates the widget background based on the V2Ray service state.
|
||||
*
|
||||
* @param context The Context in which the receiver is running.
|
||||
* @param intent The Intent being received.
|
||||
*/
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
super.onReceive(context, intent)
|
||||
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
|
||||
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
||||
Utils.stopVService(context)
|
||||
if (V2RayServiceManager.isRunning()) {
|
||||
V2RayServiceManager.stopVService(context)
|
||||
} else {
|
||||
Utils.startVServiceFromToggle(context)
|
||||
V2RayServiceManager.startVServiceFromToggle(context)
|
||||
}
|
||||
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
|
||||
AppWidgetManager.getInstance(context)?.let { manager ->
|
||||
when (intent.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
true)
|
||||
updateWidgetBackground(
|
||||
context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
false)
|
||||
updateWidgetBackground(
|
||||
context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue