Compare commits
180 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d016ab06d4 | |||
| 4d9aced5a4 | |||
| 62b928e6a0 | |||
| 0ce60eae73 | |||
| 5930a6a9eb | |||
| a360310be2 | |||
| 820e6cdf36 | |||
| 658b890325 | |||
| fb017c6659 | |||
| 00e6314afe | |||
| 463f45804f | |||
| 572955dd1e | |||
| 375a209beb | |||
| 872f9ce199 | |||
| b4f02c9bd6 | |||
| e567719f5b | |||
| 8407fc5825 | |||
| a3e49dcc3d | |||
| 7b47bbe99a | |||
| 0fb2165015 | |||
| 03eeeb9b62 | |||
| 038daf5fda | |||
| bfd1387d9b | |||
| 5afec5cf25 | |||
| ec29bdf5bf | |||
| 57efab093f | |||
| 9c92a64811 | |||
| 7ddc82d5cd | |||
| c286ba18a8 | |||
| 867b5fc880 | |||
| e8a7fa5320 | |||
| f2f9e55286 | |||
| 4a1c62a67c | |||
| c9a6a459d4 | |||
| 21fdcf4ccf | |||
| 7c7a623ae5 | |||
| b3074e9697 | |||
| 513ebcfa23 | |||
| 50d9057f1a | |||
| 2a563e7884 | |||
| c69cd18842 | |||
| 7f2ced85a8 | |||
| 6c5eef99b5 | |||
| d7c3bae8cc | |||
| 57c98f7c50 | |||
| 49be23c56a | |||
| 9b658e9a22 | |||
| aaa84d081f | |||
| 5628cbee3a | |||
| 3dd663d927 | |||
| 9e49a2dbd9 | |||
| 6c199c1687 | |||
| 39af5fdb86 | |||
| 7b2794f6be | |||
| 411d9e5c9a | |||
| a57aee9424 | |||
| 4602afc67e | |||
| ceb29840f2 | |||
| 1c1f130ca7 | |||
| afa0eb9375 | |||
| 49b682f0f3 | |||
| d5def3bf2f | |||
| 51b97b64f2 | |||
| 3af9ce1a1a | |||
| a5d3dda941 | |||
| 71a3e300d8 | |||
| 030b9a3900 | |||
| 24d105a53c | |||
| 45805d6df7 | |||
| 4437e6699b | |||
| fa409f91e4 | |||
| 89be5f077b | |||
| 896889778f | |||
| 10f705a8b2 | |||
| 2956fa2030 | |||
| c2a704a6ea | |||
| 78cac0cd90 | |||
| 3ffb2e8e05 | |||
| e2d667e0bb | |||
| 3ae0777d7f | |||
| 5e6348676c | |||
| df3f1ca3ef | |||
| 94f2bec329 | |||
| c54d8fa43a | |||
| 5bbbdcf6f2 | |||
| 4abf20fa32 | |||
| 723727feb9 | |||
| 2efd4b741c | |||
| 83aab0f880 | |||
| 66ea17877e | |||
| 26bc985368 | |||
| 5bbf40c784 | |||
| 6d5c23245c | |||
| b148290211 | |||
| c473f9bb13 | |||
| c7c3d27f36 | |||
| bebc6fea13 | |||
| 9271857b1e | |||
| 7ea78c1840 | |||
| ac668788b3 | |||
| adfcf0a5d9 | |||
| 15b5595797 | |||
| 5a18296cb2 | |||
| 1256edbaf5 | |||
| 870950e807 | |||
| e798cb3f42 | |||
| b970f0bcff | |||
| 93b9a56428 | |||
| eb8bd13266 | |||
| d850c88c63 | |||
| 6bee795c0d | |||
| d7d7e029e0 | |||
| 8efdab43d7 | |||
| 5e0235cf70 | |||
| 9f668b3da7 | |||
| b2437279dc | |||
| 180b5efd93 | |||
| dc31380cc2 | |||
| 1361e0dacf | |||
| a45eb66bd1 | |||
| 508102cebe | |||
| 1d86dbb9f3 | |||
| 20ca554be2 | |||
| 25ba455656 | |||
| 17e7c62d53 | |||
| 6f0f2fdeda | |||
| 3c9c9b5a4c | |||
| e13024d6bb | |||
| 9d58edd31f | |||
| af7dfc3a43 | |||
| ca554e6ac4 | |||
| ec391d8689 | |||
| 6afd4d0549 | |||
| 957cf85362 | |||
| 881152e10a | |||
| 6e47ebb27a | |||
| a731e6c360 | |||
| d1466ba4b2 | |||
| 617f28f399 | |||
| c0ec0d9404 | |||
| 3419dc8837 | |||
| 786aaf823a | |||
| 1144183da4 | |||
| 5bf2fda990 | |||
| 28639cc388 | |||
| ca254b2aa1 | |||
| 9721879713 | |||
| 623c1807c5 | |||
| 739fa88ba7 | |||
| 85d6f00f8c | |||
| 8986710453 | |||
| f54faacbf6 | |||
| 993ee0b8d2 | |||
| 903352ec9c | |||
| ad56106c08 | |||
| 91b8284afd | |||
| ef9e0cc0d2 | |||
| 6577c46a31 | |||
| c6dab001b2 | |||
| aea8369b8a | |||
| 92d2cb35c4 | |||
| 6ce3d540e8 | |||
| c105d84b35 | |||
| 8b149fb52f | |||
| 3ea04c076c | |||
| 98475460bf | |||
| 68ee61a753 | |||
| 90ba9ef2b7 | |||
| 0ec114322e | |||
| 76f52e7aa7 | |||
| caa25ce424 | |||
| e33d7e9bcf | |||
| e120fce0b7 | |||
| 6a90965c7b | |||
| 0542d913f2 | |||
| 746254bb7e | |||
| 2227ea3ed6 | |||
| 4d0e9e2b44 | |||
| 5cba8a228c | |||
| 2d8ed22dba |
@@ -1,6 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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@v6
|
|
||||||
with:
|
|
||||||
submodules: 'recursive'
|
|
||||||
fetch-depth: '0'
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: android-actions/setup-android@v4.0.1
|
|
||||||
with:
|
|
||||||
log-accepted-android-sdk-licenses: false
|
|
||||||
cmdline-tools-version: '13114758'
|
|
||||||
packages: 'platforms;android-36.1 build-tools;36.1.0 platform-tools'
|
|
||||||
|
|
||||||
- name: Install NDK
|
|
||||||
run: |
|
|
||||||
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
|
|
||||||
--channel=0 \
|
|
||||||
--install "ndk;28.2.13676358"
|
|
||||||
echo "NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> $GITHUB_ENV
|
|
||||||
sed -i '10i\
|
|
||||||
\
|
|
||||||
ndkVersion = "28.2.13676358"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
|
||||||
|
|
||||||
- name: Restore cached libhevtun
|
|
||||||
id: cache-libhevtun-restore
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/libs
|
|
||||||
key: libhevtun-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hev-socks5-tunnel/HEAD') }}-${{ hashFiles('compile-hevtun.sh') }}
|
|
||||||
|
|
||||||
- name: Build libhevtun
|
|
||||||
if: steps.cache-libhevtun-restore.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
bash compile-hevtun.sh
|
|
||||||
|
|
||||||
- name: Save libhevtun
|
|
||||||
if: steps.cache-libhevtun-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/libs
|
|
||||||
key: libhevtun-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hev-socks5-tunnel/HEAD') }}-${{ hashFiles('compile-hevtun.sh') }}
|
|
||||||
|
|
||||||
- name: Copy libhevtun
|
|
||||||
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.13
|
|
||||||
with:
|
|
||||||
repository: '2dust/AndroidLibXrayLite'
|
|
||||||
tag: ${{ env.CURRENT_TAG }}
|
|
||||||
fileName: 'libv2ray.aar'
|
|
||||||
out-file-path: V2rayNG/app/libs/
|
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '21'
|
|
||||||
|
|
||||||
- name: Decode Keystore
|
|
||||||
uses: timheuer/base64-to-file@v2.0.0
|
|
||||||
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@v7
|
|
||||||
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@v7
|
|
||||||
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@v7
|
|
||||||
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/*/release/*.apk
|
|
||||||
tag: ${{ github.event.inputs.release_tag }}
|
|
||||||
file_glob: true
|
|
||||||
prerelease: true
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
name: Validate Fastlane metadata
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches: [ "master" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "master" ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
go:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Validate Fastlane Supply Metadata
|
|
||||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0
|
|
||||||
+5
-62
@@ -1,66 +1,9 @@
|
|||||||
# Ignore data and key store files
|
V2rayNG/app/src/main/res/layout/activity_inapp_buy.xml
|
||||||
|
V2rayNG/app/src/main/assets/geoip.dat
|
||||||
|
V2rayNG/app/src/main/assets/geosite.dat
|
||||||
|
V2rayNG/app/src/main/java/com/v2ray/ang/InappBuyActivity.java
|
||||||
*.dat
|
*.dat
|
||||||
*.jks
|
*.jks
|
||||||
|
|
||||||
# Ignore output JSON file
|
|
||||||
V2rayNG/app/release/output.json
|
V2rayNG/app/release/output.json
|
||||||
|
|
||||||
# Ignore IDE and build system directories
|
|
||||||
.idea/
|
.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
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[submodule "AndroidLibXrayLite"]
|
|
||||||
path = AndroidLibXrayLite
|
|
||||||
url = https://github.com/2dust/AndroidLibXrayLite
|
|
||||||
[submodule "hev-socks5-tunnel"]
|
|
||||||
path = hev-socks5-tunnel
|
|
||||||
url = https://github.com/heiher/hev-socks5-tunnel
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
sudo: required
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- "1.12"
|
||||||
|
go_import_path: github.com/2dust/AndroidLibV2rayLite
|
||||||
|
git:
|
||||||
|
depth: 5
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
update: true
|
||||||
|
before_script:
|
||||||
|
- sudo ntpdate -u time.google.com
|
||||||
|
- date
|
||||||
|
- make all
|
||||||
|
- make downloadGoMobile
|
||||||
|
script:
|
||||||
|
- make BuildMobile
|
||||||
|
after_success:
|
||||||
|
deploy:
|
||||||
|
provider: releases
|
||||||
|
api_key: ${GH_TOKEN}
|
||||||
|
file:
|
||||||
|
- libv2ray.aar
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package CoreI
|
||||||
|
|
||||||
|
import (
|
||||||
|
v2core "v2ray.com/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
IsRunning bool
|
||||||
|
IsTRunning bool
|
||||||
|
PackageName string
|
||||||
|
PackageCodePath string
|
||||||
|
|
||||||
|
Vpoint v2core.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckVersion() int {
|
||||||
|
return 22
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
fetchDep:
|
||||||
|
-go get github.com/2dust/AndroidLibV2rayLite
|
||||||
|
go get github.com/2dust/AndroidLibV2rayLite
|
||||||
|
|
||||||
|
ANDROID_HOME=$(HOME)/android-sdk-linux
|
||||||
|
export ANDROID_HOME
|
||||||
|
PATH:=$(PATH):$(GOPATH)/bin
|
||||||
|
export PATH
|
||||||
|
downloadGoMobile:
|
||||||
|
go get golang.org/x/mobile/cmd/...
|
||||||
|
sudo apt-get install -qq libstdc++6:i386 lib32z1 expect
|
||||||
|
cd ~ ;curl -L https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/ubuntu-cli-install-android-sdk.sh | sudo bash - > /dev/null
|
||||||
|
ls ~
|
||||||
|
ls ~/android-sdk-linux/
|
||||||
|
gomobile init ;gomobile bind -v -tags json github.com/2dust/AndroidLibV2rayLite
|
||||||
|
|
||||||
|
BuildMobile:
|
||||||
|
@echo Stub
|
||||||
|
|
||||||
|
all: asset pb fetchDep
|
||||||
|
@echo DONE
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# AndroidLibV2rayLite
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package VPN
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
v2net "v2ray.com/core/common/net"
|
||||||
|
v2internet "v2ray.com/core/transport/internet"
|
||||||
|
)
|
||||||
|
|
||||||
|
type protectSet interface {
|
||||||
|
Protect(int) int
|
||||||
|
}
|
||||||
|
|
||||||
|
type resolved struct {
|
||||||
|
domain string
|
||||||
|
IPs []net.IP
|
||||||
|
Port int
|
||||||
|
ipIdx uint8
|
||||||
|
ipLock sync.Mutex
|
||||||
|
lastSwitched time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextIP switch to another resolved result.
|
||||||
|
// there still be race-condition here if multiple err concurently occured
|
||||||
|
// may cause idx keep switching,
|
||||||
|
// but that's an outside error can hardly handled here
|
||||||
|
func (r *resolved) NextIP() {
|
||||||
|
r.ipLock.Lock()
|
||||||
|
defer r.ipLock.Unlock()
|
||||||
|
|
||||||
|
if len(r.IPs) > 1 {
|
||||||
|
|
||||||
|
// throttle, don't switch too quickly
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(r.lastSwitched) < time.Second*5 {
|
||||||
|
log.Println("switch too quickly")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.lastSwitched = now
|
||||||
|
r.ipIdx++
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.ipIdx >= uint8(len(r.IPs)) {
|
||||||
|
r.ipIdx = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cur := r.currentIP()
|
||||||
|
log.Printf("switched to next IP: %s", cur)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resolved) currentIP() net.IP {
|
||||||
|
if len(r.IPs) > 0 {
|
||||||
|
return r.IPs[r.ipIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPreotectedDialer ...
|
||||||
|
func NewPreotectedDialer(p protectSet) *ProtectedDialer {
|
||||||
|
d := &ProtectedDialer{
|
||||||
|
// prefer native lookup on Android
|
||||||
|
resolver: &net.Resolver{PreferGo: false},
|
||||||
|
protectSet: p,
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtectedDialer ...
|
||||||
|
type ProtectedDialer struct {
|
||||||
|
currentServer string
|
||||||
|
resolveChan chan struct{}
|
||||||
|
|
||||||
|
vServer *resolved
|
||||||
|
resolver *net.Resolver
|
||||||
|
|
||||||
|
protectSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtectedDialer) IsVServerReady() bool {
|
||||||
|
return (d.vServer != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtectedDialer) PrepareResolveChan() {
|
||||||
|
d.resolveChan = make(chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtectedDialer) ResolveChan() <-chan struct{} {
|
||||||
|
return d.resolveChan
|
||||||
|
}
|
||||||
|
|
||||||
|
// simplicated version of golang: internetAddrList in src/net/ipsock.go
|
||||||
|
func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
host, port string
|
||||||
|
portnum int
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if host, port, err = net.SplitHostPort(addr); err != nil {
|
||||||
|
log.Printf("PrepareDomain SplitHostPort Err: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if portnum, err = d.resolver.LookupPort(ctx, "tcp", port); err != nil {
|
||||||
|
log.Printf("PrepareDomain LookupPort Err: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := d.resolver.LookupIPAddr(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
return nil, fmt.Errorf("domain %s Failed to resolve", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
IPs := make([]net.IP, len(addrs))
|
||||||
|
for i, ia := range addrs {
|
||||||
|
IPs[i] = ia.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := &resolved{
|
||||||
|
domain: host,
|
||||||
|
IPs: IPs,
|
||||||
|
Port: portnum,
|
||||||
|
}
|
||||||
|
|
||||||
|
return rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareDomain caches direct v2ray server host
|
||||||
|
func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct{}) {
|
||||||
|
log.Printf("Preparing Domain: %s", domainName)
|
||||||
|
d.currentServer = domainName
|
||||||
|
|
||||||
|
defer close(d.resolveChan)
|
||||||
|
maxRetry := 10
|
||||||
|
for {
|
||||||
|
if maxRetry == 0 {
|
||||||
|
log.Println("PrepareDomain maxRetry reached. exiting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved, err := d.lookupAddr(domainName)
|
||||||
|
if err != nil {
|
||||||
|
maxRetry--
|
||||||
|
log.Printf("PrepareDomain err: %v\n", err)
|
||||||
|
select {
|
||||||
|
case <-closeCh:
|
||||||
|
log.Printf("PrepareDomain exit due to v2ray closed")
|
||||||
|
return
|
||||||
|
case <-time.After(time.Second * 2):
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
d.vServer = resolved
|
||||||
|
log.Printf("Prepare Result:\n Domain: %s\n Port: %d\n IPs: %v\n",
|
||||||
|
resolved.domain, resolved.Port, resolved.IPs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtectedDialer) getFd(network v2net.Network) (fd int, err error) {
|
||||||
|
switch network {
|
||||||
|
case v2net.Network_TCP:
|
||||||
|
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
|
||||||
|
case v2net.Network_UDP:
|
||||||
|
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknow network")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial exported as the protected dial method
|
||||||
|
func (d *ProtectedDialer) Dial(ctx context.Context,
|
||||||
|
src v2net.Address, dest v2net.Destination, sockopt *v2internet.SocketConfig) (net.Conn, error) {
|
||||||
|
|
||||||
|
network := dest.Network.SystemString()
|
||||||
|
Address := dest.NetAddr()
|
||||||
|
|
||||||
|
// v2ray server address,
|
||||||
|
// try to connect fixed IP if multiple IP parsed from domain,
|
||||||
|
// and switch to next IP if error occurred
|
||||||
|
if strings.Compare(Address, d.currentServer) == 0 {
|
||||||
|
if d.vServer == nil {
|
||||||
|
log.Println("Dial pending prepare ...", Address)
|
||||||
|
<-d.resolveChan
|
||||||
|
|
||||||
|
// user may close connection during PrepareDomain,
|
||||||
|
// fast return release resources.
|
||||||
|
if d.vServer == nil {
|
||||||
|
return nil, fmt.Errorf("fail to prepare domain %s", d.currentServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := d.getFd(dest.Network)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
curIP := d.vServer.currentIP()
|
||||||
|
conn, err := d.fdConn(ctx, curIP, d.vServer.Port, fd)
|
||||||
|
if err != nil {
|
||||||
|
d.vServer.NextIP()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Printf("Using Prepared: %s", curIP)
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// v2ray connecting to "domestic" servers, no caching results
|
||||||
|
log.Printf("Not Using Prepared: %s,%s", network, Address)
|
||||||
|
resolved, err := d.lookupAddr(Address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := d.getFd(dest.Network)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the first resolved address.
|
||||||
|
// the result IP may vary, eg: IPv6 addrs comes first if client has ipv6 address
|
||||||
|
return d.fdConn(ctx, resolved.IPs[0], resolved.Port, fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProtectedDialer) fdConn(ctx context.Context, ip net.IP, port int, fd int) (net.Conn, error) {
|
||||||
|
|
||||||
|
defer unix.Close(fd)
|
||||||
|
|
||||||
|
// call android VPN service to "protect" the fd connecting straight out
|
||||||
|
d.Protect(fd)
|
||||||
|
|
||||||
|
sa := &unix.SockaddrInet6{
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
copy(sa.Addr[:], ip)
|
||||||
|
|
||||||
|
if err := unix.Connect(fd, sa); err != nil {
|
||||||
|
log.Printf("fdConn unix.Connect err, Close Fd: %d Err: %v", fd, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := os.NewFile(uintptr(fd), "Socket")
|
||||||
|
if file == nil {
|
||||||
|
// returned value will be nil if fd is not a valid file descriptor
|
||||||
|
return nil, errors.New("fdConn fd invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
//Closing conn does not affect file, and closing file does not affect conn.
|
||||||
|
conn, err := net.FileConn(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("fdConn FileConn Close Fd: %d Err: %v", fd, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package VPN
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v2net "v2ray.com/core/common/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeSupportSet struct{}
|
||||||
|
|
||||||
|
func (f fakeSupportSet) Protect(int) int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProtectedDialer_PrepareDomain(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
domainName string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
{"", args{"baidu.com:80"}},
|
||||||
|
// {"", args{"cloudflare.com:443"}},
|
||||||
|
// {"", args{"apple.com:443"}},
|
||||||
|
// {"", args{"110.110.110.110:443"}},
|
||||||
|
// {"", args{"[2002:1234::1]:443"}},
|
||||||
|
}
|
||||||
|
d := NewPreotectedDialer(fakeSupportSet{})
|
||||||
|
for _, tt := range tests {
|
||||||
|
ch := make(chan struct{})
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
go d.PrepareDomain(tt.args.domainName, ch)
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
go d.vServer.NextIP()
|
||||||
|
t.Log(d.vServer.currentIP())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProtectedDialer_Dial(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
{"baidu.com:80", false},
|
||||||
|
{"cloudflare.com:80", false},
|
||||||
|
{"172.16.192.11:80", true},
|
||||||
|
// {"172.16.192.10:80", true},
|
||||||
|
// {"[2fff:4322::1]:443", true},
|
||||||
|
// {"[fc00::1]:443", true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ch := make(chan struct{})
|
||||||
|
|
||||||
|
d := NewPreotectedDialer(fakeSupportSet{})
|
||||||
|
d.currentServer = tt.name
|
||||||
|
|
||||||
|
go d.PrepareDomain(tt.name, ch)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
dial := func() {
|
||||||
|
defer wg.Done()
|
||||||
|
dest, _ := v2net.ParseDestination("tcp:" + tt.name)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := d.Dial(ctx, nil, dest, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_host, _, _ := net.SplitHostPort(tt.name)
|
||||||
|
fmt.Fprintf(conn, fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\n\r\n", _host))
|
||||||
|
status, err := bufio.NewReader(conn).ReadString('\n')
|
||||||
|
t.Logf("%#v, %#v\n", status, err)
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for n := 0; n < 3; n++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go dial()
|
||||||
|
// time.Sleep(time.Millisecond * 10)
|
||||||
|
// d.pendingMap[tt.name] = make(chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_resolved_NextIP(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
domain string
|
||||||
|
IPs []net.IP
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
}{
|
||||||
|
// TODO: Add test cases.
|
||||||
|
{"test1",
|
||||||
|
fields{
|
||||||
|
domain: "www.baidu.com",
|
||||||
|
IPs: []net.IP{
|
||||||
|
net.ParseIP("1.2.3.4"),
|
||||||
|
net.ParseIP("4.3.2.1"),
|
||||||
|
net.ParseIP("1234::1"),
|
||||||
|
net.ParseIP("4321::1"),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := &resolved{
|
||||||
|
domain: tt.fields.domain,
|
||||||
|
IPs: tt.fields.IPs,
|
||||||
|
Port: tt.fields.Port,
|
||||||
|
}
|
||||||
|
t.Logf("%v", r.IPs)
|
||||||
|
t.Logf("%v", r.currentIP())
|
||||||
|
r.NextIP()
|
||||||
|
t.Logf("%v", r.currentIP())
|
||||||
|
r.NextIP()
|
||||||
|
t.Logf("%v", r.currentIP())
|
||||||
|
r.NextIP()
|
||||||
|
t.Logf("%v", r.currentIP())
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
r.NextIP()
|
||||||
|
t.Logf("%v", r.currentIP())
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
r.NextIP()
|
||||||
|
t.Logf("%v", r.currentIP())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
set -o nounset
|
||||||
|
|
||||||
|
# Set magic variables for current file & dir
|
||||||
|
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
||||||
|
__base="$(basename ${__file} .sh)"
|
||||||
|
|
||||||
|
if [[ ! -d $NDK_HOME ]]; then
|
||||||
|
echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
clear_tmp () {
|
||||||
|
rm -rf $TMPDIR
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; clear_tmp; exit 1' ERR INT
|
||||||
|
install -m644 $__dir/tun2socks.mk $TMPDIR/
|
||||||
|
|
||||||
|
pushd $TMPDIR
|
||||||
|
git clone --depth=1 https://github.com/shadowsocks/badvpn.git
|
||||||
|
git clone --depth=1 https://github.com/shadowsocks/libancillary.git
|
||||||
|
$NDK_HOME/ndk-build \
|
||||||
|
NDK_PROJECT_PATH=. \
|
||||||
|
APP_BUILD_SCRIPT=./tun2socks.mk \
|
||||||
|
APP_ABI=all \
|
||||||
|
APP_PLATFORM=android-19 \
|
||||||
|
NDK_LIBS_OUT=$TMPDIR/libs \
|
||||||
|
NDK_OUT=$TMPDIR/tmp \
|
||||||
|
APP_SHORT_COMMANDS=false LOCAL_SHORT_COMMANDS=false -B -j4
|
||||||
|
|
||||||
|
install -v -m755 libs/armeabi-v7a/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/armeabi-v7a/libtun2socks.so
|
||||||
|
install -v -m755 libs/arm64-v8a/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/arm64-v8a/libtun2socks.so
|
||||||
|
install -v -m755 libs/x86/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/x86/libtun2socks.so
|
||||||
|
install -v -m755 libs/x86_64/tun2socks $__dir/../V2rayNG/app/src/main/jniLibs/x86_64/libtun2socks.so
|
||||||
|
popd
|
||||||
|
|
||||||
|
rm -rf $TMPDIR
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
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 "v2ray.com/core"
|
||||||
|
v2filesystem "v2ray.com/core/common/platform/filesystem"
|
||||||
|
v2stats "v2ray.com/core/features/stats"
|
||||||
|
v2serial "v2ray.com/core/infra/conf/serial"
|
||||||
|
v2internet "v2ray.com/core/transport/internet"
|
||||||
|
|
||||||
|
v2applog "v2ray.com/core/app/log"
|
||||||
|
v2commlog "v2ray.com/core/common/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
v2Assert = "v2ray.location.asset"
|
||||||
|
assetperfix = "/dev/libv2rayfs0/asset"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*V2RayPoint V2Ray Point Server
|
||||||
|
This is territory of Go, so no getter and setters!
|
||||||
|
*/
|
||||||
|
type V2RayPoint struct {
|
||||||
|
SupportSet V2RayVPNServiceSupportsSet
|
||||||
|
statsManager v2stats.Manager
|
||||||
|
|
||||||
|
dialer *VPN.ProtectedDialer
|
||||||
|
status *CoreI.Status
|
||||||
|
escorter *Escort.Escorting
|
||||||
|
v2rayOP *sync.Mutex
|
||||||
|
closeChan chan struct{}
|
||||||
|
|
||||||
|
PackageName string
|
||||||
|
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())
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package libv2ray
|
||||||
|
|
||||||
|
import (
|
||||||
|
// The following are necessary as they register handlers in their init functions.
|
||||||
|
|
||||||
|
// Required features. Can't remove unless there is replacements.
|
||||||
|
_ "v2ray.com/core/app/dispatcher"
|
||||||
|
_ "v2ray.com/core/app/proxyman/inbound"
|
||||||
|
_ "v2ray.com/core/app/proxyman/outbound"
|
||||||
|
|
||||||
|
// Other optional features.
|
||||||
|
_ "v2ray.com/core/app/dns"
|
||||||
|
_ "v2ray.com/core/app/log"
|
||||||
|
_ "v2ray.com/core/app/policy"
|
||||||
|
_ "v2ray.com/core/app/router"
|
||||||
|
_ "v2ray.com/core/app/stats"
|
||||||
|
|
||||||
|
// Inbound and outbound proxies.
|
||||||
|
_ "v2ray.com/core/proxy/blackhole"
|
||||||
|
_ "v2ray.com/core/proxy/dns"
|
||||||
|
_ "v2ray.com/core/proxy/dokodemo"
|
||||||
|
_ "v2ray.com/core/proxy/freedom"
|
||||||
|
_ "v2ray.com/core/proxy/http"
|
||||||
|
_ "v2ray.com/core/proxy/mtproto"
|
||||||
|
_ "v2ray.com/core/proxy/shadowsocks"
|
||||||
|
_ "v2ray.com/core/proxy/socks"
|
||||||
|
_ "v2ray.com/core/proxy/trojan"
|
||||||
|
_ "v2ray.com/core/proxy/vless/inbound"
|
||||||
|
_ "v2ray.com/core/proxy/vless/outbound"
|
||||||
|
_ "v2ray.com/core/proxy/vmess/inbound"
|
||||||
|
_ "v2ray.com/core/proxy/vmess/outbound"
|
||||||
|
|
||||||
|
// Transport
|
||||||
|
_ "v2ray.com/core/transport/internet/http"
|
||||||
|
_ "v2ray.com/core/transport/internet/kcp"
|
||||||
|
_ "v2ray.com/core/transport/internet/quic"
|
||||||
|
_ "v2ray.com/core/transport/internet/tcp"
|
||||||
|
_ "v2ray.com/core/transport/internet/tls"
|
||||||
|
_ "v2ray.com/core/transport/internet/udp"
|
||||||
|
_ "v2ray.com/core/transport/internet/websocket"
|
||||||
|
|
||||||
|
// Transport headers
|
||||||
|
_ "v2ray.com/core/transport/internet/headers/http"
|
||||||
|
_ "v2ray.com/core/transport/internet/headers/noop"
|
||||||
|
_ "v2ray.com/core/transport/internet/headers/srtp"
|
||||||
|
_ "v2ray.com/core/transport/internet/headers/tls"
|
||||||
|
_ "v2ray.com/core/transport/internet/headers/utp"
|
||||||
|
_ "v2ray.com/core/transport/internet/headers/wechat"
|
||||||
|
_ "v2ray.com/core/transport/internet/headers/wireguard"
|
||||||
|
|
||||||
|
// JSON config support. Choose only one from the two below.
|
||||||
|
// The following line loads JSON from v2ctl
|
||||||
|
// _ "v2ray.com/core/main/json"
|
||||||
|
// The following line loads JSON internally
|
||||||
|
_ "v2ray.com/core/main/jsonem"
|
||||||
|
// Load config from file or http(s)
|
||||||
|
// _ "v2ray.com/core/main/confloader/external"
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package libv2ray
|
||||||
|
|
||||||
|
//go:generate make all
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
readme.txt
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Copyright (C) 2009 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
LOCAL_PATH := $(call my-dir)
|
||||||
|
ROOT_PATH := $(LOCAL_PATH)
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
## libancillary
|
||||||
|
########################################################
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
|
||||||
|
ANCILLARY_SOURCE := fd_recv.c fd_send.c
|
||||||
|
|
||||||
|
LOCAL_MODULE := libancillary
|
||||||
|
#LOCAL_CFLAGS += -I$(LOCAL_PATH)/libancillary
|
||||||
|
LOCAL_C_INCLUDES := $(LOCAL_PATH)/libancillary
|
||||||
|
LOCAL_SRC_FILES := $(addprefix libancillary/, $(ANCILLARY_SOURCE))
|
||||||
|
|
||||||
|
include $(BUILD_STATIC_LIBRARY)
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
## tun2socks
|
||||||
|
########################################################
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
|
||||||
|
LOCAL_CFLAGS := -std=gnu99
|
||||||
|
LOCAL_CFLAGS += -DBADVPN_THREADWORK_USE_PTHREAD -DBADVPN_LINUX -DBADVPN_BREACTOR_BADVPN -D_GNU_SOURCE
|
||||||
|
LOCAL_CFLAGS += -DBADVPN_USE_SIGNALFD -DBADVPN_USE_EPOLL
|
||||||
|
LOCAL_CFLAGS += -DBADVPN_LITTLE_ENDIAN -DBADVPN_THREAD_SAFE
|
||||||
|
LOCAL_CFLAGS += -DNDEBUG -DANDROID
|
||||||
|
LOCAL_CFLAGS += -I
|
||||||
|
|
||||||
|
LOCAL_STATIC_LIBRARIES := libancillary
|
||||||
|
|
||||||
|
LOCAL_C_INCLUDES := \
|
||||||
|
$(LOCAL_PATH)/badvpn/libancillary \
|
||||||
|
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv4 \
|
||||||
|
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv6 \
|
||||||
|
$(LOCAL_PATH)/badvpn/lwip/src/include \
|
||||||
|
$(LOCAL_PATH)/badvpn/lwip/custom \
|
||||||
|
$(LOCAL_PATH)/badvpn \
|
||||||
|
$(LOCAL_PATH)/libancillary
|
||||||
|
|
||||||
|
TUN2SOCKS_SOURCES := \
|
||||||
|
base/BLog_syslog.c \
|
||||||
|
system/BReactor_badvpn.c \
|
||||||
|
system/BSignal.c \
|
||||||
|
system/BConnection_common.c \
|
||||||
|
system/BConnection_unix.c \
|
||||||
|
system/BTime.c \
|
||||||
|
system/BUnixSignal.c \
|
||||||
|
system/BNetwork.c \
|
||||||
|
flow/StreamRecvInterface.c \
|
||||||
|
flow/PacketRecvInterface.c \
|
||||||
|
flow/PacketPassInterface.c \
|
||||||
|
flow/StreamPassInterface.c \
|
||||||
|
flow/SinglePacketBuffer.c \
|
||||||
|
flow/BufferWriter.c \
|
||||||
|
flow/PacketBuffer.c \
|
||||||
|
flow/PacketStreamSender.c \
|
||||||
|
flow/PacketPassConnector.c \
|
||||||
|
flow/PacketProtoFlow.c \
|
||||||
|
flow/PacketPassFairQueue.c \
|
||||||
|
flow/PacketProtoEncoder.c \
|
||||||
|
flow/PacketProtoDecoder.c \
|
||||||
|
socksclient/BSocksClient.c \
|
||||||
|
tuntap/BTap.c \
|
||||||
|
lwip/src/core/udp.c \
|
||||||
|
lwip/src/core/memp.c \
|
||||||
|
lwip/src/core/init.c \
|
||||||
|
lwip/src/core/pbuf.c \
|
||||||
|
lwip/src/core/tcp.c \
|
||||||
|
lwip/src/core/tcp_out.c \
|
||||||
|
lwip/src/core/netif.c \
|
||||||
|
lwip/src/core/def.c \
|
||||||
|
lwip/src/core/ip.c \
|
||||||
|
lwip/src/core/mem.c \
|
||||||
|
lwip/src/core/tcp_in.c \
|
||||||
|
lwip/src/core/stats.c \
|
||||||
|
lwip/src/core/inet_chksum.c \
|
||||||
|
lwip/src/core/timeouts.c \
|
||||||
|
lwip/src/core/ipv4/icmp.c \
|
||||||
|
lwip/src/core/ipv4/igmp.c \
|
||||||
|
lwip/src/core/ipv4/ip4_addr.c \
|
||||||
|
lwip/src/core/ipv4/ip4_frag.c \
|
||||||
|
lwip/src/core/ipv4/ip4.c \
|
||||||
|
lwip/src/core/ipv4/autoip.c \
|
||||||
|
lwip/src/core/ipv6/ethip6.c \
|
||||||
|
lwip/src/core/ipv6/inet6.c \
|
||||||
|
lwip/src/core/ipv6/ip6_addr.c \
|
||||||
|
lwip/src/core/ipv6/mld6.c \
|
||||||
|
lwip/src/core/ipv6/dhcp6.c \
|
||||||
|
lwip/src/core/ipv6/icmp6.c \
|
||||||
|
lwip/src/core/ipv6/ip6.c \
|
||||||
|
lwip/src/core/ipv6/ip6_frag.c \
|
||||||
|
lwip/src/core/ipv6/nd6.c \
|
||||||
|
lwip/custom/sys.c \
|
||||||
|
tun2socks/tun2socks.c \
|
||||||
|
base/DebugObject.c \
|
||||||
|
base/BLog.c \
|
||||||
|
base/BPending.c \
|
||||||
|
system/BDatagram_unix.c \
|
||||||
|
flowextra/PacketPassInactivityMonitor.c \
|
||||||
|
tun2socks/SocksUdpGwClient.c \
|
||||||
|
udpgw_client/UdpGwClient.c
|
||||||
|
|
||||||
|
LOCAL_MODULE := tun2socks
|
||||||
|
|
||||||
|
LOCAL_LDLIBS := -ldl -llog
|
||||||
|
|
||||||
|
LOCAL_SRC_FILES := $(addprefix badvpn/, $(TUN2SOCKS_SOURCES))
|
||||||
|
|
||||||
|
include $(BUILD_SYSTEM)/build-executable.mk
|
||||||
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package libv2ray
|
||||||
|
|
||||||
|
// This struct creates our own log writer without datatime stamp
|
||||||
|
// As Android adds time stamps on each line
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
v2commlog "v2ray.com/core/common/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type consoleLogWriter struct {
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *consoleLogWriter) Write(s string) error {
|
||||||
|
w.logger.Print(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *consoleLogWriter) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This logger won't print data/time stamps
|
||||||
|
func createStdoutLogWriter() v2commlog.WriterCreator {
|
||||||
|
return func() v2commlog.Writer {
|
||||||
|
return &consoleLogWriter{
|
||||||
|
logger: log.New(os.Stdout, "", 0)}
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule AndroidLibXrayLite deleted from 1b0ec8e111
@@ -1,40 +1,29 @@
|
|||||||
**v2rayNG 隐私权政策**
|
v2rayNG 隐私条款
|
||||||
|
|
||||||
本政策自2023年11月17日起施行
|
最后更新 2017-11-22
|
||||||
|
|
||||||
2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。
|
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
||||||
|
|
||||||
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
1. 信息收集
|
||||||
|
|
||||||
**信息收集**
|
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
|
||||||
|
|
||||||
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。
|
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
||||||
|
|
||||||
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
||||||
|
|
||||||
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
2. 信息共享
|
||||||
|
|
||||||
**信息共享**
|
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
||||||
|
|
||||||
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
3. 信息存留
|
||||||
|
|
||||||
**信息存留**
|
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
||||||
|
|
||||||
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
4. 信息泄露
|
||||||
|
|
||||||
**信息泄露**
|
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
||||||
|
|
||||||
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
5. 条款修改
|
||||||
|
|
||||||
**儿童隐私**
|
|
||||||
|
|
||||||
这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。
|
|
||||||
|
|
||||||
**条款修改**
|
|
||||||
|
|
||||||
我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。**
|
|
||||||
|
|
||||||
**联系我们**
|
|
||||||
|
|
||||||
如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。
|
|
||||||
|
|
||||||
|
我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
|
||||||
|
|||||||
@@ -2,31 +2,34 @@
|
|||||||
|
|
||||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
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://developer.android.com/about/versions/jelly-bean#android-4.2)
|
||||||
[](https://kotlinlang.org)
|
[](https://kotlinlang.org)
|
||||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||||
[](https://github.com/2dust/v2rayNG/releases)
|
[](https://github.com/2dust/v2rayNG/releases)
|
||||||
[](https://t.me/v2rayn)
|
[](https://t.me/v2rayn)
|
||||||
|
|
||||||
### Telegram Channel
|
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
||||||
[github_2dust](https://t.me/github_2dust)
|
<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>
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
#### Geoip and Geosite
|
#### 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)
|
v2rayNG release already embedded domain file `geoip.dat` and `geosite.dat`. However it is (probably) not the latest and not the most complete list.
|
||||||
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
For power user, the embedded files can be easily replaced with the following steps:
|
||||||
- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually
|
1. Launch v2rayNG (v1.4.9+)
|
||||||
- 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)
|
2. Find existing geoip.dat and geosite.dat in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
||||||
|
3. Replace them with the latest [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip)
|
||||||
|
4. Enhanced version can be found in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (recommend to use `geosite:geolocation-!cn` for proxy dns and routing)
|
||||||
|
5. It is also 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)
|
#### See more in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||||
|
|
||||||
### Development guide
|
### 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.
|
Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
|
||||||
The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
|
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)
|
||||||
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/)
|
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
|
v2rayNG can run on Android Emulators, with minimum Android 5.0
|
||||||
`appops set [package name] ACTIVATE_VPN allow`
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
*.apk
|
||||||
|
signing.properties
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/build
|
||||||
|
/google-services.json
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion Integer.parseInt("$compileSdkVer")
|
||||||
|
buildToolsVersion buildToolsVer
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility = "8"
|
||||||
|
sourceCompatibility = "8"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.v2ray.ang"
|
||||||
|
minSdkVersion 17
|
||||||
|
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.versionCodeOverride =
|
||||||
|
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
|
||||||
|
1000000 + android.defaultConfig.versionCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
testImplementation 'junit:junit:4.13'
|
||||||
|
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
|
||||||
|
|
||||||
|
// Androidx ktx
|
||||||
|
implementation 'android.arch.lifecycle:extensions:1.1.1'
|
||||||
|
implementation 'android.arch.lifecycle:livedata:1.1.1'
|
||||||
|
|
||||||
|
// Android support library
|
||||||
|
implementation "com.android.support:support-v4:$supportLibVersion"
|
||||||
|
implementation "com.android.support:appcompat-v7:$supportLibVersion"
|
||||||
|
implementation "com.android.support:design:$supportLibVersion"
|
||||||
|
implementation "com.android.support:cardview-v7:$supportLibVersion"
|
||||||
|
implementation "com.android.support:preference-v7:$supportLibVersion"
|
||||||
|
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
|
||||||
|
implementation "com.android.support:multidex:1.0.3"
|
||||||
|
implementation 'com.android.support.constraint:constraint-layout:2.0.1'
|
||||||
|
|
||||||
|
// DSL
|
||||||
|
implementation 'com.tencent:mmkv-static:1.0.19'
|
||||||
|
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(name: 'libv2ray', ext: 'aar')
|
||||||
|
//implementation(name: 'tun2socks', ext: 'aar')
|
||||||
|
}
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
maven { url 'https://maven.google.com' }
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
flatDir {
|
||||||
|
dirs 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.android.application)
|
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
id("com.jaredsburrows.license")
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.v2ray.ang"
|
|
||||||
compileSdk = 36
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "com.v2ray.ang"
|
|
||||||
minSdk = 24
|
|
||||||
targetSdk = 36
|
|
||||||
versionCode = 725
|
|
||||||
versionName = "2.1.5"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
implementation(libs.androidx.viewpager2)
|
|
||||||
implementation(libs.androidx.fragment)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
implementation(libs.okhttp)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite
|
||||||
Binary file not shown.
Vendored
+45
-8
@@ -1,10 +1,14 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Add project specific ProGuard rules here.
|
||||||
# You can control the set of applied configuration files using the
|
# By default, the flags in this file are appended to flags specified
|
||||||
# proguardFiles setting in build.gradle.
|
# in G:\android-sdk/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
#
|
#
|
||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# 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
|
# If your project uses WebView with JS, uncomment the following
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
# class:
|
# class:
|
||||||
@@ -12,10 +16,43 @@
|
|||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||||
# debugging stack traces.
|
# removes such information by default, so configure it to keep all of it.
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
-keepattributes Signature
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# For using GSON @Expose annotation
|
||||||
# hide the original source file name.
|
-keepattributes *Annotation*
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
|
# 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.** { *;}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<item name="app_name" type="string">v2rayNG (DEV)</item>
|
|
||||||
</resources>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name" translatable="false">v2rayNG (F-Droid)</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/ic_qu_switch_24dp"
|
|
||||||
android:shortcutDisabledMessage="@string/app_widget_name"
|
|
||||||
android:shortcutId="shortcuts_switch"
|
|
||||||
android:shortcutLongLabel="@string/app_widget_name"
|
|
||||||
android:shortcutShortLabel="@string/app_widget_name">
|
|
||||||
|
|
||||||
<!--suppress AndroidDomInspection -->
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.VIEW"
|
|
||||||
android:targetClass="com.v2ray.ang.ui.ScSwitchActivity"
|
|
||||||
android:targetPackage="com.v2ray.ang.fdroid" />
|
|
||||||
<categories android:name="android.shortcut.conversation" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/ic_qu_scan_24dp"
|
|
||||||
android:shortcutDisabledMessage="@string/menu_item_import_config_qrcode"
|
|
||||||
android:shortcutId="shortcuts_scan"
|
|
||||||
android:shortcutLongLabel="@string/menu_item_import_config_qrcode"
|
|
||||||
android:shortcutShortLabel="@string/menu_item_import_config_qrcode">
|
|
||||||
|
|
||||||
<!--suppress AndroidDomInspection -->
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.VIEW"
|
|
||||||
android:targetClass="com.v2ray.ang.ui.ScScannerActivity"
|
|
||||||
android:targetPackage="com.v2ray.ang.fdroid" />
|
|
||||||
<categories android:name="android.shortcut.conversation" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/ic_qu_start_24dp"
|
|
||||||
android:shortcutDisabledMessage="@string/toast_services_start"
|
|
||||||
android:shortcutId="shortcuts_start"
|
|
||||||
android:shortcutLongLabel="@string/toast_services_start"
|
|
||||||
android:shortcutShortLabel="@string/toast_services_start">
|
|
||||||
|
|
||||||
<!--suppress AndroidDomInspection -->
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.VIEW"
|
|
||||||
android:targetClass="com.v2ray.ang.ui.ScStartActivity"
|
|
||||||
android:targetPackage="com.v2ray.ang.fdroid" />
|
|
||||||
<categories android:name="android.shortcut.conversation" />
|
|
||||||
</shortcut>
|
|
||||||
<shortcut
|
|
||||||
android:enabled="true"
|
|
||||||
android:icon="@drawable/ic_qu_stop_24dp"
|
|
||||||
android:shortcutDisabledMessage="@string/toast_services_stop"
|
|
||||||
android:shortcutId="shortcuts_stop"
|
|
||||||
android:shortcutLongLabel="@string/toast_services_stop"
|
|
||||||
android:shortcutShortLabel="@string/toast_services_stop">
|
|
||||||
|
|
||||||
<!--suppress AndroidDomInspection -->
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.VIEW"
|
|
||||||
android:targetClass="com.v2ray.ang.ui.ScStopActivity"
|
|
||||||
android:targetPackage="com.v2ray.ang.fdroid" />
|
|
||||||
<categories android:name="android.shortcut.conversation" />
|
|
||||||
</shortcut>
|
|
||||||
</shortcuts>
|
|
||||||
@@ -1,301 +1,145 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:ignore="MissingLeanbackLauncher">
|
package="com.v2ray.ang">
|
||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
android:largeScreens="true"
|
android:smallScreens="true"
|
||||||
android:normalScreens="true"
|
android:normalScreens="true"
|
||||||
android:smallScreens="true"
|
android:largeScreens="true"
|
||||||
android:xlargeScreens="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-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 -->
|
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
||||||
<uses-permission
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_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.CHANGE_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<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="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" />
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
|
||||||
android:minSdkVersion="34" />
|
|
||||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AngApplication"
|
android:name=".AngApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@mipmap/ic_banner"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppThemeDayNight.NoActionBar"
|
android:extractNativeLibs="true"
|
||||||
android:usesCleartextTraffic="true">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="true"
|
android:theme="@style/AppTheme.NoActionBar"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
|
||||||
<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.ServerGroupActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.ServerProxyChainActivity"
|
|
||||||
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.AppPickerActivity"
|
|
||||||
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/AppThemeDayNight.NoActionBar.Translucent" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.ScStartActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
|
||||||
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.ScStopActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
|
||||||
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.UrlSchemeActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
|
|
||||||
<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>
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.CheckUpdateActivity"
|
android:name=".ui.ServerActivity"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.BackupActivity"
|
android:name=".ui.ServerCustomConfigActivity"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
|
<activity android:name=".ui.SettingsActivity" />
|
||||||
|
<activity android:name=".ui.PerAppProxyActivity" />
|
||||||
|
<activity android:name=".ui.ScannerActivity" />
|
||||||
|
<!-- <activity android:name=".InappBuyActivity" />-->
|
||||||
|
<activity android:name=".ui.LogcatActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.AboutActivity"
|
android:name=".ui.RoutingSettingsActivity"
|
||||||
android:exported="false" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
|
<activity android:name=".ui.SubSettingActivity" />
|
||||||
|
|
||||||
|
<activity android:name=".ui.SubEditActivity" />
|
||||||
|
<activity android:name=".ui.ScScannerActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScSwitchActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.CoreVpnService"
|
android:name=".service.V2RayVpnService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
tools:ignore="VpnServicePolicy">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService" />
|
<action android:name="android.net.VpnService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="vpn" />
|
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service android:name=".service.V2RayProxyOnlyService"
|
||||||
android:name=".service.CoreProxyOnlyService"
|
android:exported="false"
|
||||||
android:exported="false"
|
android:label="@string/app_name"
|
||||||
android:foregroundServiceType="specialUse"
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
android:label="@string/app_name"
|
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="proxy" />
|
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<receiver android:name=".receiver.WidgetProvider"
|
||||||
android:name=".service.CoreTestService"
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="test" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".receiver.WidgetProvider"
|
|
||||||
android:exported="true"
|
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/app_widget_provider" />
|
android:resource="@xml/app_widget_provider" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
<action android:name="${applicationId}.action.widget.click" />
|
<action android:name="com.v2ray.ang.action.widget.click" />
|
||||||
<action android:name="${applicationId}.action.activity" />
|
<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>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.QSTileService"
|
android:name=".service.QSTileService"
|
||||||
android:exported="true"
|
android:icon="@drawable/ic_v"
|
||||||
android:foregroundServiceType="specialUse"
|
android:label="@string/app_tile_name"
|
||||||
android:icon="@drawable/ic_stat_name"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
android:label="@string/app_tile_name"
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="tile" />
|
|
||||||
</service>
|
</service>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.TaskerActivity"
|
android:name=".ui.TaskerActivity"
|
||||||
android:exported="true"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:label="@string/app_name">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver android:name=".receiver.TaskerReceiver"
|
||||||
android:name=".receiver.TaskerReceiver"
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
android:exported="true"
|
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
|
||||||
tools:ignore="ExportedReceiver">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<provider
|
|
||||||
android:name="androidx.startup.InitializationProvider"
|
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
|
||||||
android:exported="false"
|
|
||||||
tools:node="merge">
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="androidx.work.WorkManagerInitializer"
|
|
||||||
android:value="androidx.startup"
|
|
||||||
tools:node="remove" />
|
|
||||||
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="androidx.work.multiprocess.RemoteWorkManagerService"
|
|
||||||
android:exported="false"
|
|
||||||
android:process=":bg" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.cache"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/cache_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "绕过bittorrent",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"protocol": [
|
|
||||||
"bittorrent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "阻断udp443",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"port": "443",
|
|
||||||
"network": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "代理Google",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"geosite:google"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "阻断udp443",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"port": "443",
|
|
||||||
"network": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "代理Google",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"geosite:google"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "Bypass bittorrent",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"protocol": [
|
|
||||||
"bittorrent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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 Russia domains",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ru"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Bypass Russia IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:ru"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
+1
-173
@@ -4,7 +4,6 @@ au.com.shiftyjelly.pocketcasts
|
|||||||
bbc.mobile.news.ww
|
bbc.mobile.news.ww
|
||||||
be.mygod.vpnhotspot
|
be.mygod.vpnhotspot
|
||||||
ch.protonmail.android
|
ch.protonmail.android
|
||||||
cm.aptoide.pt
|
|
||||||
co.wanqu.android
|
co.wanqu.android
|
||||||
com.alphainventor.filemanager
|
com.alphainventor.filemanager
|
||||||
com.amazon.kindle
|
com.amazon.kindle
|
||||||
@@ -35,9 +34,7 @@ com.chrome.canary
|
|||||||
com.chrome.dev
|
com.chrome.dev
|
||||||
com.cl.newt66y
|
com.cl.newt66y
|
||||||
com.cradle.iitc_mobile
|
com.cradle.iitc_mobile
|
||||||
org.exarhteam.iitc_mobile
|
|
||||||
com.cygames.shadowverse
|
com.cygames.shadowverse
|
||||||
com.dcard.freedom
|
|
||||||
com.devhd.feedly
|
com.devhd.feedly
|
||||||
com.devolver.reigns2
|
com.devolver.reigns2
|
||||||
com.discord
|
com.discord
|
||||||
@@ -111,7 +108,6 @@ com.ifttt.ifttt
|
|||||||
com.imgur.mobile
|
com.imgur.mobile
|
||||||
com.innologica.inoreader
|
com.innologica.inoreader
|
||||||
com.instagram.android
|
com.instagram.android
|
||||||
com.instagram.lite
|
|
||||||
com.instapaper.android
|
com.instapaper.android
|
||||||
com.jarvanh.vpntether
|
com.jarvanh.vpntether
|
||||||
com.kapp.youtube.final
|
com.kapp.youtube.final
|
||||||
@@ -119,7 +115,6 @@ com.klinker.android.twitter_l
|
|||||||
com.lastpass.lpandroid
|
com.lastpass.lpandroid
|
||||||
com.linecorp.linelite
|
com.linecorp.linelite
|
||||||
com.lingodeer
|
com.lingodeer
|
||||||
com.ltnnews.news
|
|
||||||
com.mediapods.tumbpods
|
com.mediapods.tumbpods
|
||||||
com.mgoogle.android.gms
|
com.mgoogle.android.gms
|
||||||
com.microsoft.emmx
|
com.microsoft.emmx
|
||||||
@@ -164,7 +159,6 @@ com.slack
|
|||||||
com.snaptube.premium
|
com.snaptube.premium
|
||||||
com.sololearn
|
com.sololearn
|
||||||
com.sonelli.juicessh
|
com.sonelli.juicessh
|
||||||
com.sparkslab.dcardreader
|
|
||||||
com.spotify.music
|
com.spotify.music
|
||||||
com.tencent.huatuo
|
com.tencent.huatuo
|
||||||
com.termux
|
com.termux
|
||||||
@@ -179,13 +173,10 @@ com.twitter.android
|
|||||||
com.u91porn
|
com.u91porn
|
||||||
com.u9porn
|
com.u9porn
|
||||||
com.ubisoft.dance.justdance2015companion
|
com.ubisoft.dance.justdance2015companion
|
||||||
com.udn.news
|
|
||||||
com.utopia.pxview
|
com.utopia.pxview
|
||||||
|
com.valvesoftware.android.steam.communimunity
|
||||||
com.valvesoftware.android.steam.community
|
com.valvesoftware.android.steam.community
|
||||||
com.vanced.manager
|
|
||||||
com.vanced.android.youtube
|
com.vanced.android.youtube
|
||||||
com.vanced.android.apps.youtube.music
|
|
||||||
com.mgoogle.android.gms
|
|
||||||
com.vimeo.android.videoapp
|
com.vimeo.android.videoapp
|
||||||
com.vivaldi.browser
|
com.vivaldi.browser
|
||||||
com.vivaldi.browser.snapshot
|
com.vivaldi.browser.snapshot
|
||||||
@@ -195,12 +186,10 @@ com.wire
|
|||||||
com.wuxiangai.refactor
|
com.wuxiangai.refactor
|
||||||
com.xda.labs
|
com.xda.labs
|
||||||
com.xvideos.app
|
com.xvideos.app
|
||||||
com.yahoo.mobile.client.android.superapp
|
|
||||||
com.yandex.browser
|
com.yandex.browser
|
||||||
com.yandex.browser.beta
|
com.yandex.browser.beta
|
||||||
com.yandex.browser.alpha
|
com.yandex.browser.alpha
|
||||||
com.z28j.feel
|
com.z28j.feel
|
||||||
com.zhiliaoapp.musically
|
|
||||||
con.medium.reader
|
con.medium.reader
|
||||||
de.apkgrabber
|
de.apkgrabber
|
||||||
de.robv.android.xposed.installer
|
de.robv.android.xposed.installer
|
||||||
@@ -221,7 +210,6 @@ jp.bokete.app.android
|
|||||||
jp.naver.line.android
|
jp.naver.line.android
|
||||||
jp.pxv.android
|
jp.pxv.android
|
||||||
luo.speedometergpspro
|
luo.speedometergpspro
|
||||||
m.cna.com.tw.App
|
|
||||||
mark.via.gp
|
mark.via.gp
|
||||||
me.tshine.easymark
|
me.tshine.easymark
|
||||||
net.teeha.android.url_shortener
|
net.teeha.android.url_shortener
|
||||||
@@ -238,7 +226,6 @@ org.mozilla.firefox_beta
|
|||||||
org.mozilla.focus
|
org.mozilla.focus
|
||||||
org.schabi.newpipe
|
org.schabi.newpipe
|
||||||
org.telegram.messenger
|
org.telegram.messenger
|
||||||
org.telegram.messenger.web
|
|
||||||
org.telegram.multi
|
org.telegram.multi
|
||||||
org.telegram.plus
|
org.telegram.plus
|
||||||
org.thunderdog.challegram
|
org.thunderdog.challegram
|
||||||
@@ -252,162 +239,3 @@ tw.com.gamer.android.activecenter
|
|||||||
videodownloader.downloadvideo.downloader
|
videodownloader.downloadvideo.downloader
|
||||||
uk.co.bbc.learningenglish
|
uk.co.bbc.learningenglish
|
||||||
com.ted.android
|
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
|
|
||||||
@@ -33,6 +33,14 @@
|
|||||||
"tls"
|
"tls"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "http",
|
||||||
|
"port": 10809,
|
||||||
|
"protocol": "http",
|
||||||
|
"settings": {
|
||||||
|
"userLevel": 8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outbounds": [{
|
"outbounds": [{
|
||||||
@@ -46,7 +54,7 @@
|
|||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||||
"alterId": 0,
|
"alterId": 64,
|
||||||
"security": "auto",
|
"security": "auto",
|
||||||
"level": 8
|
"level": 8
|
||||||
}
|
}
|
||||||
@@ -73,9 +81,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"protocol": "freedom",
|
"protocol": "freedom",
|
||||||
"settings": {
|
"settings": {},
|
||||||
"domainStrategy": "UseIP"
|
|
||||||
},
|
|
||||||
"tag": "direct"
|
"tag": "direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -89,7 +95,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"routing": {
|
"routing": {
|
||||||
"domainStrategy": "AsIs",
|
"domainStrategy": "IPIfNonMatch",
|
||||||
"rules": []
|
"rules": []
|
||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
{
|
|
||||||
"stats":{},
|
|
||||||
"log": {
|
|
||||||
"loglevel": "warning"
|
|
||||||
},
|
|
||||||
"policy":{
|
|
||||||
"levels": {
|
|
||||||
"8": {
|
|
||||||
"handshake": 4,
|
|
||||||
"connIdle": 300,
|
|
||||||
"uplinkOnly": 1,
|
|
||||||
"downlinkOnly": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"system": {
|
|
||||||
"statsOutboundUplink": true,
|
|
||||||
"statsOutboundDownlink": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"inbounds": [{
|
|
||||||
"tag": "socks",
|
|
||||||
"port": 10808,
|
|
||||||
"protocol": "socks",
|
|
||||||
"settings": {
|
|
||||||
"auth": "noauth",
|
|
||||||
"udp": true,
|
|
||||||
"userLevel": 8
|
|
||||||
},
|
|
||||||
"sniffing": {
|
|
||||||
"enabled": true,
|
|
||||||
"destOverride": [
|
|
||||||
"http",
|
|
||||||
"tls"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tag": "tun",
|
|
||||||
"protocol": "tun",
|
|
||||||
"settings": {
|
|
||||||
"name": "xray0",
|
|
||||||
"MTU": 1500,
|
|
||||||
"userLevel": 8
|
|
||||||
},
|
|
||||||
"sniffing": {
|
|
||||||
"enabled": true,
|
|
||||||
"destOverride": [
|
|
||||||
"http",
|
|
||||||
"tls"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outbounds": [{
|
|
||||||
"tag": "proxy",
|
|
||||||
"protocol": "vmess",
|
|
||||||
"settings": {
|
|
||||||
"vnext": [
|
|
||||||
{
|
|
||||||
"address": "v2ray.cool",
|
|
||||||
"port": 10086,
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
|
||||||
"alterId": 0,
|
|
||||||
"security": "auto",
|
|
||||||
"level": 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"address": "v2ray.cool",
|
|
||||||
"method": "chacha20",
|
|
||||||
"ota": false,
|
|
||||||
"password": "123456",
|
|
||||||
"port": 10086,
|
|
||||||
"level": 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"streamSettings": {
|
|
||||||
"network": "tcp"
|
|
||||||
},
|
|
||||||
"mux": {
|
|
||||||
"enabled": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"protocol": "freedom",
|
|
||||||
"settings": {
|
|
||||||
"domainStrategy": "UseIP"
|
|
||||||
},
|
|
||||||
"tag": "direct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"protocol": "blackhole",
|
|
||||||
"tag": "block",
|
|
||||||
"settings": {
|
|
||||||
"response": {
|
|
||||||
"type": "http"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"routing": {
|
|
||||||
"domainStrategy": "AsIs",
|
|
||||||
"rules": []
|
|
||||||
},
|
|
||||||
"dns": {
|
|
||||||
"hosts": {},
|
|
||||||
"servers": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 28 KiB |
@@ -1,48 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
||||||
// Initialize WorkManager with the custom configuration
|
|
||||||
WorkManager.initialize(this, workManagerConfiguration)
|
|
||||||
|
|
||||||
// Ensure critical preference defaults are present in MMKV early
|
|
||||||
SettingsManager.initApp(this)
|
|
||||||
SettingsManager.setNightMode()
|
|
||||||
|
|
||||||
es.dmoral.toasty.Toasty.Config.getInstance()
|
|
||||||
.setGravity(android.view.Gravity.BOTTOM, 0, 300)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
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 WEBDAV_BACKUP_DIR = "backups"
|
|
||||||
const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip"
|
|
||||||
|
|
||||||
/** Legacy configuration keys. */
|
|
||||||
const val ANG_CONFIG = "ang_config"
|
|
||||||
|
|
||||||
// Default subscription ID for ungrouped servers
|
|
||||||
const val DEFAULT_SUBSCRIPTION_ID = "__default_subscription__"
|
|
||||||
|
|
||||||
/** 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_VPN_MTU = "pref_vpn_mtu"
|
|
||||||
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_UPDATE_TASK_NAME = "subscription_updater"
|
|
||||||
const val SUBSCRIPTION_MIN_INTERVAL_MINUTES = 15L
|
|
||||||
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_GROUP_ALL_DISPLAY = "pref_group_all_display"
|
|
||||||
const val PREF_LANGUAGE = "pref_language"
|
|
||||||
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
|
||||||
const val PREF_IPV6_ENABLED = "pref_ipv6_enabled"
|
|
||||||
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_ENABLE_LOCAL_PROXY = "pref_enable_local_proxy"
|
|
||||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
|
||||||
const val PREF_DYNAMIC_SOCKS_PORT = "pref_dynamic_socks_port"
|
|
||||||
const val PREF_SOCKS_USERNAME = "pref_socks_username"
|
|
||||||
const val PREF_SOCKS_PASSWORD = "pref_socks_password"
|
|
||||||
const val PREF_SOCKS_ENABLE_UDP = "pref_socks_enable_udp"
|
|
||||||
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_IP_API_URL = "pref_ip_api_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"
|
|
||||||
const val PREF_USE_HEV_TUNNEL = "pref_use_hev_tunnel_v2"
|
|
||||||
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
|
|
||||||
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
|
||||||
const val PREF_AUTO_REMOVE_INVALID_AFTER_TEST = "pref_auto_remove_invalid_after_test"
|
|
||||||
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
|
||||||
const val PREF_REAL_PING_CONCURRENCY = "pref_real_ping_concurrency"
|
|
||||||
|
|
||||||
/** Cache keys. */
|
|
||||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
|
||||||
|
|
||||||
/** Protocol identifiers. */
|
|
||||||
const val PROTOCOL_FREEDOM = "freedom"
|
|
||||||
|
|
||||||
/** Broadcast actions. */
|
|
||||||
const val BROADCAST_ACTION_SERVICE = "$ANG_PACKAGE.action.service"
|
|
||||||
const val BROADCAST_ACTION_ACTIVITY = "$ANG_PACKAGE.action.activity"
|
|
||||||
const val BROADCAST_ACTION_WIDGET_CLICK = "$ANG_PACKAGE.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"
|
|
||||||
const val TAG_DNS = "dns-module"
|
|
||||||
const val TAG_DOMESTIC_DNS = "domestic-dns"
|
|
||||||
const val TAG_BALANCER = "proxy-round"
|
|
||||||
|
|
||||||
/** 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"
|
|
||||||
const val IP_API_URL = "https://api.ip.sb/geoip"
|
|
||||||
|
|
||||||
/** 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"
|
|
||||||
|
|
||||||
/** Geo data file names. */
|
|
||||||
const val GEOSITE_DAT = "geosite.dat"
|
|
||||||
const val GEOIP_DAT = "geoip.dat"
|
|
||||||
const val GEOIP_ONLY_CN_PRIVATE_DAT = "geoip-only-cn-private.dat"
|
|
||||||
const val GEOIP_ONLY_CN_PRIVATE_URL = "$GITHUB_RAW_URL/Loyalsoldier/geoip/release/$GEOIP_ONLY_CN_PRIVATE_DAT"
|
|
||||||
|
|
||||||
/** 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_START = 7
|
|
||||||
const val MSG_MEASURE_CONFIG_CANCEL = 71
|
|
||||||
const val MSG_MEASURE_CONFIG_SUCCESS = 72
|
|
||||||
const val MSG_MEASURE_CONFIG_NOTIFY = 73
|
|
||||||
const val MSG_MEASURE_CONFIG_FINISH = 74
|
|
||||||
|
|
||||||
/** 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"
|
|
||||||
|
|
||||||
/** 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 HYSTERIA = "hysteria://"
|
|
||||||
const val HYSTERIA2 = "hysteria2://"
|
|
||||||
const val HY2 = "hy2://"
|
|
||||||
|
|
||||||
/** Give a good name to this, IDK*/
|
|
||||||
const val VPN = "VPN"
|
|
||||||
const val VPN_MTU = 1500
|
|
||||||
|
|
||||||
/** hev-sock5-tunnel read-write-timeout value */
|
|
||||||
const val HEVTUN_RW_TIMEOUT = "300,60"
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
|
|
||||||
const val UNIDENTIFIED_PACKAGE = "__unknown_app__"
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
val BUILTIN_OUTBOUND_TAGS = setOf(
|
|
||||||
TAG_PROXY,
|
|
||||||
TAG_DIRECT,
|
|
||||||
TAG_BLOCKED,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.v2ray.ang.contracts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A common Adapter -> host callback interface that includes common actions: edit, remove and refresh.
|
|
||||||
* Extend this interface or define more specific interfaces for different adapters as needed.
|
|
||||||
*/
|
|
||||||
interface BaseAdapterListener {
|
|
||||||
/**
|
|
||||||
* Request the host to edit the specified item.
|
|
||||||
* @param guid Unique identifier (GUID) of the item
|
|
||||||
* @param position Current position in the adapter (optional; host should validate it)
|
|
||||||
*/
|
|
||||||
fun onEdit(guid: String, position: Int)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the host to remove the specified item. Position is provided for optional animation or validation.
|
|
||||||
* @param guid Unique identifier (GUID) of the item
|
|
||||||
* @param position Current position in the adapter (optional; host should validate it)
|
|
||||||
*/
|
|
||||||
fun onRemove(guid: String, position: Int)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the host to share the specified URL.
|
|
||||||
* @param url The URL to be shared
|
|
||||||
*/
|
|
||||||
fun onShare(url: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the host to refresh data (for example, reload from the ViewModel or call notifyDataSetChanged).
|
|
||||||
*/
|
|
||||||
fun onRefreshData()
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.v2ray.ang.contracts
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
|
|
||||||
interface MainAdapterListener : BaseAdapterListener {
|
|
||||||
|
|
||||||
fun onEdit(guid: String, position: Int, profile: ProfileItem)
|
|
||||||
|
|
||||||
fun onSelectServer(guid: String)
|
|
||||||
|
|
||||||
fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.v2ray.ang.contracts
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
|
|
||||||
interface ServiceControl {
|
|
||||||
/**
|
|
||||||
* Gets the service instance.
|
|
||||||
* @return The service instance.
|
|
||||||
*/
|
|
||||||
fun getService(): Service
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the service.
|
|
||||||
*/
|
|
||||||
fun startService()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the service.
|
|
||||||
*/
|
|
||||||
fun stopService()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Protects the VPN socket.
|
|
||||||
* @param socket The socket to protect.
|
|
||||||
* @return True if the socket is protected, false otherwise.
|
|
||||||
*/
|
|
||||||
fun vpnProtect(socket: Int): Boolean
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.v2ray.ang.contracts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface that defines the control operations for tun2socks implementations.
|
|
||||||
*
|
|
||||||
* This interface is implemented by different tunnel solutions like:
|
|
||||||
*/
|
|
||||||
interface Tun2SocksControl {
|
|
||||||
/**
|
|
||||||
* Starts the tun2socks process with the appropriate parameters.
|
|
||||||
* This initializes the VPN tunnel and connects it to the SOCKS proxy.
|
|
||||||
*/
|
|
||||||
fun startTun2Socks()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the tun2socks process and cleans up resources.
|
|
||||||
*/
|
|
||||||
fun stopTun2Socks()
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
package com.v2ray.ang.core
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.CoreConfigContext
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.CoreResolvedType
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.util.LogUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds [com.v2ray.ang.dto.CoreConfigContext] from the selected profile.
|
|
||||||
* Keeps parsing and resolution logic out of [CoreConfigManager].
|
|
||||||
*/
|
|
||||||
object CoreConfigContextBuilder {
|
|
||||||
|
|
||||||
/** Loads profile by guid and returns a resolved runtime context. */
|
|
||||||
fun build(context: Context, guid: String): CoreConfigContext? {
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return null
|
|
||||||
if (config.configType == EConfigType.CUSTOM) {
|
|
||||||
return CoreConfigContext(
|
|
||||||
context = context,
|
|
||||||
guid = guid,
|
|
||||||
selectedProfile = config,
|
|
||||||
resolvedProfiles = listOf(config),
|
|
||||||
resolvedType = CoreResolvedType.CUSTOM,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-resolve custom outbound profiles from routing rulesets
|
|
||||||
val customOutbounds = resolveCustomOutbounds()
|
|
||||||
|
|
||||||
// Determine resolved profiles and type based on config type
|
|
||||||
val (resolvedProfiles, resolvedType) = when (config.configType) {
|
|
||||||
EConfigType.POLICYGROUP -> {
|
|
||||||
val profiles = resolvePolicyGroupProfiles(config)
|
|
||||||
Pair(profiles, CoreResolvedType.POLICYGROUP)
|
|
||||||
}
|
|
||||||
|
|
||||||
EConfigType.PROXYCHAIN -> {
|
|
||||||
val profiles = resolveProxyChainProfiles(config)
|
|
||||||
Pair(profiles, CoreResolvedType.PROXYCHAIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val chainProfiles = resolveProxyChainProfilesFromGroup(config)
|
|
||||||
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
|
|
||||||
Pair(chainProfiles, type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create context with common fields
|
|
||||||
return CoreConfigContext(
|
|
||||||
context = context,
|
|
||||||
guid = guid,
|
|
||||||
selectedProfile = config,
|
|
||||||
resolvedProfiles = resolvedProfiles,
|
|
||||||
resolvedType = resolvedType,
|
|
||||||
customOutboundProfiles = customOutbounds,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolves policy-group members with the same filters as runtime build. */
|
|
||||||
private fun resolvePolicyGroupProfiles(config: ProfileItem): List<ProfileItem> {
|
|
||||||
try {
|
|
||||||
val serverList = MmkvManager.decodeAllServerList()
|
|
||||||
return serverList
|
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { id -> MmkvManager.decodeServerConfig(id) }
|
|
||||||
.filter { profile ->
|
|
||||||
val subscriptionId = config.policyGroupSubscriptionId
|
|
||||||
if (subscriptionId.isNullOrBlank()) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
profile.subscriptionId == subscriptionId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter { profile ->
|
|
||||||
val filter = config.policyGroupFilter
|
|
||||||
if (filter.isNullOrBlank()) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
Regex(filter).containsMatchIn(profile.remarks)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
profile.remarks.contains(filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter { it.server.isNotNullEmpty() }
|
|
||||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
|
||||||
.filter { it.configType != EConfigType.CUSTOM }
|
|
||||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
|
||||||
.filter { it.configType != EConfigType.PROXYCHAIN }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for config '${config.remarks}'", e)
|
|
||||||
return listOf(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolves proxy-chain members with the same filters as runtime build. */
|
|
||||||
private fun resolveProxyChainProfiles(config: ProfileItem): List<ProfileItem> {
|
|
||||||
if (config.proxyChainProfiles.isNullOrBlank()) {
|
|
||||||
return listOf(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return config.proxyChainProfiles.orEmpty().split(",")
|
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
|
|
||||||
.filter { it.server.isNotNullEmpty() }
|
|
||||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
|
||||||
.filter { it.configType != EConfigType.CUSTOM }
|
|
||||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
|
||||||
.filter { it.configType != EConfigType.PROXYCHAIN }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles for config '${config.remarks}'", e)
|
|
||||||
return listOf(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves chain nodes in fixed order: next -> current -> prev.
|
|
||||||
* If chain cannot be built, caller treats result as normal mode.
|
|
||||||
*/
|
|
||||||
private fun resolveProxyChainProfilesFromGroup(config: ProfileItem): List<ProfileItem> {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
|
|
||||||
return listOf(config)
|
|
||||||
}
|
|
||||||
if (config.subscriptionId.isEmpty()) {
|
|
||||||
return listOf(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val subItem = MmkvManager.decodeSubscription(config.subscriptionId) ?: return listOf(config)
|
|
||||||
val resolved = mutableListOf<ProfileItem>()
|
|
||||||
|
|
||||||
// Keep the same practical chain order as current runtime assembly:
|
|
||||||
// next -> current -> prev
|
|
||||||
SettingsManager.getServerViaRemarks(subItem.nextProfile)?.let { resolved.add(it) }
|
|
||||||
resolved.add(config)
|
|
||||||
SettingsManager.getServerViaRemarks(subItem.prevProfile)?.let { resolved.add(it) }
|
|
||||||
|
|
||||||
return resolved
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles from group for config '${config.remarks}'", e)
|
|
||||||
return listOf(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves custom outbound profiles from routing rulesets.
|
|
||||||
* Scans rulesets for non-builtin outbound tags and looks up matching profiles by remarks.
|
|
||||||
* Returns a map of tag -> ProfileItem for independent processing.
|
|
||||||
*/
|
|
||||||
private fun resolveCustomOutbounds(): Map<String, ProfileItem> {
|
|
||||||
val customMap = mutableMapOf<String, ProfileItem>()
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return customMap
|
|
||||||
|
|
||||||
try {
|
|
||||||
val processedTags = mutableSetOf<String>()
|
|
||||||
|
|
||||||
rulesetItems
|
|
||||||
.filter { it.enabled }
|
|
||||||
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
|
|
||||||
.filter { tag -> tag !in AppConfig.BUILTIN_OUTBOUND_TAGS }
|
|
||||||
.distinct()
|
|
||||||
.forEach { tag ->
|
|
||||||
if (tag in processedTags) return@forEach
|
|
||||||
processedTags.add(tag)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
customMap[tag] = profile
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to resolve custom outbound for tag '$tag', skipping", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to resolve custom outbound profiles", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return customMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,991 +0,0 @@
|
|||||||
package com.v2ray.ang.core
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ConfigResult
|
|
||||||
import com.v2ray.ang.dto.CoreConfigContext
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.enums.CoreResolvedType
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.LogUtil
|
|
||||||
import com.v2ray.ang.util.PackageUidResolver
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
object CoreConfigManager {
|
|
||||||
private var initConfigCache: String? = null
|
|
||||||
private var initConfigCacheWithTun: String? = null
|
|
||||||
|
|
||||||
//region get config function
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds normal runtime config JSON for the selected profile.
|
|
||||||
*
|
|
||||||
* @param context The context of the caller.
|
|
||||||
* @param guid The unique identifier for the V2ray configuration.
|
|
||||||
* @return A ConfigResult object containing the configuration details or indicating failure.
|
|
||||||
*/
|
|
||||||
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
|
|
||||||
try {
|
|
||||||
val configContext = CoreConfigContextBuilder.build(context, guid) ?: return ConfigResult(false)
|
|
||||||
if (configContext.resolvedType == CoreResolvedType.CUSTOM) {
|
|
||||||
return getV2rayCustomConfig(configContext)
|
|
||||||
}
|
|
||||||
val v2rayConfig = when (configContext.resolvedType) {
|
|
||||||
CoreResolvedType.POLICYGROUP -> buildGroupConfig(configContext)
|
|
||||||
CoreResolvedType.PROXYCHAIN -> buildProxyChainConfig(configContext)
|
|
||||||
CoreResolvedType.NORMAL -> buildNormalConfig(configContext)
|
|
||||||
CoreResolvedType.CUSTOM -> null
|
|
||||||
} ?: return ConfigResult(false)
|
|
||||||
|
|
||||||
return toConfigResult(configContext, v2rayConfig)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to get V2ray config", e)
|
|
||||||
return ConfigResult(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds speedtest config for the selected profile.
|
|
||||||
*
|
|
||||||
* It reuses the same build flow as normal config, then removes
|
|
||||||
* unnecessary sections for delay testing.
|
|
||||||
*
|
|
||||||
* @param context The context of the caller.
|
|
||||||
* @param guid The unique identifier for the V2ray configuration.
|
|
||||||
* @return A ConfigResult object containing the configuration details or indicating failure.
|
|
||||||
*/
|
|
||||||
fun getV2rayConfig4Speedtest(context: Context, guid: String): ConfigResult {
|
|
||||||
try {
|
|
||||||
val configContext = CoreConfigContextBuilder.build(context, guid) ?: return ConfigResult(false)
|
|
||||||
if (configContext.resolvedType == CoreResolvedType.CUSTOM) {
|
|
||||||
return getV2rayCustomConfig(configContext)
|
|
||||||
}
|
|
||||||
val v2rayConfig = when (configContext.resolvedType) {
|
|
||||||
CoreResolvedType.POLICYGROUP -> buildGroupConfig(configContext)
|
|
||||||
CoreResolvedType.PROXYCHAIN -> buildProxyChainConfig(configContext)
|
|
||||||
CoreResolvedType.NORMAL -> buildNormalConfig(configContext)
|
|
||||||
CoreResolvedType.CUSTOM -> null
|
|
||||||
} ?: return ConfigResult(false)
|
|
||||||
|
|
||||||
postProcessForSpeedtest(v2rayConfig)
|
|
||||||
|
|
||||||
return toConfigResult(configContext, v2rayConfig)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e)
|
|
||||||
return ConfigResult(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds config result for CUSTOM profiles.
|
|
||||||
*/
|
|
||||||
private fun getV2rayCustomConfig(configContext: CoreConfigContext): ConfigResult {
|
|
||||||
val context = configContext.context
|
|
||||||
val raw = MmkvManager.decodeServerRaw(configContext.guid) ?: return ConfigResult(false)
|
|
||||||
val result = ConfigResult(true, configContext.guid, raw)
|
|
||||||
if (!needTun()) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
val json = JsonUtil.parseString(raw)?.takeIf { it.isJsonObject }?.asJsonObject ?: return result
|
|
||||||
|
|
||||||
// Check whether package names need to be replaced with UIDs
|
|
||||||
if (SettingsManager.canUseProcessRouting()) {
|
|
||||||
val rulesJson = json.get("routing")?.takeIf { it.isJsonObject }?.asJsonObject
|
|
||||||
?.get("rules")?.takeIf { it.isJsonArray }?.asJsonArray
|
|
||||||
?: JsonArray()
|
|
||||||
|
|
||||||
for (elem in rulesJson) {
|
|
||||||
val rule = elem.takeIf { it.isJsonObject }?.asJsonObject ?: continue
|
|
||||||
val process = rule.get("process")?.takeIf { it.isJsonArray }?.asJsonArray ?: continue
|
|
||||||
val packages = process.mapNotNull {
|
|
||||||
it.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }?.asString
|
|
||||||
}.takeIf { it.isNotEmpty() } ?: continue
|
|
||||||
val uids = PackageUidResolver.packageNamesToUids(context, packages).takeIf { it.isNotEmpty() } ?: continue
|
|
||||||
|
|
||||||
rule.add("process", JsonArray().apply { uids.forEach { add(it) } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if tun inbound exists
|
|
||||||
val inboundsJson = json.get("inbounds")?.takeIf { it.isJsonArray }?.asJsonArray
|
|
||||||
?: JsonArray().also { json.add("inbounds", it) }
|
|
||||||
val tunNotExists = inboundsJson.none { elem ->
|
|
||||||
elem.isJsonObject && elem.asJsonObject.get("protocol")
|
|
||||||
?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }
|
|
||||||
?.asString == "tun"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tunNotExists) {
|
|
||||||
// add tun inbound from template
|
|
||||||
initV2rayConfig(configContext)?.let { templateConfig ->
|
|
||||||
templateConfig.inbounds.firstOrNull { it.tag == "tun" }?.let { inboundTun ->
|
|
||||||
inboundTun.settings?.mtu = SettingsManager.getVpnMtu()
|
|
||||||
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonUtil.toJsonPretty(json)?.let { ConfigResult(true, configContext.guid, it) } ?: result
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Builds full config for policy-group mode. */
|
|
||||||
private fun buildGroupConfig(configContext: CoreConfigContext): V2rayConfig? {
|
|
||||||
val config = configContext.selectedProfile
|
|
||||||
val validConfigs = configContext.resolvedProfiles
|
|
||||||
|
|
||||||
if (validConfigs.isEmpty()) {
|
|
||||||
LogUtil.w(AppConfig.TAG, "All configs are invalid")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val v2rayConfig = initV2rayConfig(configContext) ?: return null
|
|
||||||
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
|
||||||
v2rayConfig.remarks = config.remarks
|
|
||||||
|
|
||||||
getInbounds(v2rayConfig)
|
|
||||||
|
|
||||||
v2rayConfig.outbounds.removeAt(0)
|
|
||||||
val outboundsList = mutableListOf<V2rayConfig.OutboundBean>()
|
|
||||||
var index = 0
|
|
||||||
for (item in validConfigs) {
|
|
||||||
index++
|
|
||||||
val outbound = convertProfile2Outbound(item) ?: continue
|
|
||||||
outbound.tag = "proxy-$index-${item.remarks.trim()}"
|
|
||||||
outboundsList.add(outbound)
|
|
||||||
}
|
|
||||||
outboundsList.addAll(v2rayConfig.outbounds)
|
|
||||||
v2rayConfig.outbounds = ArrayList(outboundsList)
|
|
||||||
|
|
||||||
getRouting(configContext, v2rayConfig)
|
|
||||||
getFakeDns(v2rayConfig)
|
|
||||||
getDns(v2rayConfig)
|
|
||||||
getBalance(configContext, v2rayConfig)
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
|
|
||||||
getCustomLocalDns(v2rayConfig)
|
|
||||||
}
|
|
||||||
if (!MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED)) {
|
|
||||||
v2rayConfig.stats = null
|
|
||||||
v2rayConfig.policy = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve and add to DNS Hosts
|
|
||||||
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") {
|
|
||||||
resolveOutboundDomainsToHosts(v2rayConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v2rayConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds full V2Ray config for proxy-chain mode.
|
|
||||||
*
|
|
||||||
* Uses resolvedProfiles as an ordered chain and links each hop
|
|
||||||
* with dialerProxy.
|
|
||||||
*/
|
|
||||||
private fun buildProxyChainConfig(configContext: CoreConfigContext): V2rayConfig? {
|
|
||||||
val config = configContext.selectedProfile
|
|
||||||
val resolvedProfiles = configContext.resolvedProfiles
|
|
||||||
|
|
||||||
val v2rayConfig = initV2rayConfig(configContext) ?: return null
|
|
||||||
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
|
||||||
v2rayConfig.remarks = config.remarks
|
|
||||||
|
|
||||||
getInbounds(v2rayConfig)
|
|
||||||
|
|
||||||
// Build and link the whole chain directly from resolvedProfiles.
|
|
||||||
val chainOutbounds = resolvedProfiles.mapNotNull { profile ->
|
|
||||||
convertProfile2Outbound(profile)
|
|
||||||
}.toMutableList()
|
|
||||||
if (chainOutbounds.size < 2) {
|
|
||||||
LogUtil.w(AppConfig.TAG, "Proxy chain requires at least 2 valid profiles, but only ${chainOutbounds.size} found")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
chainOutbounds.forEachIndexed { index, outbound ->
|
|
||||||
outbound.tag = if (index == 0) AppConfig.TAG_PROXY else AppConfig.TAG_PROXY + index
|
|
||||||
}
|
|
||||||
for (index in 0 until chainOutbounds.size - 1) {
|
|
||||||
chainOutbounds[index].ensureSockopt().dialerProxy = chainOutbounds[index + 1].tag
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep built-in outbounds and place the chain before them.
|
|
||||||
val builtinOutbounds = if (v2rayConfig.outbounds.size > 1) {
|
|
||||||
v2rayConfig.outbounds.drop(1)
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
v2rayConfig.outbounds = ArrayList(chainOutbounds + builtinOutbounds)
|
|
||||||
|
|
||||||
getRouting(configContext, v2rayConfig)
|
|
||||||
getFakeDns(v2rayConfig)
|
|
||||||
getDns(v2rayConfig)
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
|
||||||
getCustomLocalDns(v2rayConfig)
|
|
||||||
}
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
|
|
||||||
v2rayConfig.stats = null
|
|
||||||
v2rayConfig.policy = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve and add to DNS Hosts
|
|
||||||
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") {
|
|
||||||
resolveOutboundDomainsToHosts(v2rayConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v2rayConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Builds full config for normal single-node mode. */
|
|
||||||
private fun buildNormalConfig(configContext: CoreConfigContext): V2rayConfig? {
|
|
||||||
val config = configContext.selectedProfile
|
|
||||||
|
|
||||||
val address = config.server ?: return null
|
|
||||||
if (!Utils.isPureIpAddress(address) && !Utils.isValidUrl(address)) {
|
|
||||||
LogUtil.w(AppConfig.TAG, "$address is an invalid ip or domain")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val v2rayConfig = initV2rayConfig(configContext) ?: return null
|
|
||||||
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
|
||||||
v2rayConfig.remarks = config.remarks
|
|
||||||
|
|
||||||
getInbounds(v2rayConfig)
|
|
||||||
getOutbounds(configContext, v2rayConfig) ?: return null
|
|
||||||
getRouting(configContext, v2rayConfig)
|
|
||||||
getFakeDns(v2rayConfig)
|
|
||||||
getDns(v2rayConfig)
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
|
||||||
getCustomLocalDns(v2rayConfig)
|
|
||||||
}
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
|
|
||||||
v2rayConfig.stats = null
|
|
||||||
v2rayConfig.policy = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve and add to DNS Hosts
|
|
||||||
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") {
|
|
||||||
resolveOutboundDomainsToHosts(v2rayConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v2rayConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes non-essential sections for speedtest use.
|
|
||||||
*/
|
|
||||||
private fun postProcessForSpeedtest(v2rayConfig: V2rayConfig) {
|
|
||||||
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
|
||||||
v2rayConfig.inbounds.clear()
|
|
||||||
v2rayConfig.routing.rules.clear()
|
|
||||||
v2rayConfig.dns = null
|
|
||||||
v2rayConfig.fakedns = null
|
|
||||||
v2rayConfig.stats = null
|
|
||||||
v2rayConfig.policy = null
|
|
||||||
v2rayConfig.outbounds.forEach { key -> key.mux = null }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Converts a built config object into a unified result payload. */
|
|
||||||
private fun toConfigResult(configContext: CoreConfigContext, v2rayConfig: V2rayConfig): ConfigResult {
|
|
||||||
return ConfigResult(
|
|
||||||
status = true,
|
|
||||||
guid = configContext.guid,
|
|
||||||
content = JsonUtil.toJsonPretty(v2rayConfig) ?: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes V2ray configuration.
|
|
||||||
*
|
|
||||||
* This function loads the V2ray configuration from assets or from a cached value.
|
|
||||||
* It first attempts to use the cached configuration if available, otherwise reads
|
|
||||||
* the configuration from the "v2ray_config.json" asset file.
|
|
||||||
*
|
|
||||||
* @param configContext Runtime context used to access app assets and profile data
|
|
||||||
* @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty
|
|
||||||
*/
|
|
||||||
private fun initV2rayConfig(configContext: CoreConfigContext): V2rayConfig? {
|
|
||||||
val context = configContext.context
|
|
||||||
var assets = ""
|
|
||||||
if (needTun()) {
|
|
||||||
assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json")
|
|
||||||
if (TextUtils.isEmpty(assets)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
initConfigCacheWithTun = assets
|
|
||||||
} else {
|
|
||||||
assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
|
||||||
if (TextUtils.isEmpty(assets)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
initConfigCache = assets
|
|
||||||
}
|
|
||||||
val config = JsonUtil.fromJson(assets, V2rayConfig::class.java)
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
|
|
||||||
//region some sub function
|
|
||||||
|
|
||||||
private fun needTun(): Boolean {
|
|
||||||
return SettingsManager.isVpnMode() && !SettingsManager.isUsingHevTun()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the inbound settings for V2ray.
|
|
||||||
*
|
|
||||||
* This function sets up the listening ports, sniffing options, and other inbound-related configurations.
|
|
||||||
*
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
* @return true if inbound configuration was successful, false otherwise
|
|
||||||
*/
|
|
||||||
private fun getInbounds(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
val vpn = SettingsManager.isVpnMode()
|
|
||||||
val useHev = SettingsManager.isUsingHevTun()
|
|
||||||
val forcedByHev = vpn && useHev
|
|
||||||
|
|
||||||
val enableLocalProxy = forcedByHev || MmkvManager.decodeSettingsBool(AppConfig.PREF_ENABLE_LOCAL_PROXY, true)
|
|
||||||
|
|
||||||
val socksPort = SettingsManager.getSocksPort()
|
|
||||||
val socksUsername = SettingsManager.getSocksUsername()
|
|
||||||
val socksPassword = SettingsManager.getSocksPassword()
|
|
||||||
val inbound1 = v2rayConfig.inbounds[0]
|
|
||||||
if (inbound1.settings == null) {
|
|
||||||
inbound1.settings = V2rayConfig.InboundBean.InSettingsBean()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
|
||||||
inbound1.listen = AppConfig.LOOPBACK
|
|
||||||
}
|
|
||||||
inbound1.port = socksPort
|
|
||||||
inbound1.settings?.udp = MmkvManager.decodeSettingsBool(AppConfig.PREF_SOCKS_ENABLE_UDP, true)
|
|
||||||
if (socksUsername != null && socksPassword != null) {
|
|
||||||
inbound1.settings?.auth = "password"
|
|
||||||
inbound1.settings?.accounts = listOf(
|
|
||||||
V2rayConfig.InboundBean.InSettingsBean.SocksAccountBean(
|
|
||||||
user = socksUsername,
|
|
||||||
pass = socksPassword
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
inbound1.settings?.auth = "noauth"
|
|
||||||
inbound1.settings?.accounts = null
|
|
||||||
}
|
|
||||||
val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
|
||||||
val sniffAllTlsAndHttp =
|
|
||||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
|
|
||||||
inbound1.sniffing?.enabled = fakedns || sniffAllTlsAndHttp
|
|
||||||
inbound1.sniffing?.routeOnly =
|
|
||||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
|
|
||||||
if (!sniffAllTlsAndHttp) {
|
|
||||||
inbound1.sniffing?.destOverride?.clear()
|
|
||||||
}
|
|
||||||
if (fakedns) {
|
|
||||||
inbound1.sniffing?.destOverride?.add("fakedns")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.isXray()) {
|
|
||||||
val inbound2 = JsonUtil.fromJson(JsonUtil.toJson(inbound1), V2rayConfig.InboundBean::class.java) ?: return false
|
|
||||||
inbound2.tag = EConfigType.HTTP.name.lowercase()
|
|
||||||
inbound2.port = SettingsManager.getHttpPort()
|
|
||||||
inbound2.protocol = EConfigType.HTTP.name.lowercase()
|
|
||||||
inbound2.settings?.auth = null
|
|
||||||
inbound2.settings?.udp = null
|
|
||||||
v2rayConfig.inbounds.add(inbound2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableLocalProxy) {
|
|
||||||
v2rayConfig.inbounds.removeIf { it.protocol == "socks" || it.protocol == "http" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needTun()) {
|
|
||||||
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
|
|
||||||
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
|
|
||||||
inboundTun?.sniffing = inbound1.sniffing
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to configure inbounds", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the fake DNS settings if enabled.
|
|
||||||
*
|
|
||||||
* Adds FakeDNS configuration to v2rayConfig if both local DNS and fake DNS are enabled.
|
|
||||||
*
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
*/
|
|
||||||
private fun getFakeDns(v2rayConfig: V2rayConfig) {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
|
||||||
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
|
||||||
) {
|
|
||||||
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injects custom outbound profiles into the V2ray configuration.
|
|
||||||
* Uses pre-resolved profiles from the context instead of re-parsing.
|
|
||||||
*
|
|
||||||
* @param v2rayConfig The V2ray configuration to modify
|
|
||||||
* @param customOutbounds Pre-resolved custom outbound profiles (tag -> ProfileItem mapping)
|
|
||||||
*/
|
|
||||||
private fun injectCustomOutbounds(v2rayConfig: V2rayConfig, customOutbounds: Map<String, ProfileItem>) {
|
|
||||||
val existingTags = v2rayConfig.outbounds.mapTo(mutableSetOf()) { it.tag }
|
|
||||||
|
|
||||||
customOutbounds.forEach { (tag, profile) ->
|
|
||||||
if (tag in existingTags) return@forEach
|
|
||||||
try {
|
|
||||||
val outbound = convertProfile2Outbound(profile) ?: run {
|
|
||||||
LogUtil.w(AppConfig.TAG, "Could not convert profile '$tag' to outbound, skipping")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
outbound.tag = tag
|
|
||||||
v2rayConfig.outbounds.add(outbound)
|
|
||||||
existingTags.add(tag)
|
|
||||||
LogUtil.d(AppConfig.TAG, "Injected custom outbound: tag='$tag'")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to inject custom outbound for tag '$tag', skipping", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures routing settings for V2ray.
|
|
||||||
*
|
|
||||||
* Sets up the domain strategy, injects custom outbounds from rulesets, and adds routing rules.
|
|
||||||
*
|
|
||||||
* @param configContext Configuration context with custom outbound profiles
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
* @return true if routing configuration was successful, false otherwise
|
|
||||||
*/
|
|
||||||
private fun getRouting(configContext: CoreConfigContext, v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
|
|
||||||
v2rayConfig.routing.domainStrategy =
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
|
||||||
?: "AsIs"
|
|
||||||
|
|
||||||
// Inject custom outbound profiles from routing rulesets
|
|
||||||
injectCustomOutbounds(v2rayConfig, configContext.customOutboundProfiles)
|
|
||||||
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
|
||||||
rulesetItems?.forEach { key ->
|
|
||||||
getRoutingUserRule(configContext, key, v2rayConfig)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to configure routing", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a specific ruleset item to the routing configuration.
|
|
||||||
*
|
|
||||||
* @param configContext Runtime context used by routing helpers
|
|
||||||
* @param item The ruleset item to add
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
*/
|
|
||||||
private fun getRoutingUserRule(configContext: CoreConfigContext, item: RulesetItem?, v2rayConfig: V2rayConfig) {
|
|
||||||
val context = configContext.context
|
|
||||||
try {
|
|
||||||
if (item == null || !item.enabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), V2rayConfig.RoutingBean.RulesBean::class.java) ?: return
|
|
||||||
|
|
||||||
// Replace specific geoip rules with ext versions
|
|
||||||
rule.ip?.let { ipList ->
|
|
||||||
val updatedIpList = ArrayList<String>()
|
|
||||||
ipList.forEach { ip ->
|
|
||||||
when (ip) {
|
|
||||||
AppConfig.GEOIP_CN -> updatedIpList.add("ext:${AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT}:cn")
|
|
||||||
AppConfig.GEOIP_PRIVATE -> updatedIpList.add("ext:${AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT}:private")
|
|
||||||
else -> updatedIpList.add(ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rule.ip = updatedIpList
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsManager.canUseProcessRouting()) {
|
|
||||||
// Convert process package names to UIDs
|
|
||||||
rule.process?.let { processList ->
|
|
||||||
if (processList.isNotEmpty()) {
|
|
||||||
val uids = PackageUidResolver.packageNamesToUids(context, processList)
|
|
||||||
rule.process = uids.ifEmpty { null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rule.process = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the outbound tag is a custom one that failed to inject, fall back to proxy
|
|
||||||
val outboundTag = rule.outboundTag
|
|
||||||
if (!outboundTag.isNullOrBlank()
|
|
||||||
&& outboundTag !in AppConfig.BUILTIN_OUTBOUND_TAGS
|
|
||||||
&& v2rayConfig.outbounds.none { it.tag == outboundTag }
|
|
||||||
) {
|
|
||||||
LogUtil.w(AppConfig.TAG, "Outbound tag '$outboundTag' not found, falling back to '${AppConfig.TAG_PROXY}'")
|
|
||||||
rule.outboundTag = AppConfig.TAG_PROXY
|
|
||||||
}
|
|
||||||
|
|
||||||
v2rayConfig.routing.rules.add(rule)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to apply routing user rule", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves domain rules for a specific outbound tag.
|
|
||||||
*
|
|
||||||
* Searches through all rulesets to find domains targeting the specified tag.
|
|
||||||
*
|
|
||||||
* @param tag The outbound tag to search for
|
|
||||||
* @return ArrayList of domain rules matching the tag
|
|
||||||
*/
|
|
||||||
private fun getUserRule2Domain(tag: String): ArrayList<String> {
|
|
||||||
val domain = ArrayList<String>()
|
|
||||||
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
|
||||||
rulesetItems?.forEach { key ->
|
|
||||||
if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
|
|
||||||
key.domain?.forEach {
|
|
||||||
domain.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves domain rules for custom outbound tags.
|
|
||||||
*
|
|
||||||
* Searches through all rulesets to find domains targeting any custom outbound tags.
|
|
||||||
*
|
|
||||||
* @return ArrayList of domain rules matching custom outbound tags
|
|
||||||
*/
|
|
||||||
private fun getCustomOutboundUserRule2Domain(): ArrayList<String> {
|
|
||||||
val domain = ArrayList<String>()
|
|
||||||
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
|
||||||
rulesetItems?.forEach { key ->
|
|
||||||
if (key.enabled && !AppConfig.BUILTIN_OUTBOUND_TAGS.contains(key.outboundTag)
|
|
||||||
&& !key.domain.isNullOrEmpty()
|
|
||||||
) {
|
|
||||||
key.domain?.forEach {
|
|
||||||
domain.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures custom local DNS settings.
|
|
||||||
*
|
|
||||||
* Sets up DNS inbound, outbound, and routing rules for local DNS resolution.
|
|
||||||
*
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
* @return true if custom local DNS configuration was successful, false otherwise
|
|
||||||
*/
|
|
||||||
private fun getCustomLocalDns(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
|
||||||
val geositeCn = arrayListOf(AppConfig.GEOSITE_CN)
|
|
||||||
val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
|
|
||||||
val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
|
|
||||||
val finalDomain = geositeCn.plus(proxyDomain).plus(directDomain).distinct()
|
|
||||||
// fakedns with all domains to make it always top priority
|
|
||||||
v2rayConfig.dns?.servers?.add(
|
|
||||||
0,
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
address = "fakedns",
|
|
||||||
domains = finalDomain
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsManager.isVpnMode()) {
|
|
||||||
if (SettingsManager.isUsingHevTun()) {
|
|
||||||
//hev-socks5-tunnel dns routing
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
0, V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
inboundTag = arrayListOf("socks"),
|
|
||||||
outboundTag = "dns-out",
|
|
||||||
port = "53",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
0, V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
inboundTag = arrayListOf("tun"),
|
|
||||||
outboundTag = "dns-out",
|
|
||||||
port = "53",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS outbound
|
|
||||||
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
|
|
||||||
v2rayConfig.outbounds.add(
|
|
||||||
V2rayConfig.OutboundBean(
|
|
||||||
protocol = "dns",
|
|
||||||
tag = "dns-out",
|
|
||||||
settings = null,
|
|
||||||
streamSettings = null,
|
|
||||||
mux = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to configure custom local DNS", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the DNS settings for V2ray.
|
|
||||||
*
|
|
||||||
* Sets up DNS servers, hosts, and routing rules for DNS resolution.
|
|
||||||
*
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
* @return true if DNS configuration was successful, false otherwise
|
|
||||||
*/
|
|
||||||
private fun getDns(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
val hosts = mutableMapOf<String, Any>()
|
|
||||||
val servers = ArrayList<Any>()
|
|
||||||
|
|
||||||
//remote Dns
|
|
||||||
val remoteDns = SettingsManager.getRemoteDnsServers()
|
|
||||||
val proxyDomain = (getUserRule2Domain(AppConfig.TAG_PROXY) + getCustomOutboundUserRule2Domain()).distinct()
|
|
||||||
remoteDns.forEach {
|
|
||||||
servers.add(it)
|
|
||||||
}
|
|
||||||
if (proxyDomain.isNotEmpty()) {
|
|
||||||
servers.add(
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
address = remoteDns.first(),
|
|
||||||
domains = proxyDomain,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// domestic DNS
|
|
||||||
val domesticDns = SettingsManager.getDomesticDnsServers()
|
|
||||||
val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
|
|
||||||
val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
|
|
||||||
val cnRegionFilter = { domain: String ->
|
|
||||||
domain.startsWith("geosite:") && (domain.endsWith("-cn") || domain.endsWith("@cn"))
|
|
||||||
|| domain == AppConfig.GEOSITE_CN
|
|
||||||
}
|
|
||||||
val finalDirectDomain = if (isCnRoutingMode) directDomain.filterNot {
|
|
||||||
cnRegionFilter(it)
|
|
||||||
} else directDomain
|
|
||||||
val domesticDnsTags = mutableListOf<String>()
|
|
||||||
domesticDns.forEachIndexed { index, element ->
|
|
||||||
val tag = AppConfig.TAG_DOMESTIC_DNS + index
|
|
||||||
servers.add(
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
address = element,
|
|
||||||
domains = finalDirectDomain,
|
|
||||||
skipFallback = true,
|
|
||||||
tag = tag
|
|
||||||
)
|
|
||||||
)
|
|
||||||
domesticDnsTags.add(tag)
|
|
||||||
}
|
|
||||||
if (isCnRoutingMode) {
|
|
||||||
val geoipCn = arrayListOf(AppConfig.GEOIP_CN)
|
|
||||||
val cnRegionDomain = directDomain.filter { cnRegionFilter(it) }
|
|
||||||
domesticDns.forEachIndexed { index, element ->
|
|
||||||
val geositeCnDnsTag = AppConfig.TAG_DOMESTIC_DNS + index + "_cn_expect"
|
|
||||||
servers.add(
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
address = element,
|
|
||||||
domains = cnRegionDomain,
|
|
||||||
expectIPs = geoipCn,
|
|
||||||
skipFallback = true,
|
|
||||||
tag = geositeCnDnsTag
|
|
||||||
)
|
|
||||||
)
|
|
||||||
domesticDnsTags.add(geositeCnDnsTag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//block dns
|
|
||||||
val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
|
|
||||||
if (blkDomain.isNotEmpty()) {
|
|
||||||
hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
|
|
||||||
}
|
|
||||||
|
|
||||||
// hardcode googleapi rule to fix play store problems
|
|
||||||
hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
|
|
||||||
|
|
||||||
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
|
||||||
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
|
|
||||||
hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES
|
|
||||||
hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES
|
|
||||||
hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES
|
|
||||||
hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES
|
|
||||||
hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
|
|
||||||
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
|
|
||||||
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
|
|
||||||
|
|
||||||
//User DNS hosts
|
|
||||||
try {
|
|
||||||
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
|
||||||
if (userHosts.isNotNullEmpty()) {
|
|
||||||
var userHostsMap = userHosts?.split(",")
|
|
||||||
?.filter { it.isNotEmpty() }
|
|
||||||
?.filter { it.contains(":") }
|
|
||||||
?.associate { it.split(":").let { (k, v) -> k to v } }
|
|
||||||
if (userHostsMap != null) hosts.putAll(userHostsMap)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS dns
|
|
||||||
v2rayConfig.dns = V2rayConfig.DnsBean(
|
|
||||||
servers = servers,
|
|
||||||
hosts = hosts,
|
|
||||||
tag = AppConfig.TAG_DNS,
|
|
||||||
enableParallelQuery = if ((domesticDns.size + remoteDns.size) > 2) true else null
|
|
||||||
)
|
|
||||||
|
|
||||||
// DNS routing
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
outboundTag = AppConfig.TAG_DIRECT,
|
|
||||||
inboundTag = domesticDnsTags,
|
|
||||||
domain = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
outboundTag = AppConfig.TAG_PROXY,
|
|
||||||
inboundTag = arrayListOf(AppConfig.TAG_DNS),
|
|
||||||
domain = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to configure DNS", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
|
|
||||||
//region outbound related functions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the primary outbound connection.
|
|
||||||
*
|
|
||||||
* Converts the profile to an outbound configuration and applies global settings.
|
|
||||||
*
|
|
||||||
* @param configContext Runtime context containing the selected profile
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
* @return true if outbound configuration was successful, null if there was an error
|
|
||||||
*/
|
|
||||||
private fun getOutbounds(configContext: CoreConfigContext, v2rayConfig: V2rayConfig): Boolean? {
|
|
||||||
val outbound = convertProfile2Outbound(configContext.selectedProfile) ?: return null
|
|
||||||
|
|
||||||
if (v2rayConfig.outbounds.isNotEmpty()) {
|
|
||||||
v2rayConfig.outbounds[0] = outbound
|
|
||||||
} else {
|
|
||||||
v2rayConfig.outbounds.add(outbound)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures load balancing settings for the V2ray configuration.
|
|
||||||
*
|
|
||||||
* @param configContext Runtime context containing policy group settings
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified with balancing settings
|
|
||||||
*/
|
|
||||||
private fun getBalance(configContext: CoreConfigContext, v2rayConfig: V2rayConfig) {
|
|
||||||
val config = configContext.selectedProfile
|
|
||||||
try {
|
|
||||||
v2rayConfig.routing.rules.forEach { rule ->
|
|
||||||
if (rule.outboundTag == AppConfig.TAG_PROXY) {
|
|
||||||
rule.outboundTag = null
|
|
||||||
rule.balancerTag = AppConfig.TAG_BALANCER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val lstSelector = listOf("proxy-")
|
|
||||||
when (config.policyGroupType) {
|
|
||||||
// Least Ping goto else
|
|
||||||
"1" -> {
|
|
||||||
// Least Load
|
|
||||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
|
||||||
tag = AppConfig.TAG_BALANCER,
|
|
||||||
selector = lstSelector,
|
|
||||||
strategy = V2rayConfig.RoutingBean.StrategyObject(
|
|
||||||
type = "leastLoad"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
v2rayConfig.routing.balancers = listOf(balancer)
|
|
||||||
v2rayConfig.burstObservatory = V2rayConfig.BurstObservatoryObject(
|
|
||||||
subjectSelector = lstSelector,
|
|
||||||
pingConfig = V2rayConfig.BurstObservatoryObject.PingConfigObject(
|
|
||||||
destination = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL,
|
|
||||||
interval = "5m",
|
|
||||||
sampling = 2,
|
|
||||||
timeout = "30s"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"2" -> {
|
|
||||||
// Random
|
|
||||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
|
||||||
tag = AppConfig.TAG_BALANCER,
|
|
||||||
selector = lstSelector,
|
|
||||||
strategy = V2rayConfig.RoutingBean.StrategyObject(
|
|
||||||
type = "random"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
v2rayConfig.routing.balancers = listOf(balancer)
|
|
||||||
}
|
|
||||||
|
|
||||||
"3" -> {
|
|
||||||
// Round Robin
|
|
||||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
|
||||||
tag = AppConfig.TAG_BALANCER,
|
|
||||||
selector = lstSelector,
|
|
||||||
strategy = V2rayConfig.RoutingBean.StrategyObject(
|
|
||||||
type = "roundRobin"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
v2rayConfig.routing.balancers = listOf(balancer)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// Default: Least Ping
|
|
||||||
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
|
||||||
tag = AppConfig.TAG_BALANCER,
|
|
||||||
selector = lstSelector,
|
|
||||||
strategy = V2rayConfig.RoutingBean.StrategyObject(
|
|
||||||
type = "leastPing"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
v2rayConfig.routing.balancers = listOf(balancer)
|
|
||||||
v2rayConfig.observatory = V2rayConfig.ObservatoryObject(
|
|
||||||
subjectSelector = lstSelector,
|
|
||||||
probeUrl = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL,
|
|
||||||
probeInterval = "3m",
|
|
||||||
enableConcurrency = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (v2rayConfig.routing.domainStrategy == "IPIfNonMatch") {
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
ip = arrayListOf("0.0.0.0/0", "::/0"),
|
|
||||||
balancerTag = AppConfig.TAG_BALANCER,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
network = "tcp,udp",
|
|
||||||
balancerTag = AppConfig.TAG_BALANCER,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to configure balance", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves domain names to IP addresses in outbound connections.
|
|
||||||
*
|
|
||||||
* Pre-resolves domains to improve connection speed and reliability.
|
|
||||||
*
|
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
|
||||||
*/
|
|
||||||
private fun resolveOutboundDomainsToHosts(v2rayConfig: V2rayConfig) {
|
|
||||||
val proxyOutboundList = v2rayConfig.getAllProxyOutbound()
|
|
||||||
val dns = v2rayConfig.dns ?: return
|
|
||||||
val newHosts = dns.hosts?.toMutableMap() ?: mutableMapOf()
|
|
||||||
val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true
|
|
||||||
|
|
||||||
for (item in proxyOutboundList) {
|
|
||||||
val domain = item.getServerAddress()
|
|
||||||
if (domain.isNullOrEmpty()) continue
|
|
||||||
|
|
||||||
if (newHosts.containsKey(domain)) {
|
|
||||||
item.ensureSockopt().domainStrategy = "UseIP"
|
|
||||||
item.ensureSockopt().happyEyeballs = V2rayConfig.OutboundBean.StreamSettingsBean.HappyEyeballsBean(
|
|
||||||
prioritizeIPv6 = preferIpv6,
|
|
||||||
interleave = 2
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6)
|
|
||||||
if (resolvedIps.isNullOrEmpty()) continue
|
|
||||||
|
|
||||||
item.ensureSockopt().domainStrategy = "UseIP"
|
|
||||||
item.ensureSockopt().happyEyeballs = V2rayConfig.OutboundBean.StreamSettingsBean.HappyEyeballsBean(
|
|
||||||
prioritizeIPv6 = preferIpv6,
|
|
||||||
interleave = 2
|
|
||||||
)
|
|
||||||
newHosts[domain] = if (resolvedIps.size == 1) {
|
|
||||||
resolvedIps[0]
|
|
||||||
} else {
|
|
||||||
resolvedIps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dns.hosts = newHosts
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a profile item to an outbound configuration.
|
|
||||||
*
|
|
||||||
* Delegates to [CoreOutboundBuilder] which owns all per-protocol
|
|
||||||
* conversion logic, keeping this manager focused on config orchestration.
|
|
||||||
*
|
|
||||||
* @param profileItem The profile item to convert
|
|
||||||
* @return OutboundBean configuration for the profile, or null if not supported
|
|
||||||
*/
|
|
||||||
private fun convertProfile2Outbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
|
|
||||||
return CoreOutboundBuilder.convert(profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package com.v2ray.ang.core
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.util.LogUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import go.Seq
|
|
||||||
import libv2ray.CoreCallbackHandler
|
|
||||||
import libv2ray.CoreController
|
|
||||||
import libv2ray.Libv2ray
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* V2Ray Native Library Manager
|
|
||||||
*
|
|
||||||
* Thread-safe singleton wrapper for Libv2ray native methods.
|
|
||||||
* Provides initialization protection and unified API for V2Ray core operations.
|
|
||||||
*/
|
|
||||||
object CoreNativeManager {
|
|
||||||
private val initialized = AtomicBoolean(false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize V2Ray core environment.
|
|
||||||
* This method is thread-safe and ensures initialization happens only once.
|
|
||||||
* Subsequent calls will be ignored silently.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fun initCoreEnv(context: Context?) {
|
|
||||||
if (initialized.compareAndSet(false, true)) {
|
|
||||||
try {
|
|
||||||
Seq.setContext(context?.applicationContext)
|
|
||||||
val assetPath = Utils.userAssetPath(context)
|
|
||||||
val deviceId = Utils.getDeviceIdForXUDPBaseKey()
|
|
||||||
Libv2ray.initCoreEnv(assetPath, deviceId)
|
|
||||||
LogUtil.i(AppConfig.TAG, "V2Ray core environment initialized successfully")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to initialize V2Ray core environment", e)
|
|
||||||
initialized.set(false)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogUtil.d(AppConfig.TAG, "V2Ray core environment already initialized, skipping")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get V2Ray core version.
|
|
||||||
*
|
|
||||||
* @return Version string of the V2Ray core
|
|
||||||
*/
|
|
||||||
fun getLibVersion(): String {
|
|
||||||
return try {
|
|
||||||
Libv2ray.checkVersionX()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to check V2Ray version", e)
|
|
||||||
"Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measure outbound connection delay.
|
|
||||||
*
|
|
||||||
* @param config The configuration JSON string
|
|
||||||
* @param testUrl The URL to test against
|
|
||||||
* @return Delay in milliseconds, or -1 if test failed
|
|
||||||
*/
|
|
||||||
fun measureOutboundDelay(config: String, testUrl: String): Long {
|
|
||||||
return try {
|
|
||||||
Libv2ray.measureOutboundDelay(config, testUrl)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to measure outbound delay", e)
|
|
||||||
-1L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new core controller instance.
|
|
||||||
*
|
|
||||||
* @param handler The callback handler for core events
|
|
||||||
* @return A new CoreController instance
|
|
||||||
*/
|
|
||||||
fun newCoreController(handler: CoreCallbackHandler): CoreController {
|
|
||||||
return try {
|
|
||||||
Libv2ray.newCoreController(handler)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to create core controller", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,697 +0,0 @@
|
|||||||
package com.v2ray.ang.core
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.enums.NetworkType
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.extension.nullIfBlank
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.LogUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Centralizes ProfileItem -> OutboundBean conversion.
|
|
||||||
* Most protocol builders mirror the previous *Fmt.toOutbound behavior.
|
|
||||||
*/
|
|
||||||
object CoreOutboundBuilder {
|
|
||||||
|
|
||||||
/** Dispatches a profile to protocol-specific outbound builder. */
|
|
||||||
fun convert(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outbound = when (profileItem.configType) {
|
|
||||||
EConfigType.VMESS -> toOutboundVmess(profileItem)
|
|
||||||
EConfigType.SHADOWSOCKS -> toOutboundShadowsocks(profileItem)
|
|
||||||
EConfigType.SOCKS -> toOutboundSocks(profileItem)
|
|
||||||
EConfigType.VLESS -> toOutboundVless(profileItem)
|
|
||||||
EConfigType.TROJAN -> toOutboundTrojan(profileItem)
|
|
||||||
EConfigType.WIREGUARD -> toOutboundWireguard(profileItem)
|
|
||||||
EConfigType.HYSTERIA2 -> toOutboundHysteria2(profileItem)
|
|
||||||
EConfigType.HTTP -> toOutboundHttp(profileItem)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
outbound ?: return null
|
|
||||||
val ret = updateOutboundWithGlobalSettings(outbound)
|
|
||||||
if (!ret) return null
|
|
||||||
return outbound
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Applies global outbound options (mux, protocol-specific tweaks, etc.). */
|
|
||||||
private fun updateOutboundWithGlobalSettings(outbound: OutboundBean): Boolean {
|
|
||||||
try {
|
|
||||||
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
|
||||||
val protocol = outbound.protocol
|
|
||||||
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.WIREGUARD.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA.name, true)
|
|
||||||
) {
|
|
||||||
muxEnabled = false
|
|
||||||
} else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
|
|
||||||
muxEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (muxEnabled) {
|
|
||||||
outbound.mux?.enabled = true
|
|
||||||
outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt()
|
|
||||||
outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt()
|
|
||||||
outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC, "reject")
|
|
||||||
if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true) {
|
|
||||||
outbound.mux?.concurrency = -1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
outbound.mux?.enabled = false
|
|
||||||
outbound.mux?.concurrency = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
var localTunAddr = if (outbound.settings?.address == null) {
|
|
||||||
listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
|
|
||||||
} else {
|
|
||||||
outbound.settings?.address as List<*>
|
|
||||||
}
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) != true) {
|
|
||||||
localTunAddr = listOf(localTunAddr.first())
|
|
||||||
}
|
|
||||||
outbound.settings?.address = localTunAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outbound.streamSettings?.network == AppConfig.DEFAULT_NETWORK
|
|
||||||
&& outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.HEADER_TYPE_HTTP
|
|
||||||
) {
|
|
||||||
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
|
||||||
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
|
||||||
|
|
||||||
val requestString: String by lazy {
|
|
||||||
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":["no-cache"]}}"""
|
|
||||||
}
|
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
|
||||||
requestString,
|
|
||||||
OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
|
||||||
)
|
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request?.path =
|
|
||||||
if (path.isNullOrEmpty()) {
|
|
||||||
listOf("/")
|
|
||||||
} else {
|
|
||||||
path
|
|
||||||
}
|
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Creates an initial outbound template for a protocol type. */
|
|
||||||
fun createInitOutbound(configType: EConfigType): OutboundBean? {
|
|
||||||
return when (configType) {
|
|
||||||
EConfigType.VMESS,
|
|
||||||
EConfigType.VLESS ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutboundBean.OutSettingsBean(
|
|
||||||
vnext = listOf(
|
|
||||||
OutboundBean.OutSettingsBean.VnextBean(
|
|
||||||
users = listOf(OutboundBean.OutSettingsBean.VnextBean.UsersBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
streamSettings = OutboundBean.StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.SHADOWSOCKS,
|
|
||||||
EConfigType.SOCKS,
|
|
||||||
EConfigType.HTTP,
|
|
||||||
EConfigType.TROJAN ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutboundBean.OutSettingsBean(
|
|
||||||
servers = listOf(OutboundBean.OutSettingsBean.ServersBean())
|
|
||||||
),
|
|
||||||
streamSettings = OutboundBean.StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.WIREGUARD ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutboundBean.OutSettingsBean(
|
|
||||||
secretKey = "",
|
|
||||||
peers = listOf(OutboundBean.OutSettingsBean.WireGuardBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.HYSTERIA,
|
|
||||||
EConfigType.HYSTERIA2 ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = EConfigType.HYSTERIA.name.lowercase(),
|
|
||||||
settings = OutboundBean.OutSettingsBean(
|
|
||||||
servers = null
|
|
||||||
),
|
|
||||||
streamSettings = OutboundBean.StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Per-protocol builders — implementations are identical to each *Fmt.toOutbound ──
|
|
||||||
|
|
||||||
private fun toOutboundVmess(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = 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 {
|
|
||||||
populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.let {
|
|
||||||
populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toOutboundVless(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = 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 {
|
|
||||||
populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.let {
|
|
||||||
populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toOutboundShadowsocks(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = 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 {
|
|
||||||
populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.let {
|
|
||||||
populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toOutboundTrojan(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = 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 {
|
|
||||||
populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.let {
|
|
||||||
populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toOutboundSocks(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = 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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toOutboundHttp(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = 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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = createInitOutbound(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
outboundBean?.settings?.let { wireguard ->
|
|
||||||
wireguard.secretKey = profileItem.secretKey
|
|
||||||
wireguard.address = (profileItem.localAddress ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
|
||||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
|
||||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
|
||||||
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toOutboundHysteria2(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = createInitOutbound(EConfigType.HYSTERIA2) ?: return null
|
|
||||||
profileItem.network = NetworkType.HYSTERIA.type
|
|
||||||
profileItem.alpn = "h3"
|
|
||||||
|
|
||||||
outboundBean.settings?.let { server ->
|
|
||||||
server.address = getServerAddress(profileItem)
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
server.version = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
val sni = outboundBean.streamSettings?.let {
|
|
||||||
populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean.streamSettings?.let {
|
|
||||||
populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures transport settings for an outbound connection.
|
|
||||||
*
|
|
||||||
* Sets up protocol-specific transport options based on the profile settings.
|
|
||||||
*
|
|
||||||
* @param streamSettings The stream settings to configure
|
|
||||||
* @param profileItem The profile containing transport configuration
|
|
||||||
* @return The Server Name Indication (SNI) value to use, or null if not applicable
|
|
||||||
*/
|
|
||||||
fun populateTransportSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem): String? {
|
|
||||||
val transport = profileItem.network.orEmpty()
|
|
||||||
val headerType = profileItem.headerType
|
|
||||||
val host = profileItem.host
|
|
||||||
val path = profileItem.path
|
|
||||||
val seed = profileItem.seed
|
|
||||||
// val quicSecurity = profileItem.quicSecurity
|
|
||||||
// val key = profileItem.quicKey
|
|
||||||
val mode = profileItem.mode
|
|
||||||
val serviceName = profileItem.serviceName
|
|
||||||
val authority = profileItem.authority
|
|
||||||
val xhttpMode = profileItem.xhttpMode
|
|
||||||
val xhttpExtra = profileItem.xhttpExtra
|
|
||||||
val finalMask = profileItem.finalMask
|
|
||||||
var sni: String? = null
|
|
||||||
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
|
||||||
when (streamSettings.network) {
|
|
||||||
NetworkType.TCP.type -> {
|
|
||||||
val tcpSetting = OutboundBean.StreamSettingsBean.TcpSettingsBean()
|
|
||||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
|
||||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
|
||||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
|
||||||
val requestObj = OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
|
|
||||||
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
tcpSetting.header.request = requestObj
|
|
||||||
sni = requestObj.headers.Host?.getOrNull(0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tcpSetting.header.type = "none"
|
|
||||||
sni = host
|
|
||||||
}
|
|
||||||
streamSettings.tcpSettings = tcpSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP.type -> {
|
|
||||||
val kcpSetting = OutboundBean.StreamSettingsBean.KcpSettingsBean()
|
|
||||||
profileItem.kcpMtu?.let { kcpSetting.mtu = it }
|
|
||||||
profileItem.kcpTti?.let { kcpSetting.tti = it }
|
|
||||||
streamSettings.kcpSettings = kcpSetting
|
|
||||||
val udpMaskList = mutableListOf<OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean>()
|
|
||||||
if (!headerType.isNullOrEmpty() && headerType != "none") {
|
|
||||||
val kcpHeaderType = when {
|
|
||||||
headerType == "wechat-video" -> "header-wechat"
|
|
||||||
else -> "header-$headerType"
|
|
||||||
}
|
|
||||||
udpMaskList.add(
|
|
||||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
|
||||||
type = kcpHeaderType,
|
|
||||||
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
|
||||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
|
||||||
domain = host
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (seed.isNullOrEmpty()) {
|
|
||||||
udpMaskList.add(
|
|
||||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
|
||||||
type = "mkcp-original"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
udpMaskList.add(
|
|
||||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
|
||||||
type = "mkcp-aes128gcm",
|
|
||||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
|
||||||
password = seed
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
streamSettings.finalmask = OutboundBean.StreamSettingsBean.FinalMaskBean(
|
|
||||||
udp = udpMaskList.toList()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.WS.type -> {
|
|
||||||
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
|
|
||||||
wssetting.headers.Host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
wssetting.path = path ?: "/"
|
|
||||||
streamSettings.wsSettings = wssetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HTTP_UPGRADE.type -> {
|
|
||||||
val httpupgradeSetting = OutboundBean.StreamSettingsBean.HttpupgradeSettingsBean()
|
|
||||||
httpupgradeSetting.host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
httpupgradeSetting.path = path ?: "/"
|
|
||||||
streamSettings.httpupgradeSettings = httpupgradeSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.XHTTP.type -> {
|
|
||||||
val xhttpSetting = OutboundBean.StreamSettingsBean.XhttpSettingsBean()
|
|
||||||
xhttpSetting.host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
xhttpSetting.path = path ?: "/"
|
|
||||||
xhttpSetting.mode = xhttpMode
|
|
||||||
xhttpSetting.extra = JsonUtil.parseString(xhttpExtra)
|
|
||||||
streamSettings.xhttpSettings = xhttpSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
|
||||||
streamSettings.network = NetworkType.H2.type
|
|
||||||
val h2Setting = OutboundBean.StreamSettingsBean.HttpSettingsBean()
|
|
||||||
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
sni = h2Setting.host.getOrNull(0)
|
|
||||||
h2Setting.path = path ?: "/"
|
|
||||||
streamSettings.httpSettings = h2Setting
|
|
||||||
}
|
|
||||||
|
|
||||||
// "quic" -> {
|
|
||||||
// val quicsetting = QuicSettingBean()
|
|
||||||
// quicsetting.security = quicSecurity ?: "none"
|
|
||||||
// quicsetting.key = key.orEmpty()
|
|
||||||
// quicsetting.header.type = headerType ?: "none"
|
|
||||||
// quicSettings = quicsetting
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC.type -> {
|
|
||||||
val grpcSetting = OutboundBean.StreamSettingsBean.GrpcSettingsBean()
|
|
||||||
grpcSetting.multiMode = mode == "multi"
|
|
||||||
grpcSetting.serviceName = serviceName.orEmpty()
|
|
||||||
grpcSetting.authority = authority.orEmpty()
|
|
||||||
grpcSetting.idle_timeout = 60
|
|
||||||
grpcSetting.health_check_timeout = 20
|
|
||||||
sni = authority
|
|
||||||
streamSettings.grpcSettings = grpcSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HYSTERIA.type -> {
|
|
||||||
val hysteriaSetting = OutboundBean.StreamSettingsBean.HysteriaSettingsBean(
|
|
||||||
version = 2,
|
|
||||||
auth = profileItem.password.orEmpty(),
|
|
||||||
)
|
|
||||||
val quicParams = OutboundBean.StreamSettingsBean.FinalMaskBean.QuicParamsBean(
|
|
||||||
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
|
|
||||||
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
|
|
||||||
)
|
|
||||||
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
|
|
||||||
if (profileItem.portHopping.isNotNullEmpty()) {
|
|
||||||
val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
|
|
||||||
val interval = if (rawInterval == null) {
|
|
||||||
"30"
|
|
||||||
} else {
|
|
||||||
val singleValue = rawInterval.toIntOrNull()
|
|
||||||
if (singleValue != null) {
|
|
||||||
if (singleValue < 5) {
|
|
||||||
"30"
|
|
||||||
} else {
|
|
||||||
rawInterval
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val parts = rawInterval.split('-')
|
|
||||||
if (parts.size == 2) {
|
|
||||||
val start = parts[0].trim().toIntOrNull()
|
|
||||||
val end = parts[1].trim().toIntOrNull()
|
|
||||||
if (start != null && end != null) {
|
|
||||||
val minStart = maxOf(5, start)
|
|
||||||
val minEnd = maxOf(minStart, end)
|
|
||||||
"$minStart-$minEnd"
|
|
||||||
} else {
|
|
||||||
"30"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
"30"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
quicParams.udpHop = OutboundBean.StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
|
|
||||||
ports = profileItem.portHopping,
|
|
||||||
interval = interval
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val finalmask = OutboundBean.StreamSettingsBean.FinalMaskBean(
|
|
||||||
quicParams = quicParams
|
|
||||||
)
|
|
||||||
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
|
||||||
finalmask.udp = listOf(
|
|
||||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
|
||||||
type = "salamander",
|
|
||||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
|
||||||
password = profileItem.obfsPassword.orEmpty()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
streamSettings.hysteriaSettings = hysteriaSetting
|
|
||||||
streamSettings.finalmask = finalmask
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finalMask?.let {
|
|
||||||
val parsedFinalMask = JsonUtil.parseString(finalMask)
|
|
||||||
if (parsedFinalMask != null) {
|
|
||||||
streamSettings.finalmask = parsedFinalMask
|
|
||||||
} else {
|
|
||||||
LogUtil.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sni
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures TLS or REALITY security settings for an outbound connection.
|
|
||||||
*
|
|
||||||
* Sets up security-related parameters like certificates, fingerprints, and SNI.
|
|
||||||
*
|
|
||||||
* @param streamSettings The stream settings to configure
|
|
||||||
* @param profileItem The profile containing security configuration
|
|
||||||
* @param sniExt An external SNI value to use if the profile doesn't specify one
|
|
||||||
*/
|
|
||||||
fun populateTlsSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
|
|
||||||
val streamSecurity = profileItem.security.orEmpty()
|
|
||||||
val allowInsecure = profileItem.insecure == true
|
|
||||||
val sni = if (profileItem.sni.isNullOrEmpty()) {
|
|
||||||
when {
|
|
||||||
sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
|
|
||||||
profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server
|
|
||||||
else -> sniExt
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
profileItem.sni
|
|
||||||
}
|
|
||||||
|
|
||||||
streamSettings.security = streamSecurity.nullIfBlank()
|
|
||||||
if (streamSettings.security == null) return
|
|
||||||
val tlsSetting = OutboundBean.StreamSettingsBean.TlsSettingsBean(
|
|
||||||
allowInsecure = allowInsecure,
|
|
||||||
serverName = sni.nullIfBlank(),
|
|
||||||
fingerprint = profileItem.fingerPrint.nullIfBlank(),
|
|
||||||
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
|
|
||||||
echConfigList = profileItem.echConfigList.nullIfBlank(),
|
|
||||||
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
|
|
||||||
publicKey = profileItem.publicKey.nullIfBlank(),
|
|
||||||
shortId = profileItem.shortId.nullIfBlank(),
|
|
||||||
spiderX = profileItem.spiderX.nullIfBlank(),
|
|
||||||
mldsa65Verify = profileItem.mldsa65Verify.nullIfBlank(),
|
|
||||||
)
|
|
||||||
if (streamSettings.security == AppConfig.TLS) {
|
|
||||||
streamSettings.tlsSettings = tlsSetting
|
|
||||||
streamSettings.realitySettings = null
|
|
||||||
} else if (streamSettings.security == AppConfig.REALITY) {
|
|
||||||
streamSettings.tlsSettings = null
|
|
||||||
streamSettings.realitySettings = tlsSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profileItem.finalMask.isNullOrEmpty()) {
|
|
||||||
updateOutboundFragment(streamSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the outbound with fragment settings for traffic optimization.
|
|
||||||
*
|
|
||||||
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
|
||||||
*
|
|
||||||
* @param streamSettings The streamSettings object to be modified
|
|
||||||
* @return true if fragment configuration was successful, false otherwise
|
|
||||||
*/
|
|
||||||
private fun updateOutboundFragment(streamSettings: OutboundBean.StreamSettingsBean): Boolean {
|
|
||||||
try {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (streamSettings.security != AppConfig.TLS
|
|
||||||
&& streamSettings.security != AppConfig.REALITY
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (streamSettings.sockopt?.dialerProxy.isNotNullEmpty()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var packets =
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
|
||||||
if (streamSettings.security == AppConfig.REALITY
|
|
||||||
&& packets == "tlshello"
|
|
||||||
) {
|
|
||||||
packets = "1-3"
|
|
||||||
} else if (streamSettings.security == AppConfig.TLS
|
|
||||||
&& packets != "tlshello"
|
|
||||||
) {
|
|
||||||
packets = "tlshello"
|
|
||||||
}
|
|
||||||
|
|
||||||
val fragmentMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
|
||||||
type = "fragment",
|
|
||||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
|
||||||
packets = packets,
|
|
||||||
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
|
||||||
?: "50-100",
|
|
||||||
delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
|
||||||
?: "10-20"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val noiseMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
|
||||||
type = "noise",
|
|
||||||
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
|
||||||
noise = listOf(
|
|
||||||
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean.NoiseMaskBean(
|
|
||||||
rand = "10-20",
|
|
||||||
delay = "10-16",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val finalMaskObj = streamSettings.finalmask?.let { existingFinalMask ->
|
|
||||||
JsonUtil.parseString(JsonUtil.toJson(existingFinalMask))
|
|
||||||
} ?: JsonObject()
|
|
||||||
|
|
||||||
// finalmask.tcp / finalmask.udp are arrays; prepend mask at index 0.
|
|
||||||
fun prependMask(scope: String, mask: OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean) {
|
|
||||||
val current = finalMaskObj.get(scope)
|
|
||||||
if (current != null && current.isJsonArray && current.asJsonArray.size() > 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val newArray = JsonArray()
|
|
||||||
newArray.add(JsonUtil.parseString(JsonUtil.toJson(mask)))
|
|
||||||
|
|
||||||
if (current != null && current.isJsonArray) {
|
|
||||||
current.asJsonArray.forEach { newArray.add(it) }
|
|
||||||
}
|
|
||||||
finalMaskObj.add(scope, newArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
prependMask("tcp", fragmentMask)
|
|
||||||
prependMask("udp", noiseMask)
|
|
||||||
streamSettings.finalmask = finalMaskObj
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,475 +0,0 @@
|
|||||||
package com.v2ray.ang.core
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.ParcelFileDescriptor
|
|
||||||
import android.system.OsConstants
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.contracts.ServiceControl
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.NotificationManager
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.handler.SpeedtestManager
|
|
||||||
import com.v2ray.ang.service.CoreProxyOnlyService
|
|
||||||
import com.v2ray.ang.service.CoreVpnService
|
|
||||||
import com.v2ray.ang.util.LogUtil
|
|
||||||
import com.v2ray.ang.util.MessageUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import libv2ray.CoreCallbackHandler
|
|
||||||
import libv2ray.CoreController
|
|
||||||
import libv2ray.ProcessFinder
|
|
||||||
import java.lang.ref.SoftReference
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
|
|
||||||
object CoreServiceManager {
|
|
||||||
|
|
||||||
private val coreController: CoreController = CoreNativeManager.newCoreController(CoreCallback())
|
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
|
||||||
private var currentConfig: ProfileItem? = null
|
|
||||||
private var processFinder: XrayProcessFinder? = null
|
|
||||||
|
|
||||||
var serviceControl: SoftReference<ServiceControl>? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
val service = value?.get()?.getService()
|
|
||||||
CoreNativeManager.initCoreEnv(service)
|
|
||||||
if (service != null && processFinder == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
processFinder = XrayProcessFinder(service)
|
|
||||||
coreController.registerProcessFinder(processFinder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the V2Ray service from a toggle action.
|
|
||||||
* @param context The context from which the service is started.
|
|
||||||
* @return True if the service was started successfully, false otherwise.
|
|
||||||
*/
|
|
||||||
fun startVServiceFromToggle(context: Context): Boolean {
|
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
|
||||||
context.toast(R.string.app_tile_first_use)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
startContextService(context)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the V2Ray service.
|
|
||||||
* @param context The context from which the service is started.
|
|
||||||
* @param guid The GUID of the server configuration to use (optional).
|
|
||||||
*/
|
|
||||||
fun startVService(context: Context, guid: String? = null) {
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
|
||||||
|
|
||||||
if (guid != null) {
|
|
||||||
MmkvManager.setSelectServer(guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
startContextService(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the V2Ray service.
|
|
||||||
* @param context The context from which the service is stopped.
|
|
||||||
*/
|
|
||||||
fun stopVService(context: Context) {
|
|
||||||
//context.toast(R.string.toast_services_stop)
|
|
||||||
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the V2Ray service is running.
|
|
||||||
* @return True if the service is running, false otherwise.
|
|
||||||
*/
|
|
||||||
fun isRunning() = coreController.isRunning
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the name of the currently running server.
|
|
||||||
* @return The name of the running server.
|
|
||||||
*/
|
|
||||||
fun getRunningServerName() = currentConfig?.remarks.orEmpty()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the context service for V2Ray.
|
|
||||||
* Chooses between VPN service or Proxy-only service based on user settings.
|
|
||||||
* @param context The context from which the service is started.
|
|
||||||
*/
|
|
||||||
private fun startContextService(context: Context) {
|
|
||||||
if (coreController.isRunning) {
|
|
||||||
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val guid = MmkvManager.getSelectServer()
|
|
||||||
if (guid == null) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid)
|
|
||||||
if (config == null) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.configType != EConfigType.CUSTOM
|
|
||||||
&& config.configType != EConfigType.POLICYGROUP
|
|
||||||
&& config.configType != EConfigType.PROXYCHAIN
|
|
||||||
&& !Utils.isValidUrl(config.server)
|
|
||||||
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
|
||||||
) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// refresh socks port when enabled dynamic socks port
|
|
||||||
SettingsManager.refreshRuntimeSocksPort()
|
|
||||||
|
|
||||||
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
|
||||||
// if (!result.status) return
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING)) {
|
|
||||||
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
|
||||||
} else {
|
|
||||||
context.toast(R.string.toast_services_start)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isVpnMode = SettingsManager.isVpnMode()
|
|
||||||
val intent = if (isVpnMode) {
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
|
|
||||||
Intent(context.applicationContext, CoreVpnService::class.java)
|
|
||||||
} else {
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service")
|
|
||||||
Intent(context.applicationContext, CoreProxyOnlyService::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to start service", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
|
||||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
|
||||||
* Starts the V2Ray core service.
|
|
||||||
*/
|
|
||||||
fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean {
|
|
||||||
if (coreController.isRunning) {
|
|
||||||
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val service = getService()
|
|
||||||
if (service == null) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Service is null")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val guid = MmkvManager.getSelectServer()
|
|
||||||
if (guid == null) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid)
|
|
||||||
if (config == null) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
|
|
||||||
val result = CoreConfigManager.getV2rayConfig(service, guid)
|
|
||||||
LogUtil.d(AppConfig.TAG, result.content)
|
|
||||||
if (!result.status) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to get V2Ray config")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
|
||||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
|
||||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
|
||||||
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to register receiver", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
currentConfig = config
|
|
||||||
var tunFd = vpnInterface?.fd ?: 0
|
|
||||||
if (SettingsManager.isUsingHevTun()) {
|
|
||||||
tunFd = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
NotificationManager.showNotification(currentConfig)
|
|
||||||
coreController.startLoop(result.content, tunFd)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to start core loop", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coreController.isRunning == false) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Core failed to start")
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
|
||||||
NotificationManager.cancelNotification()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
|
||||||
NotificationManager.startSpeedNotification(currentConfig)
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to complete startup", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the V2Ray core service.
|
|
||||||
* Unregisters broadcast receivers, stops notifications, and shuts down plugins.
|
|
||||||
* @return True if the core was stopped successfully, false otherwise.
|
|
||||||
*/
|
|
||||||
fun stopCoreLoop(): Boolean {
|
|
||||||
val service = getService() ?: return false
|
|
||||||
|
|
||||||
if (coreController.isRunning) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
coreController.stopLoop()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
|
||||||
NotificationManager.cancelNotification()
|
|
||||||
|
|
||||||
try {
|
|
||||||
service.unregisterReceiver(mMsgReceive)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the statistics for a given tag and link.
|
|
||||||
* @param tag The tag to query.
|
|
||||||
* @param link The link to query.
|
|
||||||
* @return The statistics value.
|
|
||||||
*/
|
|
||||||
fun queryStats(tag: String, link: String): Long {
|
|
||||||
return coreController.queryStats(tag, link)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the connection delay for the current V2Ray configuration.
|
|
||||||
* Tests with primary URL first, then falls back to alternative URL if needed.
|
|
||||||
* Also fetches remote IP information if the delay test was successful.
|
|
||||||
*/
|
|
||||||
private fun measureV2rayDelay() {
|
|
||||||
if (coreController.isRunning == false) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val service = getService() ?: return@launch
|
|
||||||
var time = -1L
|
|
||||||
var errorStr = ""
|
|
||||||
|
|
||||||
try {
|
|
||||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
|
||||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
|
||||||
}
|
|
||||||
if (time == -1L) {
|
|
||||||
try {
|
|
||||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
|
||||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = if (time >= 0) {
|
|
||||||
service.getString(R.string.connection_test_available, time)
|
|
||||||
} else {
|
|
||||||
service.getString(R.string.connection_test_error, errorStr)
|
|
||||||
}
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
|
||||||
|
|
||||||
// Only fetch IP info if the delay test was successful
|
|
||||||
if (time >= 0) {
|
|
||||||
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current service instance.
|
|
||||||
* @return The current service instance, or null if not available.
|
|
||||||
*/
|
|
||||||
private fun getService(): Service? {
|
|
||||||
return serviceControl?.get()?.getService()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core callback handler implementation for handling V2Ray core events.
|
|
||||||
* Handles startup, shutdown, socket protection, and status emission.
|
|
||||||
*/
|
|
||||||
private class CoreCallback : CoreCallbackHandler {
|
|
||||||
/**
|
|
||||||
* Called when V2Ray core starts up.
|
|
||||||
* @return 0 for success, any other value for failure.
|
|
||||||
*/
|
|
||||||
override fun startup(): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when V2Ray core shuts down.
|
|
||||||
* @return 0 for success, any other value for failure.
|
|
||||||
*/
|
|
||||||
override fun shutdown(): Long {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
|
||||||
return try {
|
|
||||||
serviceControl.stopService()
|
|
||||||
0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when V2Ray core emits status information.
|
|
||||||
* @param l Status code.
|
|
||||||
* @param s Status message.
|
|
||||||
* @return Always returns 0.
|
|
||||||
*/
|
|
||||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process finder implementation for Xray core.
|
|
||||||
* Uses ConnectivityManager to find the owning UID of a connection based on network parameters.
|
|
||||||
*/
|
|
||||||
private class XrayProcessFinder(context: Context) : ProcessFinder {
|
|
||||||
private val cm: ConnectivityManager? = context.getSystemService(ConnectivityManager::class.java)
|
|
||||||
|
|
||||||
override fun findProcessByConnection(network: String, srcIP: String, srcPort: Long, destIP: String, destPort: Long): Long {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return -1L
|
|
||||||
if (cm == null) return -1L
|
|
||||||
val proto = when (network) {
|
|
||||||
"tcp" -> OsConstants.IPPROTO_TCP
|
|
||||||
"udp" -> OsConstants.IPPROTO_UDP
|
|
||||||
else -> return -1L
|
|
||||||
}
|
|
||||||
|
|
||||||
if (destIP.isBlank() || destPort == 0L) {
|
|
||||||
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to :$destPort, (no dest)")
|
|
||||||
return -1L
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val uid = cm.getConnectionOwnerUid(
|
|
||||||
proto,
|
|
||||||
InetSocketAddress(srcIP, srcPort.toInt()),
|
|
||||||
InetSocketAddress(destIP, destPort.toInt())
|
|
||||||
).toLong()
|
|
||||||
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid")
|
|
||||||
//LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid,${PackageUidResolver.uidToPackageName(uid.toString())}")
|
|
||||||
|
|
||||||
uid
|
|
||||||
} catch (e: Exception) {
|
|
||||||
-1L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast receiver for handling messages sent to the service.
|
|
||||||
* Handles registration, service control, and screen events.
|
|
||||||
*/
|
|
||||||
private class ReceiveMessageHandler : BroadcastReceiver() {
|
|
||||||
/**
|
|
||||||
* Handles received broadcast messages.
|
|
||||||
* Processes service control messages and screen state changes.
|
|
||||||
* @param ctx The context in which the receiver is running.
|
|
||||||
* @param intent The intent being received.
|
|
||||||
*/
|
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return
|
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
|
||||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
|
||||||
if (coreController.isRunning) {
|
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
|
||||||
} else {
|
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START -> {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
|
||||||
serviceControl.stopService()
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
|
||||||
serviceControl.stopService()
|
|
||||||
Thread.sleep(500L)
|
|
||||||
startVService(serviceControl.getService())
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY -> {
|
|
||||||
measureV2rayDelay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (intent?.action) {
|
|
||||||
Intent.ACTION_SCREEN_OFF -> {
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off")
|
|
||||||
NotificationManager.stopSpeedNotification(currentConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent.ACTION_SCREEN_ON -> {
|
|
||||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on")
|
|
||||||
NotificationManager.startSpeedNotification(currentConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
|
|
||||||
data class AppInfo(
|
|
||||||
val appName: String,
|
|
||||||
val packageName: String,
|
|
||||||
val appIcon: Drawable,
|
|
||||||
val isSystemApp: Boolean,
|
|
||||||
var isSelected: Int
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class AssetUrlCache(
|
|
||||||
val guid: String,
|
|
||||||
val assetUrl: AssetUrlItem
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ConfigResult(
|
|
||||||
var status: Boolean,
|
|
||||||
var guid: String? = null,
|
|
||||||
var content: String = "",
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.v2ray.ang.enums.CoreResolvedType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime context produced by the builder and consumed by CoreConfigManager.
|
|
||||||
*/
|
|
||||||
data class CoreConfigContext(
|
|
||||||
val context: Context,
|
|
||||||
val guid: String,
|
|
||||||
val selectedProfile: ProfileItem,
|
|
||||||
val resolvedProfiles: List<ProfileItem>,
|
|
||||||
val resolvedType: CoreResolvedType,
|
|
||||||
val customOutboundProfiles: Map<String, ProfileItem> = emptyMap(),
|
|
||||||
)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class GroupMapItem(
|
|
||||||
var id: String,
|
|
||||||
var remarks: String,
|
|
||||||
)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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,
|
|
||||||
var location: LocationBean? = null
|
|
||||||
) {
|
|
||||||
data class LocationBean(
|
|
||||||
var country_code: String? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
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.enums.EConfigType
|
|
||||||
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 description: String? = null,
|
|
||||||
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 kcpMtu: Int? = null,
|
|
||||||
var kcpTti: Int? = 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 finalMask: String? = null,
|
|
||||||
var security: String? = null,
|
|
||||||
var sni: String? = null,
|
|
||||||
var alpn: String? = null,
|
|
||||||
var fingerPrint: String? = null,
|
|
||||||
var insecure: Boolean? = null,
|
|
||||||
var echConfigList: String? = null,
|
|
||||||
var pinnedCA256: String? = null,
|
|
||||||
|
|
||||||
var publicKey: String? = null,
|
|
||||||
var shortId: String? = null,
|
|
||||||
var spiderX: String? = null,
|
|
||||||
var mldsa65Verify: 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,
|
|
||||||
|
|
||||||
var policyGroupType: String? = null,
|
|
||||||
var policyGroupSubscriptionId: String? = null,
|
|
||||||
var policyGroupFilter: String? = null,
|
|
||||||
var proxyChainProfiles: 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.kcpMtu == obj.kcpMtu
|
|
||||||
&& this.kcpTti == obj.kcpTti
|
|
||||||
&& 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.pinnedCA256 == obj.pinnedCA256
|
|
||||||
&& this.proxyChainProfiles == obj.proxyChainProfiles
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
sealed class RealPingEvent {
|
|
||||||
|
|
||||||
/** Periodic progress update while the batch is still running. */
|
|
||||||
data class Progress(val text: String) : RealPingEvent()
|
|
||||||
|
|
||||||
/** A single server result is available. */
|
|
||||||
data class Result(val guid: String, val delayMillis: Long) : RealPingEvent()
|
|
||||||
|
|
||||||
/** The entire batch has finished or been cancelled. */
|
|
||||||
data class Finish(val status: String) : RealPingEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class RulesetItem(
|
|
||||||
var remarks: String? = "",
|
|
||||||
var ip: List<String>? = null,
|
|
||||||
var domain: List<String>? = null,
|
|
||||||
var process: 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,
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ServersCache(
|
|
||||||
val guid: String,
|
|
||||||
val profile: ProfileItem
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class SubscriptionCache(
|
|
||||||
val guid: String,
|
|
||||||
val subscription: SubscriptionItem
|
|
||||||
)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class SubscriptionItem(
|
|
||||||
var remarks: String = "",
|
|
||||||
var url: String = "",
|
|
||||||
var enabled: Boolean = true,
|
|
||||||
val addedTime: Long = System.currentTimeMillis(),
|
|
||||||
var lastUpdated: Long = -1,
|
|
||||||
var autoUpdate: Boolean = false,
|
|
||||||
var updateInterval: Long = 1440, // in minutes, default to 24 hours
|
|
||||||
var prevProfile: String? = null,
|
|
||||||
var nextProfile: String? = null,
|
|
||||||
var filter: String? = null,
|
|
||||||
var allowInsecureUrl: Boolean = false,
|
|
||||||
var userAgent: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of subscription update operation
|
|
||||||
*/
|
|
||||||
data class SubscriptionUpdateResult(
|
|
||||||
val configCount: Int = 0, // Total configs updated
|
|
||||||
val successCount: Int = 0, // Subscriptions updated successfully
|
|
||||||
val failureCount: Int = 0, // Subscriptions failed to update
|
|
||||||
val skipCount: Int = 0 // Subscriptions skipped (disabled)
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Combine two results by adding their counts
|
|
||||||
*/
|
|
||||||
operator fun plus(other: SubscriptionUpdateResult): SubscriptionUpdateResult {
|
|
||||||
return SubscriptionUpdateResult(
|
|
||||||
configCount = this.configCount + other.configCount,
|
|
||||||
successCount = this.successCount + other.successCount,
|
|
||||||
failureCount = this.failureCount + other.failureCount,
|
|
||||||
skipCount = this.skipCount + other.skipCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
data class TestServiceMessage(
|
|
||||||
val key: Int,
|
|
||||||
val subscriptionId: String = "",
|
|
||||||
val serverGuids: List<String> = emptyList()
|
|
||||||
) : Serializable
|
|
||||||
|
|
||||||
@@ -1,526 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
|
|
||||||
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,
|
|
||||||
var settings: InSettingsBean? = null,
|
|
||||||
var sniffing: SniffingBean? = null,
|
|
||||||
val streamSettings: Any? = null,
|
|
||||||
val allocate: Any? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class InSettingsBean(
|
|
||||||
var auth: String? = null,
|
|
||||||
var udp: Boolean? = null,
|
|
||||||
var userLevel: Int? = null,
|
|
||||||
var accounts: List<SocksAccountBean>? = null,
|
|
||||||
var name: String? = null,
|
|
||||||
|
|
||||||
var mtu: Int? = null
|
|
||||||
) {
|
|
||||||
data class SocksAccountBean(
|
|
||||||
var user: String = "",
|
|
||||||
var pass: String = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 servers: List<ServersBean>? = null,
|
|
||||||
/*Blackhole*/
|
|
||||||
var response: Response? = null,
|
|
||||||
/*DNS*/
|
|
||||||
val network: String? = null,
|
|
||||||
var address: Any? = null,
|
|
||||||
var 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,
|
|
||||||
var version: Int? = 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 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 hysteriaSettings: HysteriaSettingsBean? = null,
|
|
||||||
var finalmask: Any? = 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
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
var happyEyeballs: HappyEyeballsBean? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class HappyEyeballsBean(
|
|
||||||
var prioritizeIPv6: Boolean? = null,
|
|
||||||
var maxConcurrentTry: Int? = 4,
|
|
||||||
var tryDelayMs: Int? = 250, // ms
|
|
||||||
var interleave: Int? = 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,
|
|
||||||
var echConfigList: String? = null,
|
|
||||||
var pinnedPeerCertSha256: String? = null,
|
|
||||||
// REALITY settings
|
|
||||||
val show: Boolean = false,
|
|
||||||
var publicKey: String? = null,
|
|
||||||
var shortId: String? = null,
|
|
||||||
var spiderX: String? = null,
|
|
||||||
var mldsa65Verify: 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 HysteriaSettingsBean(
|
|
||||||
var version: Int,
|
|
||||||
var auth: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
//https://xtls.github.io/config/transport.html#finalmaskobject
|
|
||||||
data class FinalMaskBean(
|
|
||||||
var tcp: List<MaskBean>? = null,
|
|
||||||
var udp: List<MaskBean>? = null,
|
|
||||||
var quicParams: QuicParamsBean? = null
|
|
||||||
) {
|
|
||||||
data class MaskBean(
|
|
||||||
var type: String,
|
|
||||||
var settings: MaskSettingsBean? = null
|
|
||||||
) {
|
|
||||||
data class MaskSettingsBean(
|
|
||||||
val password: String? = null,
|
|
||||||
val domain: String? = null,
|
|
||||||
// fragment
|
|
||||||
val packets: String? = null,
|
|
||||||
val length: String? = null,
|
|
||||||
val delay: String? = null,
|
|
||||||
// val maxSplit: String? = null,
|
|
||||||
// noise
|
|
||||||
val reset: Int? = null,
|
|
||||||
val noise: List<NoiseMaskBean>? = null
|
|
||||||
) {
|
|
||||||
data class NoiseMaskBean(
|
|
||||||
val rand: String? = null,
|
|
||||||
// val randRange: String? = null,
|
|
||||||
// val type: String? = null,
|
|
||||||
// val packet: String? = null,
|
|
||||||
val delay: String? = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class QuicParamsBean(
|
|
||||||
var congestion: String? = null,
|
|
||||||
var brutalUp: String? = null,
|
|
||||||
var brutalDown: String? = null,
|
|
||||||
var udpHop: UdpHopBean? = null,
|
|
||||||
) {
|
|
||||||
// Nested data class for the udpHop JSON object
|
|
||||||
data class UdpHopBean(
|
|
||||||
var ports: String? = null,
|
|
||||||
var interval: String? = 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)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.first()?.address
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
|
|
||||||
} else if (protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.address as String?
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.first()?.port
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
|
|
||||||
} else if (protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.port
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ensureSockopt(): StreamSettingsBean.SockoptBean {
|
|
||||||
val stream = streamSettings ?: StreamSettingsBean().also {
|
|
||||||
streamSettings = it
|
|
||||||
}
|
|
||||||
|
|
||||||
val sockopt = stream.sockopt ?: 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 enableParallelQuery: Boolean? = 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,
|
|
||||||
val tag: String? = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RoutingBean(
|
|
||||||
var domainStrategy: String,
|
|
||||||
var domainMatcher: String? = null,
|
|
||||||
var rules: ArrayList<RulesBean>,
|
|
||||||
var balancers: List<BalancerBean>? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class RulesBean(
|
|
||||||
var type: String = "field",
|
|
||||||
var ip: List<String>? = null,
|
|
||||||
var domain: List<String>? = null,
|
|
||||||
var process: List<String>? = null,
|
|
||||||
var outboundTag: String? = null,
|
|
||||||
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 BalancerBean(
|
|
||||||
val tag: String,
|
|
||||||
val selector: List<String>,
|
|
||||||
val fallbackTag: String? = null,
|
|
||||||
val strategy: StrategyObject? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class StrategyObject(
|
|
||||||
val type: String = "random", // "random" | "roundRobin" | "leastPing" | "leastLoad"
|
|
||||||
val settings: StrategySettingsObject? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class StrategySettingsObject(
|
|
||||||
val expected: Int? = null,
|
|
||||||
val maxRTT: String? = null,
|
|
||||||
val tolerance: Double? = null,
|
|
||||||
val baselines: List<String>? = null,
|
|
||||||
val costs: List<CostObject>? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class CostObject(
|
|
||||||
val regexp: Boolean = false,
|
|
||||||
val match: String,
|
|
||||||
val value: Double
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ObservatoryObject(
|
|
||||||
val subjectSelector: List<String>,
|
|
||||||
val probeUrl: String,
|
|
||||||
val probeInterval: String,
|
|
||||||
val enableConcurrency: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
data class BurstObservatoryObject(
|
|
||||||
val subjectSelector: List<String>,
|
|
||||||
val pingConfig: PingConfigObject
|
|
||||||
) {
|
|
||||||
data class PingConfigObject(
|
|
||||||
val destination: String,
|
|
||||||
val connectivity: String? = null,
|
|
||||||
val interval: String,
|
|
||||||
val sampling: Int,
|
|
||||||
val timeout: String? = 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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class VmessQRCode(
|
|
||||||
var v: String = "",
|
|
||||||
var ps: String = "",
|
|
||||||
var add: String = "",
|
|
||||||
var port: String = "",
|
|
||||||
var id: String = "",
|
|
||||||
var aid: String = "0",
|
|
||||||
var 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 = "",
|
|
||||||
var insecure: String = ""
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class WebDavConfig(
|
|
||||||
val baseUrl: String,
|
|
||||||
val username: String? = null,
|
|
||||||
val password: String? = null,
|
|
||||||
val remoteBasePath: String = "/",
|
|
||||||
val timeoutSeconds: Long = 30
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.v2ray.ang.enums
|
|
||||||
|
|
||||||
/** Runtime type used during config assembly only. */
|
|
||||||
enum class CoreResolvedType {
|
|
||||||
NORMAL,
|
|
||||||
POLICYGROUP,
|
|
||||||
PROXYCHAIN,
|
|
||||||
CUSTOM,
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.v2ray.ang.enums
|
|
||||||
|
|
||||||
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),
|
|
||||||
HYSTERIA(900, AppConfig.HYSTERIA),
|
|
||||||
HTTP(10, AppConfig.HTTP),
|
|
||||||
POLICYGROUP(101, AppConfig.CUSTOM),
|
|
||||||
PROXYCHAIN(102, AppConfig.CUSTOM);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.v2ray.ang.enums
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.v2ray.ang.enums
|
|
||||||
|
|
||||||
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"),
|
|
||||||
HYSTERIA("hysteria");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.v2ray.ang.enums
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum defining different notification channels.
|
|
||||||
* Each channel has a unique channelId, notificationId, and display name.
|
|
||||||
*/
|
|
||||||
enum class NotificationChannelType(
|
|
||||||
val channelId: String,
|
|
||||||
val channelName: String,
|
|
||||||
val notificationId: Int
|
|
||||||
) {
|
|
||||||
SUBSCRIPTION_UPDATE(
|
|
||||||
channelId = "subscription_update_channel",
|
|
||||||
channelName = "Subscription Update Service",
|
|
||||||
notificationId = 13
|
|
||||||
),
|
|
||||||
CORE_TEST(
|
|
||||||
channelId = "core_test_channel",
|
|
||||||
channelName = "Core Test Service",
|
|
||||||
notificationId = 12
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.v2ray.ang.enums
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Permission types used in the app, handling API level differences.
|
|
||||||
*/
|
|
||||||
enum class PermissionType {
|
|
||||||
/** Camera permission (used for scanning QR codes) */
|
|
||||||
CAMERA {
|
|
||||||
override fun getPermission(): String = Manifest.permission.CAMERA
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Read storage / media permission (adapts to Android version) */
|
|
||||||
READ_STORAGE {
|
|
||||||
override fun getPermission(): String {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
|
||||||
} else {
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Notification permission (Android 13+) */
|
|
||||||
POST_NOTIFICATIONS {
|
|
||||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
|
||||||
override fun getPermission(): String = Manifest.permission.POST_NOTIFICATIONS
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Return the actual Android permission string */
|
|
||||||
abstract fun getPermission(): String
|
|
||||||
|
|
||||||
/** Return a human-readable label for the permission */
|
|
||||||
fun getLabel(): String {
|
|
||||||
return when (this) {
|
|
||||||
CAMERA -> "Camera"
|
|
||||||
READ_STORAGE -> "Storage"
|
|
||||||
POST_NOTIFICATIONS -> "Notification"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.v2ray.ang.enums
|
|
||||||
|
|
||||||
enum class RoutingType(val fileName: String) {
|
|
||||||
WHITE("custom_routing_white"),
|
|
||||||
BLACK("custom_routing_black"),
|
|
||||||
GLOBAL("custom_routing_global"),
|
|
||||||
WHITE_IRAN("custom_routing_white_iran"),
|
|
||||||
WHITE_RUSSIA("custom_routing_white_russia");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromIndex(index: Int): RoutingType {
|
|
||||||
return when (index) {
|
|
||||||
0 -> WHITE
|
|
||||||
1 -> BLACK
|
|
||||||
2 -> GLOBAL
|
|
||||||
3 -> WHITE_IRAN
|
|
||||||
4 -> WHITE_RUSSIA
|
|
||||||
else -> WHITE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package com.v2ray.ang.enums
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 entries.toTypedArray().indices) {
|
|
||||||
entries[index]
|
|
||||||
} else {
|
|
||||||
OPTION_1 // Default to the first configuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
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 java.io.Serializable
|
|
||||||
import java.net.URI
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
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(Locale.getDefault(), "%.1f %s", size, units[unitIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
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(" ", "")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns null if the string is null or blank, otherwise returns the string itself.
|
|
||||||
*
|
|
||||||
* @return The string or null.
|
|
||||||
*/
|
|
||||||
fun String?.nullIfBlank(): String? = this?.takeIf { it.isNotBlank() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.isNullOrBlank()
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to match text either by Regex or literal string.
|
|
||||||
*/
|
|
||||||
fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean = true): Boolean {
|
|
||||||
if (keyword.isNullOrEmpty()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return regex?.containsMatchIn(this)
|
|
||||||
?: this.contains(keyword, ignoreCase = ignoreCase)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.NetworkType
|
|
||||||
import com.v2ray.ang.extension.nullIfBlank
|
|
||||||
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.encodeURIComponent(it.second) })
|
|
||||||
else ""
|
|
||||||
|
|
||||||
val url = String.format(
|
|
||||||
"%s@%s:%s",
|
|
||||||
Utils.encodeURIComponent(userInfo ?: ""),
|
|
||||||
Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
|
|
||||||
config.serverPort
|
|
||||||
)
|
|
||||||
|
|
||||||
return "${url}${query}#${Utils.encodeURIComponent(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.decodeURIComponent(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.kcpMtu = queryParam["mtu"]?.toIntOrNull()
|
|
||||||
config.kcpTti = queryParam["tti"]?.toIntOrNull()
|
|
||||||
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.finalMask = queryParam["fm"]
|
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
|
||||||
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
|
|
||||||
config.security = null
|
|
||||||
}
|
|
||||||
// Support multiple possible query keys for allowInsecure like the C# implementation
|
|
||||||
val allowInsecureKeys = arrayOf("insecure", "allowInsecure", "allow_insecure")
|
|
||||||
config.insecure = when {
|
|
||||||
allowInsecureKeys.any { queryParam[it] == "1" } -> true
|
|
||||||
allowInsecureKeys.any { queryParam[it] == "0" } -> false
|
|
||||||
else -> allowInsecure
|
|
||||||
}
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.fingerPrint = queryParam["fp"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
config.echConfigList = queryParam["ech"]
|
|
||||||
config.pinnedCA256 = queryParam["pcs"]
|
|
||||||
config.publicKey = queryParam["pbk"]
|
|
||||||
config.shortId = queryParam["sid"]
|
|
||||||
config.spiderX = queryParam["spx"]
|
|
||||||
config.mldsa65Verify = queryParam["pqv"]
|
|
||||||
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?.nullIfBlank()?.let { dicQuery["sni"] = it }
|
|
||||||
config.alpn?.nullIfBlank()?.let { dicQuery["alpn"] = it }
|
|
||||||
config.echConfigList?.nullIfBlank()?.let { dicQuery["ech"] = it }
|
|
||||||
config.pinnedCA256?.nullIfBlank()?.let { dicQuery["pcs"] = it }
|
|
||||||
config.fingerPrint?.nullIfBlank()?.let { dicQuery["fp"] = it }
|
|
||||||
config.publicKey?.nullIfBlank()?.let { dicQuery["pbk"] = it }
|
|
||||||
config.shortId?.nullIfBlank()?.let { dicQuery["sid"] = it }
|
|
||||||
config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it }
|
|
||||||
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
|
|
||||||
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
|
|
||||||
config.finalMask?.nullIfBlank()?.let { dicQuery["fm"] = it }
|
|
||||||
config.kcpMtu?.let { dicQuery["mtu"] = it.toString() }
|
|
||||||
config.kcpTti?.let { dicQuery["tti"] = it.toString() }
|
|
||||||
// Add two keys for compatibility: "insecure" and "allowInsecure"
|
|
||||||
if (config.security == AppConfig.TLS) {
|
|
||||||
val insecureFlag = if (config.insecure == true) "1" else "0"
|
|
||||||
dicQuery["insecure"] = insecureFlag
|
|
||||||
dicQuery["allowInsecure"] = insecureFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
val networkType = NetworkType.fromString(config.network)
|
|
||||||
dicQuery["type"] = networkType.type
|
|
||||||
|
|
||||||
when (networkType) {
|
|
||||||
NetworkType.TCP -> {
|
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.host?.nullIfBlank()?.let { dicQuery["host"] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP -> {
|
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.seed?.nullIfBlank()?.let { dicQuery["seed"] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
|
|
||||||
config.host?.nullIfBlank()?.let { dicQuery["host"] = it }
|
|
||||||
config.path?.nullIfBlank()?.let { dicQuery["path"] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.XHTTP -> {
|
|
||||||
config.host?.nullIfBlank()?.let { dicQuery["host"] = it }
|
|
||||||
config.path?.nullIfBlank()?.let { dicQuery["path"] = it }
|
|
||||||
config.xhttpMode?.nullIfBlank()?.let { dicQuery["mode"] = it }
|
|
||||||
config.xhttpExtra?.nullIfBlank()?.let { dicQuery["extra"] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HTTP, NetworkType.H2 -> {
|
|
||||||
dicQuery["type"] = "http"
|
|
||||||
config.host?.nullIfBlank()?.let { dicQuery["host"] = it }
|
|
||||||
config.path?.nullIfBlank()?.let { dicQuery["path"] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkType.QUIC -> {
|
|
||||||
// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
// config.quicSecurity?.nullIfBlank()?.let { dicQuery["quicSecurity"] = it }
|
|
||||||
// config.quicKey?.nullIfBlank()?.let { dicQuery["key"] = it }
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC -> {
|
|
||||||
config.mode?.nullIfBlank()?.let { dicQuery["mode"] = it }
|
|
||||||
config.authority?.nullIfBlank()?.let { dicQuery["authority"] = it }
|
|
||||||
config.serviceName?.nullIfBlank()?.let { dicQuery["serviceName"] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dicQuery
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.enums.NetworkType
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.extension.nullIfBlank
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
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.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
config.security = AppConfig.TLS
|
|
||||||
config.network = NetworkType.HYSTERIA.type
|
|
||||||
|
|
||||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
getItemFormQuery(config, queryParam, allowInsecure)
|
|
||||||
|
|
||||||
config.security = queryParam["security"] ?: AppConfig.TLS
|
|
||||||
config.obfsPassword = queryParam["obfs-password"]
|
|
||||||
config.portHopping = queryParam["mport"]
|
|
||||||
if (config.portHopping.isNotNullEmpty()) {
|
|
||||||
config.portHoppingInterval = queryParam["mportHopInt"]
|
|
||||||
}
|
|
||||||
config.pinnedCA256 = 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?.nullIfBlank()?.let { dicQuery["sni"] = it }
|
|
||||||
config.alpn?.nullIfBlank()?.let { dicQuery["alpn"] = it }
|
|
||||||
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.portHoppingInterval.isNotNullEmpty()) {
|
|
||||||
val rawInterval = config.portHoppingInterval?.trim().nullIfBlank()
|
|
||||||
val interval = if (rawInterval == null) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
val singleValue = rawInterval.toIntOrNull()
|
|
||||||
if (singleValue != null) {
|
|
||||||
if (singleValue < 5) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
rawInterval
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val parts = rawInterval.split('-')
|
|
||||||
if (parts.size == 2) {
|
|
||||||
val start = parts[0].trim().toIntOrNull()
|
|
||||||
val end = parts[1].trim().toIntOrNull()
|
|
||||||
if (start != null && end != null) {
|
|
||||||
val minStart = maxOf(5, start)
|
|
||||||
val minEnd = maxOf(minStart, end)
|
|
||||||
(minStart + minEnd) / 2
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (interval != null) {
|
|
||||||
dicQuery["mportHopInt"] = interval.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (config.pinnedCA256.isNotNullEmpty()) {
|
|
||||||
dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.enums.NetworkType
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.util.LogUtil
|
|
||||||
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.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
|
||||||
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.decodeURIComponent(result.substring(indexSplit + 1, result.length))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.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, true), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
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.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
|
||||||
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, true), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.enums.NetworkType
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
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.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
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.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.VmessQRCode
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.enums.NetworkType
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.nullIfBlank
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.LogUtil
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
val 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)) {
|
|
||||||
LogUtil.w(AppConfig.TAG, "Toast decoding failed")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java) ?: return null
|
|
||||||
// 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)
|
|
||||||
) {
|
|
||||||
LogUtil.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
|
|
||||||
if (config.network.isNullOrEmpty()) {
|
|
||||||
config.network = 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.sni = vmessQRCode.sni
|
|
||||||
config.fingerPrint = vmessQRCode.fp
|
|
||||||
config.alpn = vmessQRCode.alpn
|
|
||||||
config.insecure = when (vmessQRCode.insecure) {
|
|
||||||
"1" -> true
|
|
||||||
"0" -> false
|
|
||||||
else -> 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 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?.nullIfBlank()?.let { vmessQRCode.host = it }
|
|
||||||
config.path?.nullIfBlank()?.let { vmessQRCode.path = it }
|
|
||||||
|
|
||||||
vmessQRCode.tls = config.security.orEmpty()
|
|
||||||
vmessQRCode.sni = config.sni.orEmpty()
|
|
||||||
vmessQRCode.fp = config.fingerPrint.orEmpty()
|
|
||||||
vmessQRCode.alpn = config.alpn.orEmpty()
|
|
||||||
vmessQRCode.insecure = when (config.insecure) {
|
|
||||||
true -> "1"
|
|
||||||
false -> "0"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
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.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
config.method = AppConfig.DEFAULT_SECURITY
|
|
||||||
|
|
||||||
getItemFormQuery(config, queryParam, allowInsecure)
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.nullIfBlank
|
|
||||||
import com.v2ray.ang.extension.removeWhiteSpace
|
|
||||||
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.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
|
|
||||||
config.secretKey = uri.userInfo.orEmpty()
|
|
||||||
config.localAddress = queryParam["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
config.publicKey = queryParam["publickey"].orEmpty()
|
|
||||||
config.preSharedKey = queryParam["presharedkey"]?.nullIfBlank()
|
|
||||||
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"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
|
||||||
config.publicKey = peerParams["publickey"].orEmpty()
|
|
||||||
config.preSharedKey = peerParams["presharedkey"]?.nullIfBlank()
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,634 +0,0 @@
|
|||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.HY2
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.core.CoreConfigManager
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.SubscriptionCache
|
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
|
||||||
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
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.LogUtil
|
|
||||||
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) {
|
|
||||||
LogUtil.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() - 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.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) {
|
|
||||||
LogUtil.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 = CoreConfigManager.getV2rayConfig(context, guid)
|
|
||||||
if (result.status) {
|
|
||||||
Utils.setClipboard(context, result.content)
|
|
||||||
} else {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.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.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
|
|
||||||
EConfigType.SOCKS -> SocksFmt.toUri(config)
|
|
||||||
EConfigType.VLESS -> VlessFmt.toUri(config)
|
|
||||||
EConfigType.TROJAN -> TrojanFmt.toUri(config)
|
|
||||||
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
|
||||||
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.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, append)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
LogUtil.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
|
|
||||||
}
|
|
||||||
// Find the currently selected server that matches the subscription ID
|
|
||||||
val removedSelected = if (subid.isNotBlank() && !append) {
|
|
||||||
MmkvManager.getSelectServer()
|
|
||||||
.takeIf { it?.isNotBlank() == true }
|
|
||||||
?.let { MmkvManager.decodeServerConfig(it) }
|
|
||||||
?.takeIf { it.subscriptionId == subid }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
|
||||||
|
|
||||||
// Parse all configs first (no I/O during parsing)
|
|
||||||
val configs = mutableListOf<ProfileItem>()
|
|
||||||
servers.lines()
|
|
||||||
.distinct()
|
|
||||||
.reversed()
|
|
||||||
.forEach {
|
|
||||||
val config = parseConfig(it, subid, subItem)
|
|
||||||
if (config != null) {
|
|
||||||
configs.add(config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch save all parsed configs (only one serverList read/write)
|
|
||||||
if (configs.isNotEmpty()) {
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
val keyToProfile = batchSaveConfigs(configs, subid)
|
|
||||||
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
|
||||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return configs.size
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to parse batch config", e)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch save configurations to reduce serverList read/write operations.
|
|
||||||
* Reads serverList once, saves all configs, then writes serverList once.
|
|
||||||
*
|
|
||||||
* @param configs The list of ProfileItem to save.
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
* @return Map of generated keys to their corresponding ProfileItem.
|
|
||||||
*/
|
|
||||||
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): Map<String, ProfileItem> {
|
|
||||||
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
|
||||||
|
|
||||||
// Read serverList once
|
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
|
||||||
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
|
||||||
|
|
||||||
configs.forEach { config ->
|
|
||||||
val key = Utils.getUuid()
|
|
||||||
// Save profile directly without updating serverList
|
|
||||||
MmkvManager.encodeProfileDirect(key, JsonUtil.toJson(config))
|
|
||||||
|
|
||||||
if (!serverList.contains(key)) {
|
|
||||||
serverList.add(0, key)
|
|
||||||
if (needSetSelected) {
|
|
||||||
MmkvManager.setSelectServer(key)
|
|
||||||
needSetSelected = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keyToProfile[key] = config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write serverList once
|
|
||||||
MmkvManager.encodeServerList(serverList, subid)
|
|
||||||
return keyToProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a matched profile key from the given key-profile map using multi-level matching.
|
|
||||||
* Matching priority (from highest to lowest):
|
|
||||||
* 1. Exact match: server + port + password
|
|
||||||
* 2. Match by remarks (exact match)
|
|
||||||
* 3. Match by server + port
|
|
||||||
* 4. Match by server only
|
|
||||||
*
|
|
||||||
* @param keyToProfile Map of server keys to their ProfileItem
|
|
||||||
* @param target Target profile to match
|
|
||||||
* @return Matched key or null
|
|
||||||
*/
|
|
||||||
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
|
|
||||||
if (keyToProfile.isEmpty() || target == null) return null
|
|
||||||
|
|
||||||
// Level 1: Match by remarks
|
|
||||||
if (target.remarks.isNotBlank()) {
|
|
||||||
keyToProfile.entries.firstOrNull { (_, saved) ->
|
|
||||||
isSameText(saved.remarks, target.remarks)
|
|
||||||
}?.key?.let { return it }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Level 2: Exact match (server + port + password)
|
|
||||||
keyToProfile.entries.firstOrNull { (_, saved) ->
|
|
||||||
isSameText(saved.server, target.server) &&
|
|
||||||
isSameText(saved.serverPort, target.serverPort) &&
|
|
||||||
isSameText(saved.password, target.password)
|
|
||||||
}?.key?.let { return it }
|
|
||||||
|
|
||||||
// Level 3: Match by server + port
|
|
||||||
keyToProfile.entries.firstOrNull { (_, saved) ->
|
|
||||||
isSameText(saved.server, target.server) &&
|
|
||||||
isSameText(saved.serverPort, target.serverPort)
|
|
||||||
}?.key?.let { return it }
|
|
||||||
|
|
||||||
// Level 4: Match by server only
|
|
||||||
keyToProfile.entries.firstOrNull { (_, saved) ->
|
|
||||||
isSameText(saved.server, target.server)
|
|
||||||
}?.key?.let { return it }
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Case-insensitive trimmed string comparison.
|
|
||||||
*
|
|
||||||
* @param left First string
|
|
||||||
* @param right Second string
|
|
||||||
* @return True if both are non-empty and equal (case-insensitive, trimmed)
|
|
||||||
*/
|
|
||||||
private fun isSameText(left: String?, right: String?): Boolean {
|
|
||||||
if (left.isNullOrBlank() || right.isNullOrBlank()) return false
|
|
||||||
return left.trim().equals(right.trim(), ignoreCase = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a custom configuration server.
|
|
||||||
*
|
|
||||||
* @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 parseCustomConfigServer(server: String?, subid: String, append: Boolean): 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) ?: arrayOf()
|
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
var count = 0
|
|
||||||
for (srv in serverList.reversed()) {
|
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
|
||||||
config.subscriptionId = subid
|
|
||||||
config.description = generateDescription(config)
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.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
|
|
||||||
config.description = generateDescription(config)
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.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
|
|
||||||
config.description = generateDescription(config)
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the configuration from a QR code or string.
|
|
||||||
* Only parses and returns ProfileItem, does not save.
|
|
||||||
*
|
|
||||||
* @param str The configuration string.
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
* @param subItem The subscription item.
|
|
||||||
* @return The parsed ProfileItem or null if parsing fails or filtered out.
|
|
||||||
*/
|
|
||||||
private fun parseConfig(
|
|
||||||
str: String?,
|
|
||||||
subid: String,
|
|
||||||
subItem: SubscriptionItem?
|
|
||||||
): ProfileItem? {
|
|
||||||
try {
|
|
||||||
if (str == null || TextUtils.isEmpty(str)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
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 null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply filter
|
|
||||||
if (subItem?.filter.isNotNullEmpty() && config.remarks.isNotNullEmpty()) {
|
|
||||||
val matched = Regex(pattern = subItem?.filter.orEmpty())
|
|
||||||
.containsMatchIn(input = config.remarks)
|
|
||||||
if (!matched) return null
|
|
||||||
}
|
|
||||||
|
|
||||||
config.subscriptionId = subid
|
|
||||||
config.description = generateDescription(config)
|
|
||||||
|
|
||||||
return config
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to parse config", e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the configuration via all subscriptions.
|
|
||||||
*
|
|
||||||
* @return Detailed result of the subscription update operation.
|
|
||||||
*/
|
|
||||||
fun updateConfigViaSubAll(): SubscriptionUpdateResult {
|
|
||||||
return try {
|
|
||||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
|
||||||
subscriptions.fold(SubscriptionUpdateResult()) { acc, subscription ->
|
|
||||||
acc + updateConfigViaSub(subscription)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
|
|
||||||
SubscriptionUpdateResult()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the configuration via a subscription.
|
|
||||||
*
|
|
||||||
* @param it The subscription item.
|
|
||||||
* @return Subscription update result.
|
|
||||||
*/
|
|
||||||
fun updateConfigViaSub(it: SubscriptionCache): SubscriptionUpdateResult {
|
|
||||||
try {
|
|
||||||
// Check if disabled
|
|
||||||
if (!it.subscription.enabled) {
|
|
||||||
return SubscriptionUpdateResult(skipCount = 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate subscription info
|
|
||||||
if (TextUtils.isEmpty(it.guid)
|
|
||||||
|| TextUtils.isEmpty(it.subscription.remarks)
|
|
||||||
|| TextUtils.isEmpty(it.subscription.url)
|
|
||||||
) {
|
|
||||||
return SubscriptionUpdateResult(skipCount = 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = HttpUtil.toIdnUrl(it.subscription.url)
|
|
||||||
if (!Utils.isValidUrl(url)) {
|
|
||||||
return SubscriptionUpdateResult(failureCount = 1)
|
|
||||||
}
|
|
||||||
if (!it.subscription.allowInsecureUrl) {
|
|
||||||
if (!Utils.isValidSubUrl(url)) {
|
|
||||||
return SubscriptionUpdateResult(failureCount = 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LogUtil.i(AppConfig.TAG, url)
|
|
||||||
val userAgent = it.subscription.userAgent
|
|
||||||
val proxyUsername = SettingsManager.getSocksUsername()
|
|
||||||
val proxyPassword = SettingsManager.getSocksPassword()
|
|
||||||
|
|
||||||
var configText = try {
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort, proxyUsername, proxyPassword)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
if (configText.isEmpty()) {
|
|
||||||
configText = try {
|
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (configText.isEmpty()) {
|
|
||||||
return SubscriptionUpdateResult(failureCount = 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val count = parseConfigViaSub(configText, it.guid, false)
|
|
||||||
if (count > 0) {
|
|
||||||
it.subscription.lastUpdated = System.currentTimeMillis()
|
|
||||||
MmkvManager.encodeSubscription(it.guid, it.subscription)
|
|
||||||
LogUtil.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
|
|
||||||
return SubscriptionUpdateResult(
|
|
||||||
configCount = count,
|
|
||||||
successCount = 1
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Got response but no valid configs parsed
|
|
||||||
return SubscriptionUpdateResult(failureCount = 1)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogUtil.e(AppConfig.TAG, "Failed to update config via subscription", e)
|
|
||||||
return SubscriptionUpdateResult(failureCount = 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, append)
|
|
||||||
}
|
|
||||||
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.subscription.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
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generates a description for the profile.
|
|
||||||
*
|
|
||||||
* @param profile The profile item.
|
|
||||||
* @return The generated description.
|
|
||||||
*/
|
|
||||||
fun generateDescription(profile: ProfileItem): String {
|
|
||||||
// Hide xxx:xxx:***/xxx.xxx.xxx.***
|
|
||||||
val server = profile.server
|
|
||||||
val port = profile.serverPort
|
|
||||||
if (server.isNullOrBlank() && port.isNullOrBlank()) return ""
|
|
||||||
|
|
||||||
val addrPart = server?.let {
|
|
||||||
if (it.contains(":"))
|
|
||||||
it.split(":").take(2).joinToString(":", postfix = ":***")
|
|
||||||
else
|
|
||||||
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
|
|
||||||
} ?: ""
|
|
||||||
|
|
||||||
return "$addrPart : ${port ?: ""}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,721 +0,0 @@
|
|||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig.DEFAULT_SUBSCRIPTION_ID
|
|
||||||
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
|
|
||||||
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
|
|
||||||
import com.v2ray.ang.dto.AssetUrlCache
|
|
||||||
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.SubscriptionCache
|
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
|
||||||
import com.v2ray.ang.dto.WebDavConfig
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
object MmkvManager {
|
|
||||||
|
|
||||||
//region private
|
|
||||||
|
|
||||||
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_SERVER_PREFIX = "SUB_SERVERS_"
|
|
||||||
private const val KEY_SUB_IDS = "SUB_IDS"
|
|
||||||
private const val KEY_WEBDAV_CONFIG = "WEBDAV_CONFIG"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the legacy server list from KEY_ANG_CONFIGS for migration.
|
|
||||||
* This method is for migration purposes only.
|
|
||||||
*
|
|
||||||
* @return The JSON string of legacy server list, or null if not exists.
|
|
||||||
*/
|
|
||||||
fun readLegacyServerList(): String? {
|
|
||||||
return mainStorage.decodeString(KEY_ANG_CONFIGS)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 for a given subscription.
|
|
||||||
* Saves to the subscription's serverList (including default subscription for ungrouped servers).
|
|
||||||
*
|
|
||||||
* @param serverList The list of server GUIDs.
|
|
||||||
* @param subscriptionId The subscription ID.
|
|
||||||
*/
|
|
||||||
fun encodeServerList(serverList: MutableList<String>, subscriptionId: String) {
|
|
||||||
val subId = getSubscriptionId(subscriptionId)
|
|
||||||
val key = "$KEY_SUB_SERVER_PREFIX$subId"
|
|
||||||
mainStorage.encode(key, JsonUtil.toJson(serverList))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the server list for a given subscription.
|
|
||||||
* If subscriptionId is empty, returns ungrouped servers.
|
|
||||||
* Otherwise, returns servers from the specified subscription's serverList.
|
|
||||||
*
|
|
||||||
* @param subscriptionId The subscription ID.
|
|
||||||
* @return The list of server GUIDs.
|
|
||||||
*/
|
|
||||||
fun decodeServerList(subscriptionId: String): MutableList<String> {
|
|
||||||
val subId = getSubscriptionId(subscriptionId)
|
|
||||||
val key = "$KEY_SUB_SERVER_PREFIX$subId"
|
|
||||||
val json = mainStorage.decodeString(key)
|
|
||||||
return if (json.isNullOrBlank()) {
|
|
||||||
mutableListOf()
|
|
||||||
} else {
|
|
||||||
JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes all server list (merged from all subscriptions including default subscription).
|
|
||||||
* Use this when you need the complete server list.
|
|
||||||
*
|
|
||||||
* @return The list of all server GUIDs.
|
|
||||||
*/
|
|
||||||
fun decodeAllServerList(): MutableList<String> {
|
|
||||||
val allServers = mutableListOf<String>()
|
|
||||||
val subsList = decodeSubsList()
|
|
||||||
|
|
||||||
// If DEFAULT_SUBSCRIPTION_ID is not in the subscriptions list, add its servers
|
|
||||||
if (!subsList.contains(DEFAULT_SUBSCRIPTION_ID)) {
|
|
||||||
allServers.addAll(decodeServerList(DEFAULT_SUBSCRIPTION_ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add servers from all subscriptions
|
|
||||||
subsList.forEach { guid ->
|
|
||||||
allServers.addAll(decodeServerList(guid))
|
|
||||||
}
|
|
||||||
|
|
||||||
return allServers
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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))
|
|
||||||
|
|
||||||
// Use default subscription for servers without subscription
|
|
||||||
val subId = getSubscriptionId(config.subscriptionId)
|
|
||||||
val serverList = decodeServerList(subId)
|
|
||||||
|
|
||||||
if (!serverList.contains(key)) {
|
|
||||||
serverList.add(0, key)
|
|
||||||
encodeServerList(serverList, subId)
|
|
||||||
if (getSelectServer().isNullOrBlank()) {
|
|
||||||
mainStorage.encode(KEY_SELECTED_SERVER, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the server configuration directly without updating serverList.
|
|
||||||
*
|
|
||||||
* @param key The server GUID.
|
|
||||||
* @param configJson The server configuration JSON string.
|
|
||||||
*/
|
|
||||||
fun encodeProfileDirect(key: String, configJson: String) {
|
|
||||||
profileFullStorage.encode(key, configJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the server configuration.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
*/
|
|
||||||
fun removeServer(guid: String) {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get config to determine which subscription to update
|
|
||||||
val config = decodeServerConfig(guid)
|
|
||||||
val subId = getSubscriptionId(config?.subscriptionId)
|
|
||||||
|
|
||||||
// Remove from appropriate server list
|
|
||||||
val serverList = decodeServerList(subId)
|
|
||||||
serverList.remove(guid)
|
|
||||||
encodeServerList(serverList, subId)
|
|
||||||
|
|
||||||
// Clean up storage
|
|
||||||
if (getSelectServer() == guid) {
|
|
||||||
mainStorage.remove(KEY_SELECTED_SERVER)
|
|
||||||
}
|
|
||||||
profileFullStorage.remove(guid)
|
|
||||||
serverAffStorage.remove(guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the server configurations via subscription ID.
|
|
||||||
*
|
|
||||||
* @param subscriptionId The subscription ID.
|
|
||||||
*/
|
|
||||||
fun removeServerViaSubid(subscriptionId: String?) {
|
|
||||||
val subId = getSubscriptionId(subscriptionId)
|
|
||||||
val serverList = decodeServerList(subId)
|
|
||||||
|
|
||||||
// Remove all servers in the list
|
|
||||||
serverList.forEach { guid ->
|
|
||||||
if (getSelectServer() == guid) {
|
|
||||||
mainStorage.remove(KEY_SELECTED_SERVER)
|
|
||||||
}
|
|
||||||
profileFullStorage.remove(guid)
|
|
||||||
serverAffStorage.remove(guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
serverList.clear()
|
|
||||||
encodeServerList(serverList, subId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()
|
|
||||||
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
|
|
||||||
|
|
||||||
private fun getSubscriptionId(subscriptionId: String?): String {
|
|
||||||
return subscriptionId?.ifEmpty { DEFAULT_SUBSCRIPTION_ID } ?: DEFAULT_SUBSCRIPTION_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<SubscriptionCache> {
|
|
||||||
initSubsList()
|
|
||||||
|
|
||||||
val subscriptions = mutableListOf<SubscriptionCache>()
|
|
||||||
decodeSubsList().forEach { key ->
|
|
||||||
val json = subStorage.decodeString(key)
|
|
||||||
if (!json.isNullOrBlank()) {
|
|
||||||
val item = JsonUtil.fromJson(json, SubscriptionItem::class.java) ?: SubscriptionItem()
|
|
||||||
subscriptions.add(SubscriptionCache(key, item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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() ?: mutableListOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Asset
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the asset URLs.
|
|
||||||
*
|
|
||||||
* @return The list of asset URLs.
|
|
||||||
*/
|
|
||||||
fun decodeAssetUrls(): List<AssetUrlCache> {
|
|
||||||
val assetUrlItems = mutableListOf<AssetUrlCache>()
|
|
||||||
assetStorage.allKeys()?.forEach { key ->
|
|
||||||
val json = assetStorage.decodeString(key)
|
|
||||||
if (!json.isNullOrBlank()) {
|
|
||||||
val item = JsonUtil.fromJson(json, AssetUrlItem::class.java) ?: AssetUrlItem()
|
|
||||||
assetUrlItems.add(AssetUrlCache(key, item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return assetUrlItems.sortedBy { it.assetUrl.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() ?: mutableListOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
|
|
||||||
//region settings
|
|
||||||
/**
|
|
||||||
* 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: Long): 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: Float): 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 integer.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param defaultValue The default value.
|
|
||||||
* @return The settings value.
|
|
||||||
*/
|
|
||||||
fun decodeSettingsInt(key: String, defaultValue: Int): Int {
|
|
||||||
return settingsStorage.decodeInt(key, defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the settings long.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param defaultValue The default value.
|
|
||||||
* @return The settings value.
|
|
||||||
*/
|
|
||||||
fun decodeSettingsLong(key: String, defaultValue: Long): Long {
|
|
||||||
return settingsStorage.decodeLong(key, defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the settings float.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param defaultValue The default value.
|
|
||||||
* @return The settings value.
|
|
||||||
*/
|
|
||||||
fun decodeSettingsFloat(key: String, defaultValue: Float): Float {
|
|
||||||
return settingsStorage.decodeFloat(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
|
|
||||||
//region WebDAV
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the WebDAV config as JSON into storage.
|
|
||||||
*/
|
|
||||||
fun encodeWebDavConfig(config: WebDavConfig): Boolean {
|
|
||||||
return mainStorage.encode(KEY_WEBDAV_CONFIG, JsonUtil.toJson(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the WebDAV config from storage.
|
|
||||||
*/
|
|
||||||
fun decodeWebDavConfig(): WebDavConfig? {
|
|
||||||
val json = mainStorage.decodeString(KEY_WEBDAV_CONFIG) ?: return null
|
|
||||||
return JsonUtil.fromJson(json, WebDavConfig::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.core.CoreServiceManager
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.extension.toSpeedString
|
|
||||||
import com.v2ray.ang.ui.MainActivity
|
|
||||||
import com.v2ray.ang.util.LogUtil
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
object NotificationManager {
|
|
||||||
private const val NOTIFICATION_ID = 1
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
|
|
||||||
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
|
||||||
private const val QUERY_INTERVAL_MS = 3000L
|
|
||||||
|
|
||||||
private var lastQueryTime = 0L
|
|
||||||
private var mBuilder: NotificationCompat.Builder? = null
|
|
||||||
private var speedNotificationJob: Job? = null
|
|
||||||
private var mNotificationManager: NotificationManager? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the speed notification.
|
|
||||||
* @param currentConfig The current profile configuration.
|
|
||||||
*/
|
|
||||||
fun startSpeedNotification(currentConfig: ProfileItem?) {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
|
|
||||||
if (speedNotificationJob != null || CoreServiceManager.isRunning() == false) return
|
|
||||||
|
|
||||||
var lastZeroSpeed = false
|
|
||||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
|
||||||
outboundTags?.remove(AppConfig.TAG_DIRECT)
|
|
||||||
|
|
||||||
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
while (isActive) {
|
|
||||||
val queryTime = System.currentTimeMillis()
|
|
||||||
val sinceLastQueryIn = (queryTime - lastQueryTime)
|
|
||||||
|
|
||||||
// If the query interval is too short, skip this round to avoid excessive CPU usage
|
|
||||||
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
|
|
||||||
LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
|
|
||||||
lastQueryTime = queryTime
|
|
||||||
delay(QUERY_INTERVAL_MS)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val sinceLastQueryInSeconds = sinceLastQueryIn / 1000.0
|
|
||||||
|
|
||||||
var proxyTotal = 0L
|
|
||||||
val text = StringBuilder()
|
|
||||||
outboundTags?.forEach {
|
|
||||||
val up = CoreServiceManager.queryStats(it, AppConfig.UPLINK)
|
|
||||||
val down = CoreServiceManager.queryStats(it, AppConfig.DOWNLINK)
|
|
||||||
if (up + down > 0) {
|
|
||||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
|
||||||
proxyTotal += up + down
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val directUplink = CoreServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.UPLINK)
|
|
||||||
val directDownlink = CoreServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.DOWNLINK)
|
|
||||||
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
|
||||||
if (!zeroSpeed || !lastZeroSpeed) {
|
|
||||||
if (proxyTotal == 0L) {
|
|
||||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
|
||||||
}
|
|
||||||
appendSpeedString(
|
|
||||||
text, AppConfig.TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
|
||||||
directDownlink / sinceLastQueryInSeconds
|
|
||||||
)
|
|
||||||
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
|
||||||
}
|
|
||||||
lastZeroSpeed = zeroSpeed
|
|
||||||
lastQueryTime = queryTime
|
|
||||||
delay(QUERY_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the notification.
|
|
||||||
* @param currentConfig The current profile configuration.
|
|
||||||
*/
|
|
||||||
fun showNotification(currentConfig: ProfileItem?) {
|
|
||||||
val service = getService() ?: return
|
|
||||||
|
|
||||||
// Reset last query time to avoid querying stats too soon after showing the notification
|
|
||||||
lastQueryTime = System.currentTimeMillis()
|
|
||||||
|
|
||||||
val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
|
|
||||||
val startMainIntent = Intent(service, MainActivity::class.java)
|
|
||||||
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
|
|
||||||
|
|
||||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
stopV2RayIntent.`package` = AppConfig.ANG_PACKAGE
|
|
||||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
|
||||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
|
|
||||||
|
|
||||||
val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
restartV2RayIntent.`package` = AppConfig.ANG_PACKAGE
|
|
||||||
restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
|
|
||||||
val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
|
|
||||||
|
|
||||||
val channelId =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
createNotificationChannel()
|
|
||||||
} else {
|
|
||||||
// If earlier version channel ID is not used
|
|
||||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
mBuilder = NotificationCompat.Builder(service, channelId)
|
|
||||||
.setSmallIcon(R.drawable.ic_stat_name)
|
|
||||||
.setContentTitle(currentConfig?.remarks)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setContentIntent(contentPendingIntent)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_delete_24dp,
|
|
||||||
service.getString(R.string.notification_action_stop_v2ray),
|
|
||||||
stopV2RayPendingIntent
|
|
||||||
)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_delete_24dp,
|
|
||||||
service.getString(R.string.title_service_restart),
|
|
||||||
restartV2RayPendingIntent
|
|
||||||
)
|
|
||||||
|
|
||||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
|
|
||||||
|
|
||||||
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the notification.
|
|
||||||
*/
|
|
||||||
fun cancelNotification() {
|
|
||||||
val service = getService() ?: return
|
|
||||||
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
|
||||||
|
|
||||||
mBuilder = null
|
|
||||||
speedNotificationJob?.cancel()
|
|
||||||
speedNotificationJob = null
|
|
||||||
mNotificationManager = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the speed notification.
|
|
||||||
* @param currentConfig The current profile configuration.
|
|
||||||
*/
|
|
||||||
fun stopSpeedNotification(currentConfig: ProfileItem?) {
|
|
||||||
speedNotificationJob?.let {
|
|
||||||
it.cancel()
|
|
||||||
speedNotificationJob = null
|
|
||||||
updateNotification(currentConfig?.remarks, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a notification channel for Android O and above.
|
|
||||||
* @return The channel ID.
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private fun createNotificationChannel(): String {
|
|
||||||
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
|
||||||
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
|
||||||
val chan = NotificationChannel(
|
|
||||||
channelId,
|
|
||||||
channelName, NotificationManager.IMPORTANCE_HIGH
|
|
||||||
)
|
|
||||||
chan.lightColor = Color.DKGRAY
|
|
||||||
chan.importance = NotificationManager.IMPORTANCE_NONE
|
|
||||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
|
||||||
getNotificationManager()?.createNotificationChannel(chan)
|
|
||||||
return channelId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the notification with the given content text and traffic data.
|
|
||||||
* @param contentText The content text.
|
|
||||||
* @param proxyTraffic The proxy traffic.
|
|
||||||
* @param directTraffic The direct traffic.
|
|
||||||
*/
|
|
||||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
|
||||||
if (mBuilder != null) {
|
|
||||||
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
|
|
||||||
} else if (proxyTraffic > directTraffic) {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
|
|
||||||
} else {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
|
|
||||||
}
|
|
||||||
mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
|
|
||||||
mBuilder?.setContentText(contentText)
|
|
||||||
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the notification manager.
|
|
||||||
* @return The notification manager.
|
|
||||||
*/
|
|
||||||
private fun getNotificationManager(): NotificationManager? {
|
|
||||||
if (mNotificationManager == null) {
|
|
||||||
val service = getService() ?: return null
|
|
||||||
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
}
|
|
||||||
return mNotificationManager
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends the speed string to the given text.
|
|
||||||
* @param text The text to append to.
|
|
||||||
* @param name The name of the tag.
|
|
||||||
* @param up The uplink speed.
|
|
||||||
* @param down The downlink speed.
|
|
||||||
*/
|
|
||||||
private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
|
|
||||||
var n = name ?: "no tag"
|
|
||||||
n = n.take(min(n.length, 6))
|
|
||||||
text.append(n)
|
|
||||||
for (i in n.length..6 step 2) {
|
|
||||||
text.append("\t")
|
|
||||||
}
|
|
||||||
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the service instance.
|
|
||||||
* @return The service instance.
|
|
||||||
*/
|
|
||||||
private fun getService(): Service? {
|
|
||||||
return CoreServiceManager.serviceControl?.get()?.getService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
|
|
||||||
object SettingsChangeManager {
|
|
||||||
private val _restartService = MutableStateFlow(false)
|
|
||||||
private val _setupGroupTab = MutableStateFlow(false)
|
|
||||||
|
|
||||||
// Mark restartService as requiring a restart
|
|
||||||
fun makeRestartService() {
|
|
||||||
_restartService.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and clear the restartService flag
|
|
||||||
fun consumeRestartService(): Boolean {
|
|
||||||
val v = _restartService.value
|
|
||||||
_restartService.value = false
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark reinitGroupTab as requiring tab reinitialization
|
|
||||||
fun makeSetupGroupTab() {
|
|
||||||
_setupGroupTab.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and clear the reinitGroupTab flag
|
|
||||||
fun consumeSetupGroupTab(): Boolean {
|
|
||||||
val v = _setupGroupTab.value
|
|
||||||
_setupGroupTab.value = false
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user