mirror of
https://github.com/2dust/v2rayNG.git
synced 2025-07-01 22:50:06 +00:00
Compare commits
No commits in common. "master" and "1.8.15" have entirely different histories.
357 changed files with 11085 additions and 22198 deletions
177
.github/workflows/build.yml
vendored
177
.github/workflows/build.yml
vendored
|
@ -1,159 +1,64 @@
|
||||||
name: Build APK
|
name: Build APK
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
release_tag:
|
XRAY_CORE_VERSION:
|
||||||
|
description: 'Xray core version or commit hash'
|
||||||
required: false
|
required: false
|
||||||
type: string
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
submodules: 'recursive'
|
|
||||||
fetch-depth: '0'
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: android-actions/setup-android@v3.2.0
|
|
||||||
with:
|
|
||||||
log-accepted-android-sdk-licenses: false
|
|
||||||
cmdline-tools-version: '12266719'
|
|
||||||
packages: 'platforms;android-35 build-tools;35.0.0 platform-tools'
|
|
||||||
|
|
||||||
- name: Install NDK
|
|
||||||
run: |
|
|
||||||
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
|
|
||||||
--channel=3 \
|
|
||||||
--install "ndk;29.0.13113456"
|
|
||||||
echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.13113456" >> $GITHUB_ENV
|
|
||||||
sed -i '10i\
|
|
||||||
\
|
|
||||||
ndkVersion = "29.0.13113456"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
|
||||||
|
|
||||||
- name: Restore cached libtun2socks
|
|
||||||
id: cache-libtun2socks-restore
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/libs
|
|
||||||
key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
|
|
||||||
|
|
||||||
- name: Build libtun2socks
|
|
||||||
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
bash compile-tun2socks.sh
|
|
||||||
|
|
||||||
- name: Save libtun2socks
|
|
||||||
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/libs
|
|
||||||
key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
|
|
||||||
|
|
||||||
- name: Copy libtun2socks
|
|
||||||
run: |
|
|
||||||
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
|
|
||||||
|
|
||||||
- name: Fetch AndroidLibXrayLite tag
|
|
||||||
run: |
|
|
||||||
pushd AndroidLibXrayLite
|
|
||||||
CURRENT_TAG=$(git describe --tags --abbrev=0)
|
|
||||||
echo "Current tag in this repo: $CURRENT_TAG"
|
|
||||||
echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV
|
|
||||||
popd
|
|
||||||
|
|
||||||
- name: Download libv2ray
|
|
||||||
uses: robinraju/release-downloader@v1.12
|
|
||||||
with:
|
|
||||||
repository: '2dust/AndroidLibXrayLite'
|
|
||||||
tag: ${{ env.CURRENT_TAG }}
|
|
||||||
fileName: 'libv2ray.aar'
|
|
||||||
out-file-path: V2rayNG/app/libs/
|
|
||||||
|
|
||||||
- name: Restore cached libhysteria2
|
|
||||||
id: cache-libhysteria2-restore
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/hysteria/libs
|
|
||||||
key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
|
||||||
|
|
||||||
- name: Setup Golang
|
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/setup-go@v5.4.0
|
|
||||||
with:
|
|
||||||
go-version-file: 'AndroidLibXrayLite/go.mod'
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Build libhysteria2
|
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
bash libhysteria2.sh
|
|
||||||
|
|
||||||
- name: Save libhysteria2
|
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/hysteria/libs
|
|
||||||
key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
|
||||||
|
|
||||||
- name: Copy libhysteria2
|
|
||||||
run: |
|
|
||||||
cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app
|
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4.7.0
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Decode Keystore
|
- name: Setup Golang
|
||||||
uses: timheuer/base64-to-file@v1.2.4
|
uses: actions/setup-go@v4
|
||||||
id: android_keystore
|
|
||||||
with:
|
with:
|
||||||
fileName: "android_keystore.jks"
|
go-version: '1.21.4'
|
||||||
encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }}
|
|
||||||
|
- name: Install gomobile
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
|
||||||
|
- name: Setup Android environment
|
||||||
|
uses: android-actions/setup-android@v2
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build dependencies
|
||||||
|
run: |
|
||||||
|
mkdir ${{ github.workspace }}/build
|
||||||
|
cd ${{ github.workspace }}/build
|
||||||
|
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
|
||||||
|
cd AndroidLibXrayLite
|
||||||
|
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
|
||||||
|
gomobile init
|
||||||
|
go mod tidy -v
|
||||||
|
gomobile bind -v -androidapi 19 -ldflags='-s -w' ./
|
||||||
|
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
|
||||||
|
|
||||||
- name: Build APK
|
- name: Build APK
|
||||||
run: |
|
run: |
|
||||||
cd ${{ github.workspace }}/V2rayNG
|
cd ${{ github.workspace }}/V2rayNG
|
||||||
echo "sdk.dir=${ANDROID_HOME}" > local.properties
|
chmod 777 *
|
||||||
chmod 755 gradlew
|
sed -i 's/org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8/org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8/' ${{ github.workspace }}/V2rayNG/gradle.properties
|
||||||
./gradlew licenseFdroidReleaseReport
|
./gradlew assembleDebug
|
||||||
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Upload arm64-v8a APK
|
|
||||||
uses: actions/upload-artifact@v4.6.2
|
|
||||||
if: ${{ success() }}
|
|
||||||
with:
|
|
||||||
name: arm64-v8a
|
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
|
|
||||||
|
|
||||||
- name: Upload armeabi-v7a APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@v3
|
||||||
if: ${{ success() }}
|
|
||||||
with:
|
with:
|
||||||
name: armeabi-v7a
|
name: apk
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/
|
||||||
|
|
||||||
- name: Upload x86 APK
|
|
||||||
uses: actions/upload-artifact@v4.6.2
|
|
||||||
if: ${{ success() }}
|
|
||||||
with:
|
|
||||||
name: x86-apk
|
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*x86*.apk
|
|
||||||
|
|
||||||
- name: Upload to release
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
if: github.event.inputs.release_tag != ''
|
|
||||||
with:
|
|
||||||
file: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*playstore*/release/*.apk
|
|
||||||
tag: ${{ github.event.inputs.release_tag }}
|
|
||||||
file_glob: true
|
|
||||||
prerelease: true
|
|
||||||
|
|
16
.github/workflows/fastlane.yml
vendored
16
.github/workflows/fastlane.yml
vendored
|
@ -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@v4
|
|
||||||
- name: Validate Fastlane Supply Metadata
|
|
||||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0
|
|
61
.gitignore
vendored
61
.gitignore
vendored
|
@ -1,66 +1,5 @@
|
||||||
# Ignore data and key store files
|
|
||||||
*.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
|
|
||||||
|
|
12
.gitmodules
vendored
12
.gitmodules
vendored
|
@ -1,12 +0,0 @@
|
||||||
[submodule "hysteria"]
|
|
||||||
path = hysteria
|
|
||||||
url = https://github.com/apernet/hysteria
|
|
||||||
[submodule "AndroidLibXrayLite"]
|
|
||||||
path = AndroidLibXrayLite
|
|
||||||
url = https://github.com/2dust/AndroidLibXrayLite
|
|
||||||
[submodule "badvpn"]
|
|
||||||
path = badvpn
|
|
||||||
url = https://github.com/XTLS/badvpn
|
|
||||||
[submodule "libancillary"]
|
|
||||||
path = libancillary
|
|
||||||
url = https://github.com/shadowsocks/libancillary
|
|
20
AndroidLibV2rayLite/README.md
Normal file
20
AndroidLibV2rayLite/README.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# AndroidLibV2rayLite
|
||||||
|
|
||||||
|
### Preparation
|
||||||
|
- latest Ubuntu environment
|
||||||
|
- At lease 30G free space
|
||||||
|
- Get Repo [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
|
||||||
|
### Prepare Go
|
||||||
|
- Go to https://golang.org/doc/install and install latest go
|
||||||
|
- Make sure `go version` works as expected
|
||||||
|
### Prepare gomobile
|
||||||
|
- Go to https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile and install gomobile
|
||||||
|
- export PATH=$PATH:~/go/bin
|
||||||
|
- Make sure `gomobile init` works as expected
|
||||||
|
### Prepare NDK
|
||||||
|
- Go to https://developer.android.com/ndk/downloads and install latest NDK
|
||||||
|
- export PATH=$PATH:<wherever you ndk is located>
|
||||||
|
- Make sure `ndk-build -v` works as expected
|
||||||
|
### Make
|
||||||
|
- sudo apt install make
|
||||||
|
- Read and understand [build script](https://github.com/2dust/AndroidLibV2rayLite/blob/master/Makefile)
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 8ad3e1ddf165d8d67e488346b2faa9153d3e33a4
|
|
|
@ -3,12 +3,16 @@
|
||||||
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/lollipop)
|
||||||
[](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)
|
||||||
|
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
||||||
|
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
||||||
|
</a>
|
||||||
|
|
||||||
### Telegram Channel
|
### Telegram Channel
|
||||||
[github_2dust](https://t.me/github_2dust)
|
[github_2dust](https://t.me/github_2dust)
|
||||||
|
|
||||||
|
@ -17,7 +21,7 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
|
||||||
#### 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)
|
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
||||||
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
||||||
- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually
|
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
|
||||||
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
|
- 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)
|
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||||
|
|
10
V2rayNG/.gitignore
vendored
Normal file
10
V2rayNG/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
*.apk
|
||||||
|
signing.properties
|
||||||
|
*.aar
|
2
V2rayNG/app/.gitignore
vendored
Normal file
2
V2rayNG/app/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/build
|
||||||
|
/google-services.json
|
140
V2rayNG/app/build.gradle
Normal file
140
V2rayNG/app/build.gradle
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion Integer.parseInt("$compileSdkVer")
|
||||||
|
buildToolsVersion "$buildToolsVer"
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.v2ray.ang"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
||||||
|
multiDexEnabled true
|
||||||
|
versionCode 540
|
||||||
|
versionName "1.8.15"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
zipAlignEnabled false
|
||||||
|
shrinkResources false
|
||||||
|
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
minifyEnabled false
|
||||||
|
zipAlignEnabled false
|
||||||
|
shrinkResources false
|
||||||
|
ndk.abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flavorDimensions "versions"
|
||||||
|
//
|
||||||
|
// productFlavors {
|
||||||
|
// dev {
|
||||||
|
// applicationIdSuffix = ".dev"
|
||||||
|
// versionNameSuffix = "-dev"
|
||||||
|
// }
|
||||||
|
// pre_release {
|
||||||
|
// applicationIdSuffix = ".pre"
|
||||||
|
// versionNameSuffix = "-pre-release"
|
||||||
|
// }
|
||||||
|
// prod {
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
jniLibs.srcDirs = ['libs']
|
||||||
|
java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
|
||||||
|
universalApk true //generate an additional APK that contains all the ABIs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// map for the version code
|
||||||
|
project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
||||||
|
|
||||||
|
android.applicationVariants.all { variant ->
|
||||||
|
// assign different version code for each output
|
||||||
|
variant.outputs.each { output ->
|
||||||
|
output.outputFileName = "v2rayNG_" + variant.versionName + "_" + output.getFilter(com.android.build.OutputFile.ABI) + ".apk"
|
||||||
|
|
||||||
|
output.versionCodeOverride =
|
||||||
|
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
|
||||||
|
1000000 + android.defaultConfig.versionCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
namespace 'com.v2ray.ang'
|
||||||
|
testNamespace 'com.v2ray.angTest'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
|
||||||
|
// Androidx
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.5.7'
|
||||||
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
|
|
||||||
|
// Androidx ktx
|
||||||
|
implementation 'androidx.activity:activity-ktx:1.7.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
|
||||||
|
|
||||||
|
//kotlin
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
||||||
|
|
||||||
|
implementation 'com.tencent:mmkv-static:1.3.3'
|
||||||
|
implementation 'com.google.code.gson:gson:2.10.1'
|
||||||
|
implementation 'io.reactivex:rxjava:1.3.8'
|
||||||
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
|
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
|
||||||
|
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
|
||||||
|
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||||
|
implementation 'com.blacksquircle.ui:editorkit:2.8.0'
|
||||||
|
implementation 'com.blacksquircle.ui:language-base:2.8.0'
|
||||||
|
implementation 'com.blacksquircle.ui:language-json:2.8.0'
|
||||||
|
implementation 'io.github.g00fy2.quickie:quickie-bundled:1.6.0'
|
||||||
|
implementation 'com.google.zxing:core:3.5.3'
|
||||||
|
|
||||||
|
def work_version = "2.8.1"
|
||||||
|
|
||||||
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
|
implementation "androidx.work:work-multiprocess:$work_version"
|
||||||
|
}
|
|
@ -1,191 +0,0 @@
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.android.application)
|
|
||||||
alias(libs.plugins.kotlin.android)
|
|
||||||
id("com.jaredsburrows.license")
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.v2ray.ang"
|
|
||||||
compileSdk = 35
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "com.v2ray.ang"
|
|
||||||
minSdk = 21
|
|
||||||
targetSdk = 35
|
|
||||||
versionCode = 658
|
|
||||||
versionName = "1.10.8"
|
|
||||||
multiDexEnabled = true
|
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
isEnable = true
|
|
||||||
reset()
|
|
||||||
if (abiFilterList != null && abiFilterList.isNotEmpty()) {
|
|
||||||
include(*abiFilterList.toTypedArray())
|
|
||||||
} else {
|
|
||||||
include(
|
|
||||||
"arm64-v8a",
|
|
||||||
"armeabi-v7a",
|
|
||||||
"x86_64",
|
|
||||||
"x86"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
isUniversalApk = abiFilterList.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
isMinifyEnabled = false
|
|
||||||
proguardFiles(
|
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions.add("distribution")
|
|
||||||
productFlavors {
|
|
||||||
create("fdroid") {
|
|
||||||
dimension = "distribution"
|
|
||||||
applicationIdSuffix = ".fdroid"
|
|
||||||
buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"")
|
|
||||||
}
|
|
||||||
create("playstore") {
|
|
||||||
dimension = "distribution"
|
|
||||||
buildConfigField("String", "DISTRIBUTION", "\"Play Store\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
getByName("main") {
|
|
||||||
jniLibs.srcDirs("libs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
isCoreLibraryDesugaringEnabled = true
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationVariants.all {
|
|
||||||
val variant = this
|
|
||||||
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
|
|
||||||
if (isFdroid) {
|
|
||||||
val versionCodes =
|
|
||||||
mapOf(
|
|
||||||
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
|
|
||||||
)
|
|
||||||
|
|
||||||
variant.outputs
|
|
||||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
|
||||||
.forEach { output ->
|
|
||||||
val abi = output.getFilter("ABI") ?: "universal"
|
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
|
|
||||||
if (versionCodes.containsKey(abi)) {
|
|
||||||
output.versionCodeOverride =
|
|
||||||
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
|
|
||||||
} else {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val versionCodes =
|
|
||||||
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
|
|
||||||
|
|
||||||
variant.outputs
|
|
||||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
|
||||||
.forEach { output ->
|
|
||||||
val abi = if (output.getFilter("ABI") != null)
|
|
||||||
output.getFilter("ABI")
|
|
||||||
else
|
|
||||||
"universal"
|
|
||||||
|
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
|
||||||
if (versionCodes.containsKey(abi)) {
|
|
||||||
output.versionCodeOverride =
|
|
||||||
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
|
||||||
} else {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding = true
|
|
||||||
buildConfig = true
|
|
||||||
}
|
|
||||||
|
|
||||||
packaging {
|
|
||||||
jniLibs {
|
|
||||||
useLegacyPackaging = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// Core Libraries
|
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
|
||||||
|
|
||||||
// AndroidX Core Libraries
|
|
||||||
implementation(libs.androidx.core.ktx)
|
|
||||||
implementation(libs.androidx.appcompat)
|
|
||||||
implementation(libs.androidx.activity)
|
|
||||||
implementation(libs.androidx.constraintlayout)
|
|
||||||
implementation(libs.preference.ktx)
|
|
||||||
implementation(libs.recyclerview)
|
|
||||||
implementation(libs.androidx.swiperefreshlayout)
|
|
||||||
|
|
||||||
// UI Libraries
|
|
||||||
implementation(libs.material)
|
|
||||||
implementation(libs.toasty)
|
|
||||||
implementation(libs.editorkit)
|
|
||||||
implementation(libs.flexbox)
|
|
||||||
|
|
||||||
// Data and Storage Libraries
|
|
||||||
implementation(libs.mmkv.static)
|
|
||||||
implementation(libs.gson)
|
|
||||||
|
|
||||||
// Reactive and Utility Libraries
|
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
|
||||||
|
|
||||||
// Language and Processing Libraries
|
|
||||||
implementation(libs.language.base)
|
|
||||||
implementation(libs.language.json)
|
|
||||||
|
|
||||||
// Intent and Utility Libraries
|
|
||||||
implementation(libs.quickie.foss)
|
|
||||||
implementation(libs.core)
|
|
||||||
|
|
||||||
// AndroidX Lifecycle and Architecture Components
|
|
||||||
implementation(libs.lifecycle.viewmodel.ktx)
|
|
||||||
implementation(libs.lifecycle.livedata.ktx)
|
|
||||||
implementation(libs.lifecycle.runtime.ktx)
|
|
||||||
|
|
||||||
// Background Task Libraries
|
|
||||||
implementation(libs.work.runtime.ktx)
|
|
||||||
implementation(libs.work.multiprocess)
|
|
||||||
|
|
||||||
// Multidex Support
|
|
||||||
implementation(libs.multidex)
|
|
||||||
|
|
||||||
// Testing Libraries
|
|
||||||
testImplementation(libs.junit)
|
|
||||||
androidTestImplementation(libs.androidx.junit)
|
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
|
||||||
testImplementation(libs.org.mockito.mockito.inline)
|
|
||||||
testImplementation(libs.mockito.kotlin)
|
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
|
||||||
}
|
|
BIN
V2rayNG/app/libs/arm64-v8a/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/arm64-v8a/libtun2socks.so
Normal file
Binary file not shown.
BIN
V2rayNG/app/libs/armeabi-v7a/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/armeabi-v7a/libtun2socks.so
Normal file
Binary file not shown.
BIN
V2rayNG/app/libs/x86/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/x86/libtun2socks.so
Normal file
Binary file not shown.
BIN
V2rayNG/app/libs/x86_64/libtun2socks.so
Normal file
BIN
V2rayNG/app/libs/x86_64/libtun2socks.so
Normal file
Binary file not shown.
21
V2rayNG/app/proguard-rules.pro
vendored
21
V2rayNG/app/proguard-rules.pro
vendored
|
@ -1,21 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
|
@ -5,53 +5,42 @@
|
||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
android:largeScreens="true"
|
|
||||||
android:normalScreens="true"
|
|
||||||
android:smallScreens="true"
|
android:smallScreens="true"
|
||||||
android:xlargeScreens="true" />
|
android:normalScreens="true"
|
||||||
|
android:largeScreens="true"
|
||||||
|
android:xlargeScreens="true"/>
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk android:minSdkVersion="21" tools:overrideLibrary="com.blacksquircle.ui.editorkit"/>
|
||||||
android:minSdkVersion="21"
|
|
||||||
tools:overrideLibrary="com.blacksquircle.ui.editorkit" />
|
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
android:name="android.hardware.camera"
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||||
android:required="false" />
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
<uses-feature
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
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="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" />
|
||||||
|
<!-- <useapplications-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="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||||
android:minSdkVersion="34" />
|
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.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<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:banner="@mipmap/ic_banner"
|
||||||
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"
|
android:theme="@style/AppThemeDayNight"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
@ -64,100 +53,85 @@
|
||||||
android:theme="@style/AppThemeDayNight.NoActionBar">
|
android:theme="@style/AppThemeDayNight.NoActionBar">
|
||||||
<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" />
|
<!-- <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>-->
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="false"
|
||||||
android:name=".ui.ServerActivity"
|
android:name=".ui.ServerActivity"
|
||||||
android:exported="false"
|
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="false"
|
||||||
android:name=".ui.ServerCustomConfigActivity"
|
android:name=".ui.ServerCustomConfigActivity"
|
||||||
android:exported="false"
|
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.SettingsActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.SettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.PerAppProxyActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.PerAppProxyActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ScannerActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.ScannerActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.LogcatActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.LogcatActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.RoutingSettingActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.RoutingSettingsActivity"
|
||||||
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.RoutingEditActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.SubSettingActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.SubSettingActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.UserAssetActivity" />
|
||||||
<activity
|
|
||||||
android:name=".ui.UserAssetActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.UserAssetUrlActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.SubEditActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.SubEditActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ScScannerActivity"
|
android:exported="false"
|
||||||
android:exported="false" />
|
android:name=".ui.ScScannerActivity" />
|
||||||
<activity
|
<activity
|
||||||
|
android:exported="false"
|
||||||
android:name=".ui.ScSwitchActivity"
|
android:name=".ui.ScSwitchActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="false"
|
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.UrlSchemeActivity"
|
android:exported="true"
|
||||||
android:exported="true">
|
android:name=".ui.UrlSchemeActivity">
|
||||||
<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.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<data android:scheme="v2rayng"/>
|
||||||
|
<data android:host="install-config"/>
|
||||||
<data android:scheme="v2rayng" />
|
<data android:host="install-sub"/>
|
||||||
<data android:host="install-config" />
|
|
||||||
<data android:host="install-sub" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.CheckUpdateActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
<activity
|
|
||||||
android:name=".ui.AboutActivity"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.V2RayVpnService"
|
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:foregroundServiceType="specialUse"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService" />
|
<action android:name="android.net.VpnService" />
|
||||||
|
@ -170,53 +144,43 @@
|
||||||
android:value="vpn" />
|
android:value="vpn" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service android:name=".service.V2RayProxyOnlyService"
|
||||||
android:name=".service.V2RayProxyOnlyService"
|
android:exported="false"
|
||||||
android:exported="false"
|
android:label="@string/app_name"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
android:label="@string/app_name"
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
|
||||||
<property
|
<property
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
android:value="proxy" />
|
android:value="proxy" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service android:name=".service.V2RayTestService"
|
||||||
android:name=".service.V2RayTestService"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:process=":RunSoLibV2RayDaemon" />
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receiver.WidgetProvider"
|
android:exported="true"
|
||||||
android:exported="true"
|
android:name=".receiver.WidgetProvider"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
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="com.v2ray.ang.action.widget.click" />
|
<action android:name="com.v2ray.ang.action.widget.click" />
|
||||||
<action android:name="com.v2ray.ang.action.activity" />
|
<action android:name="com.v2ray.ang.action.activity" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver
|
|
||||||
android:name=".receiver.BootReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="BootReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.QSTileService"
|
android:exported="true"
|
||||||
android:exported="true"
|
android:name=".service.QSTileService"
|
||||||
android:foregroundServiceType="specialUse"
|
android:icon="@drawable/ic_stat_name"
|
||||||
android:icon="@drawable/ic_stat_name"
|
android:label="@string/app_tile_name"
|
||||||
android:label="@string/app_tile_name"
|
android:foregroundServiceType="specialUse"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
tools:targetApi="24">
|
|
||||||
<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>
|
||||||
|
@ -226,19 +190,19 @@
|
||||||
</service>
|
</service>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.TaskerActivity"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:name=".ui.TaskerActivity"
|
||||||
|
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:exported="true"
|
android:exported="true"
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
android:name=".receiver.TaskerReceiver"
|
||||||
tools:ignore="ExportedReceiver">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<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>
|
||||||
|
@ -257,16 +221,6 @@
|
||||||
|
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.cache"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/cache_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "绕过bittorrent",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"protocol": [
|
|
||||||
"bittorrent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Google cn",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"domain:googleapis.cn",
|
|
||||||
"domain:gstatic.com"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "阻断udp443",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"port": "443",
|
|
||||||
"network": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网域名",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"geosite:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "代理海外公共DNSIP",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"ip": [
|
|
||||||
"1.1.1.1",
|
|
||||||
"1.0.0.1",
|
|
||||||
"2606:4700:4700::1111",
|
|
||||||
"2606:4700:4700::1001",
|
|
||||||
"1.1.1.2",
|
|
||||||
"1.0.0.2",
|
|
||||||
"2606:4700:4700::1112",
|
|
||||||
"2606:4700:4700::1002",
|
|
||||||
"1.1.1.3",
|
|
||||||
"1.0.0.3",
|
|
||||||
"2606:4700:4700::1113",
|
|
||||||
"2606:4700:4700::1003",
|
|
||||||
"8.8.8.8",
|
|
||||||
"8.8.4.4",
|
|
||||||
"2001:4860:4860::8888",
|
|
||||||
"2001:4860:4860::8844",
|
|
||||||
"94.140.14.14",
|
|
||||||
"94.140.15.15",
|
|
||||||
"2a10:50c0::ad1:ff",
|
|
||||||
"2a10:50c0::ad2:ff",
|
|
||||||
"94.140.14.15",
|
|
||||||
"94.140.15.16",
|
|
||||||
"2a10:50c0::bad1:ff",
|
|
||||||
"2a10:50c0::bad2:ff",
|
|
||||||
"94.140.14.140",
|
|
||||||
"94.140.14.141",
|
|
||||||
"2a10:50c0::1:ff",
|
|
||||||
"2a10:50c0::2:ff",
|
|
||||||
"208.67.222.222",
|
|
||||||
"208.67.220.220",
|
|
||||||
"2620:119:35::35",
|
|
||||||
"2620:119:53::53",
|
|
||||||
"208.67.222.123",
|
|
||||||
"208.67.220.123",
|
|
||||||
"2620:119:35::123",
|
|
||||||
"2620:119:53::123",
|
|
||||||
"9.9.9.9",
|
|
||||||
"149.112.112.112",
|
|
||||||
"2620:fe::9",
|
|
||||||
"2620:fe::fe",
|
|
||||||
"9.9.9.11",
|
|
||||||
"149.112.112.11",
|
|
||||||
"2620:fe::11",
|
|
||||||
"2620:fe::fe:11",
|
|
||||||
"9.9.9.10",
|
|
||||||
"149.112.112.10",
|
|
||||||
"2620:fe::10",
|
|
||||||
"2620:fe::fe:10",
|
|
||||||
"77.88.8.8",
|
|
||||||
"77.88.8.1",
|
|
||||||
"2a02:6b8::feed:0ff",
|
|
||||||
"2a02:6b8:0:1::feed:0ff",
|
|
||||||
"77.88.8.88",
|
|
||||||
"77.88.8.2",
|
|
||||||
"2a02:6b8::feed:bad",
|
|
||||||
"2a02:6b8:0:1::feed:bad",
|
|
||||||
"77.88.8.7",
|
|
||||||
"77.88.8.3",
|
|
||||||
"2a02:6b8::feed:a11",
|
|
||||||
"2a02:6b8:0:1::feed:a11"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "代理海外公共DNS域名",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"domain:cloudflare-dns.com",
|
|
||||||
"domain:one.one.one.one",
|
|
||||||
"domain:dns.google",
|
|
||||||
"domain:adguard-dns.com",
|
|
||||||
"domain:opendns.com",
|
|
||||||
"domain:umbrella.com",
|
|
||||||
"domain:quad9.net",
|
|
||||||
"domain:yandex.net"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "代理IP",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"ip": [
|
|
||||||
"geoip:facebook",
|
|
||||||
"geoip:fastly",
|
|
||||||
"geoip:google",
|
|
||||||
"geoip:netflix",
|
|
||||||
"geoip:telegram",
|
|
||||||
"geoip:twitter"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "代理GFW",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"geosite:gfw",
|
|
||||||
"geosite:greatfire"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "最终直连",
|
|
||||||
"port": "0-65535",
|
|
||||||
"outboundTag": "direct"
|
|
||||||
}
|
|
||||||
]
|
|
1
V2rayNG/app/src/main/assets/custom_routing_block
Normal file
1
V2rayNG/app/src/main/assets/custom_routing_block
Normal file
|
@ -0,0 +1 @@
|
||||||
|
geosite:category-ads-all,
|
132
V2rayNG/app/src/main/assets/custom_routing_direct
Normal file
132
V2rayNG/app/src/main/assets/custom_routing_direct
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
domain:12306.com,
|
||||||
|
domain:51ym.me,
|
||||||
|
domain:52pojie.cn,
|
||||||
|
domain:8686c.com,
|
||||||
|
domain:abercrombie.com,
|
||||||
|
domain:adobesc.com,
|
||||||
|
domain:air-matters.com,
|
||||||
|
domain:air-matters.io,
|
||||||
|
domain:airtable.com,
|
||||||
|
domain:akadns.net,
|
||||||
|
domain:apache.org,
|
||||||
|
domain:api.crisp.chat,
|
||||||
|
domain:api.termius.com,
|
||||||
|
domain:appshike.com,
|
||||||
|
domain:appstore.com,
|
||||||
|
domain:aweme.snssdk.com,
|
||||||
|
domain:bababian.com,
|
||||||
|
domain:battle.net,
|
||||||
|
domain:beatsbydre.com,
|
||||||
|
domain:bet365.com,
|
||||||
|
domain:bilibili.cn,
|
||||||
|
domain:ccgslb.com,
|
||||||
|
domain:ccgslb.net,
|
||||||
|
domain:chunbo.com,
|
||||||
|
domain:chunboimg.com,
|
||||||
|
domain:clashroyaleapp.com,
|
||||||
|
domain:cloudsigma.com,
|
||||||
|
domain:cloudxns.net,
|
||||||
|
domain:cmfu.com,
|
||||||
|
domain:culturedcode.com,
|
||||||
|
domain:dct-cloud.com,
|
||||||
|
domain:didialift.com,
|
||||||
|
domain:douyutv.com,
|
||||||
|
domain:duokan.com,
|
||||||
|
domain:dytt8.net,
|
||||||
|
domain:easou.com,
|
||||||
|
domain:ecitic.net,
|
||||||
|
domain:eclipse.org,
|
||||||
|
domain:eudic.net,
|
||||||
|
domain:ewqcxz.com,
|
||||||
|
domain:fir.im,
|
||||||
|
domain:frdic.com,
|
||||||
|
domain:fresh-ideas.cc,
|
||||||
|
domain:godic.net,
|
||||||
|
domain:goodread.com,
|
||||||
|
domain:haibian.com,
|
||||||
|
domain:hdslb.net,
|
||||||
|
domain:hollisterco.com,
|
||||||
|
domain:hongxiu.com,
|
||||||
|
domain:hxcdn.net,
|
||||||
|
domain:images.unsplash.com,
|
||||||
|
domain:img4me.com,
|
||||||
|
domain:ipify.org,
|
||||||
|
domain:ixdzs.com,
|
||||||
|
domain:jd.hk,
|
||||||
|
domain:jianshuapi.com,
|
||||||
|
domain:jomodns.com,
|
||||||
|
domain:jsboxbbs.com,
|
||||||
|
domain:knewone.com,
|
||||||
|
domain:kuaidi100.com,
|
||||||
|
domain:lemicp.com,
|
||||||
|
domain:letvcloud.com,
|
||||||
|
domain:lizhi.io,
|
||||||
|
domain:localizecdn.com,
|
||||||
|
domain:lucifr.com,
|
||||||
|
domain:luoo.net,
|
||||||
|
domain:mai.tn,
|
||||||
|
domain:maven.org,
|
||||||
|
domain:miwifi.com,
|
||||||
|
domain:moji.com,
|
||||||
|
domain:moke.com,
|
||||||
|
domain:mtalk.google.com,
|
||||||
|
domain:mxhichina.com,
|
||||||
|
domain:myqcloud.com,
|
||||||
|
domain:myunlu.com,
|
||||||
|
domain:netease.com,
|
||||||
|
domain:nfoservers.com,
|
||||||
|
domain:nssurge.com,
|
||||||
|
domain:nuomi.com,
|
||||||
|
domain:ourdvs.com,
|
||||||
|
domain:overcast.fm,
|
||||||
|
domain:paypal.com,
|
||||||
|
domain:paypalobjects.com,
|
||||||
|
domain:pgyer.com,
|
||||||
|
domain:qdaily.com,
|
||||||
|
domain:qdmm.com,
|
||||||
|
domain:qin.io,
|
||||||
|
domain:qingmang.me,
|
||||||
|
domain:qingmang.mobi,
|
||||||
|
domain:qqurl.com,
|
||||||
|
domain:rarbg.to,
|
||||||
|
domain:rrmj.tv,
|
||||||
|
domain:ruguoapp.com,
|
||||||
|
domain:sm.ms,
|
||||||
|
domain:snwx.com,
|
||||||
|
domain:soku.com,
|
||||||
|
domain:startssl.com,
|
||||||
|
domain:store.steampowered.com,
|
||||||
|
domain:symcd.com,
|
||||||
|
domain:teamviewer.com,
|
||||||
|
domain:tmzvps.com,
|
||||||
|
domain:trello.com,
|
||||||
|
domain:trellocdn.com,
|
||||||
|
domain:ttmeiju.com,
|
||||||
|
domain:udache.com,
|
||||||
|
domain:uxengine.net,
|
||||||
|
domain:weather.bjango.com,
|
||||||
|
domain:weather.com,
|
||||||
|
domain:webqxs.com,
|
||||||
|
domain:weico.cc,
|
||||||
|
domain:wenku8.net,
|
||||||
|
domain:werewolf.53site.com,
|
||||||
|
domain:windowsupdate.com,
|
||||||
|
domain:wkcdn.com,
|
||||||
|
domain:workflowy.com,
|
||||||
|
domain:xdrig.com,
|
||||||
|
domain:xiaojukeji.com,
|
||||||
|
domain:xiaomi.net,
|
||||||
|
domain:xiaomicp.com,
|
||||||
|
domain:ximalaya.com,
|
||||||
|
domain:xitek.com,
|
||||||
|
domain:xmcdn.com,
|
||||||
|
domain:xslb.net,
|
||||||
|
domain:xteko.com,
|
||||||
|
domain:yach.me,
|
||||||
|
domain:yixia.com,
|
||||||
|
domain:yunjiasu-cdn.net,
|
||||||
|
domain:zealer.com,
|
||||||
|
domain:zgslb.net,
|
||||||
|
domain:zimuzu.tv,
|
||||||
|
domain:zmz002.com,
|
||||||
|
domain:samsungdm.com,
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
33
V2rayNG/app/src/main/assets/custom_routing_proxy
Normal file
33
V2rayNG/app/src/main/assets/custom_routing_proxy
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
geosite:google,
|
||||||
|
geosite:github,
|
||||||
|
geosite:netflix,
|
||||||
|
geosite:steam,
|
||||||
|
geosite:telegram,
|
||||||
|
geosite:tumblr,
|
||||||
|
geosite:speedtest,
|
||||||
|
geosite:bbc,
|
||||||
|
domain:gvt1.com,
|
||||||
|
domain:textnow.com,
|
||||||
|
domain:twitch.tv,
|
||||||
|
domain:wikileaks.org,
|
||||||
|
domain:naver.com,
|
||||||
|
91.108.4.0/22,
|
||||||
|
91.108.8.0/22,
|
||||||
|
91.108.12.0/22,
|
||||||
|
91.108.20.0/22,
|
||||||
|
91.108.36.0/23,
|
||||||
|
91.108.38.0/23,
|
||||||
|
91.108.56.0/22,
|
||||||
|
149.154.160.0/20,
|
||||||
|
149.154.164.0/22,
|
||||||
|
149.154.172.0/22,
|
||||||
|
74.125.0.0/16,
|
||||||
|
173.194.0.0/16,
|
||||||
|
172.217.0.0/16,
|
||||||
|
216.58.200.0/24,
|
||||||
|
216.58.220.0/24,
|
||||||
|
91.108.56.116,
|
||||||
|
91.108.56.0/24,
|
||||||
|
109.239.140.0/24,
|
||||||
|
149.154.167.0/24,
|
||||||
|
149.154.175.0/24,
|
|
@ -1,96 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"remarks": "Google cn",
|
|
||||||
"outboundTag": "proxy",
|
|
||||||
"domain": [
|
|
||||||
"domain:googleapis.cn",
|
|
||||||
"domain:gstatic.com"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "阻断udp443",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"port": "443",
|
|
||||||
"network": "udp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过局域网域名",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"geosite:private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过中国公共DNSIP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"223.5.5.5",
|
|
||||||
"223.6.6.6",
|
|
||||||
"2400:3200::1",
|
|
||||||
"2400:3200:baba::1",
|
|
||||||
"119.29.29.29",
|
|
||||||
"1.12.12.12",
|
|
||||||
"120.53.53.53",
|
|
||||||
"2402:4e00::",
|
|
||||||
"2402:4e00:1::",
|
|
||||||
"180.76.76.76",
|
|
||||||
"2400:da00::6666",
|
|
||||||
"114.114.114.114",
|
|
||||||
"114.114.115.115",
|
|
||||||
"114.114.114.119",
|
|
||||||
"114.114.115.119",
|
|
||||||
"114.114.114.110",
|
|
||||||
"114.114.115.110",
|
|
||||||
"180.184.1.1",
|
|
||||||
"180.184.2.2",
|
|
||||||
"101.226.4.6",
|
|
||||||
"218.30.118.6",
|
|
||||||
"123.125.81.6",
|
|
||||||
"140.207.198.6",
|
|
||||||
"1.2.4.8",
|
|
||||||
"210.2.4.8",
|
|
||||||
"52.80.66.66",
|
|
||||||
"117.50.22.22",
|
|
||||||
"2400:7fc0:849e:200::4",
|
|
||||||
"2404:c2c0:85d8:901::4",
|
|
||||||
"117.50.10.10",
|
|
||||||
"52.80.52.52",
|
|
||||||
"2400:7fc0:849e:200::8",
|
|
||||||
"2404:c2c0:85d8:901::8",
|
|
||||||
"117.50.60.30",
|
|
||||||
"52.80.60.30"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过中国公共DNS域名",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"domain:alidns.com",
|
|
||||||
"domain:doh.pub",
|
|
||||||
"domain:dot.pub",
|
|
||||||
"domain:360.cn",
|
|
||||||
"domain:onedns.net"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过中国IP",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"ip": [
|
|
||||||
"geoip:cn"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "绕过中国域名",
|
|
||||||
"outboundTag": "direct",
|
|
||||||
"domain": [
|
|
||||||
"geosite:cn"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
|
|
@ -81,9 +81,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"protocol": "freedom",
|
"protocol": "freedom",
|
||||||
"settings": {
|
"settings": {},
|
||||||
"domainStrategy": "UseIP"
|
|
||||||
},
|
|
||||||
"tag": "direct"
|
"tag": "direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -97,7 +95,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"routing": {
|
"routing": {
|
||||||
"domainStrategy": "AsIs",
|
"domainStrategy": "IPIfNonMatch",
|
||||||
"rules": []
|
"rules": []
|
||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
|
|
|
@ -1,47 +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)
|
|
||||||
|
|
||||||
SettingsManager.setNightMode()
|
|
||||||
// Initialize WorkManager with the custom configuration
|
|
||||||
WorkManager.initialize(this, workManagerConfiguration)
|
|
||||||
|
|
||||||
SettingsManager.initRoutingRulesets(this)
|
|
||||||
|
|
||||||
es.dmoral.toasty.Toasty.Config.getInstance()
|
|
||||||
.setGravity(android.view.Gravity.BOTTOM, 0, 200)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,248 +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 DIR_BACKUPS = "backups"
|
|
||||||
|
|
||||||
/** Legacy configuration keys. */
|
|
||||||
const val ANG_CONFIG = "ang_config"
|
|
||||||
|
|
||||||
/** Preferences mapped to MMKV storage. */
|
|
||||||
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
|
||||||
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
|
|
||||||
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
|
||||||
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
|
||||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
|
||||||
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
|
||||||
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
|
||||||
const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy"
|
|
||||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
|
||||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
|
||||||
const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
|
|
||||||
const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index"
|
|
||||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
|
||||||
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
|
|
||||||
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
|
||||||
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
|
|
||||||
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
|
|
||||||
const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
|
|
||||||
const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
|
|
||||||
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
|
|
||||||
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
|
|
||||||
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
|
|
||||||
const val SUBSCRIPTION_AUTO_UPDATE = "pref_auto_update_subscription"
|
|
||||||
const val SUBSCRIPTION_AUTO_UPDATE_INTERVAL = "pref_auto_update_interval"
|
|
||||||
const val SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL = "1440" // Default is 24 hours
|
|
||||||
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
|
|
||||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
|
||||||
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
|
||||||
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
|
|
||||||
const val PREF_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display"
|
|
||||||
const val PREF_LANGUAGE = "pref_language"
|
|
||||||
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
|
||||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
|
||||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
|
||||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
|
||||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
|
||||||
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
|
||||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
|
||||||
const val PREF_DNS_HOSTS = "pref_dns_hosts"
|
|
||||||
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
|
|
||||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
|
||||||
const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method"
|
|
||||||
const val PREF_MODE = "pref_mode"
|
|
||||||
const val PREF_IS_BOOTED = "pref_is_booted"
|
|
||||||
const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release"
|
|
||||||
const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources"
|
|
||||||
|
|
||||||
/** Cache keys. */
|
|
||||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
|
||||||
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
|
||||||
|
|
||||||
/** Protocol identifiers. */
|
|
||||||
const val PROTOCOL_FREEDOM = "freedom"
|
|
||||||
|
|
||||||
/** Broadcast actions. */
|
|
||||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
|
||||||
const val BROADCAST_ACTION_ACTIVITY = "com.v2ray.ang.action.activity"
|
|
||||||
const val BROADCAST_ACTION_WIDGET_CLICK = "com.v2ray.ang.action.widget.click"
|
|
||||||
|
|
||||||
/** Tasker extras. */
|
|
||||||
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
|
||||||
const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
|
|
||||||
const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
|
|
||||||
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
|
||||||
const val TASKER_DEFAULT_GUID = "Default"
|
|
||||||
|
|
||||||
/** Tags for different proxy modes. */
|
|
||||||
const val TAG_PROXY = "proxy"
|
|
||||||
const val TAG_DIRECT = "direct"
|
|
||||||
const val TAG_BLOCKED = "block"
|
|
||||||
const val TAG_FRAGMENT = "fragment"
|
|
||||||
|
|
||||||
/** Network-related constants. */
|
|
||||||
const val UPLINK = "uplink"
|
|
||||||
const val DOWNLINK = "downlink"
|
|
||||||
|
|
||||||
/** URLs for various resources. */
|
|
||||||
const val GITHUB_URL = "https://github.com"
|
|
||||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
|
|
||||||
const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
|
|
||||||
const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
|
|
||||||
const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
|
|
||||||
const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
|
|
||||||
const val APP_ISSUES_URL = "$APP_URL/issues"
|
|
||||||
const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
|
|
||||||
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
|
||||||
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
|
||||||
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
|
||||||
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
|
||||||
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
|
||||||
const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
|
||||||
|
|
||||||
/** DNS server addresses. */
|
|
||||||
const val DNS_PROXY = "1.1.1.1"
|
|
||||||
const val DNS_DIRECT = "223.5.5.5"
|
|
||||||
const val DNS_VPN = "1.1.1.1"
|
|
||||||
const val GEOSITE_PRIVATE = "geosite:private"
|
|
||||||
const val GEOSITE_CN = "geosite:cn"
|
|
||||||
const val GEOIP_PRIVATE = "geoip:private"
|
|
||||||
const val GEOIP_CN = "geoip:cn"
|
|
||||||
|
|
||||||
/** Ports and addresses for various services. */
|
|
||||||
const val PORT_LOCAL_DNS = "10853"
|
|
||||||
const val PORT_SOCKS = "10808"
|
|
||||||
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
|
||||||
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
|
||||||
const val WIREGUARD_LOCAL_MTU = "1420"
|
|
||||||
const val LOOPBACK = "127.0.0.1"
|
|
||||||
|
|
||||||
/** Message constants for communication. */
|
|
||||||
const val MSG_REGISTER_CLIENT = 1
|
|
||||||
const val MSG_STATE_RUNNING = 11
|
|
||||||
const val MSG_STATE_NOT_RUNNING = 12
|
|
||||||
const val MSG_UNREGISTER_CLIENT = 2
|
|
||||||
const val MSG_STATE_START = 3
|
|
||||||
const val MSG_STATE_START_SUCCESS = 31
|
|
||||||
const val MSG_STATE_START_FAILURE = 32
|
|
||||||
const val MSG_STATE_STOP = 4
|
|
||||||
const val MSG_STATE_STOP_SUCCESS = 41
|
|
||||||
const val MSG_STATE_RESTART = 5
|
|
||||||
const val MSG_MEASURE_DELAY = 6
|
|
||||||
const val MSG_MEASURE_DELAY_SUCCESS = 61
|
|
||||||
const val MSG_MEASURE_CONFIG = 7
|
|
||||||
const val MSG_MEASURE_CONFIG_SUCCESS = 71
|
|
||||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
|
||||||
|
|
||||||
/** Notification channel IDs and names. */
|
|
||||||
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
|
||||||
const val RAY_NG_CHANNEL_NAME = "v2rayNG Background Service"
|
|
||||||
const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
|
|
||||||
const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
|
|
||||||
|
|
||||||
/** Protocols Scheme **/
|
|
||||||
const val VMESS = "vmess://"
|
|
||||||
const val CUSTOM = ""
|
|
||||||
const val SHADOWSOCKS = "ss://"
|
|
||||||
const val SOCKS = "socks://"
|
|
||||||
const val HTTP = "http://"
|
|
||||||
const val VLESS = "vless://"
|
|
||||||
const val TROJAN = "trojan://"
|
|
||||||
const val WIREGUARD = "wireguard://"
|
|
||||||
const val TUIC = "tuic://"
|
|
||||||
const val HYSTERIA2 = "hysteria2://"
|
|
||||||
const val HY2 = "hy2://"
|
|
||||||
|
|
||||||
/** Give a good name to this, IDK*/
|
|
||||||
const val VPN = "VPN"
|
|
||||||
|
|
||||||
// Google API rule constants
|
|
||||||
const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
|
|
||||||
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
|
||||||
|
|
||||||
// Android Private DNS constants
|
|
||||||
const val DNS_DNSPOD_DOMAIN = "dot.pub"
|
|
||||||
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
|
||||||
const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
|
|
||||||
const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
|
|
||||||
const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
|
|
||||||
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
|
||||||
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
|
||||||
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
|
||||||
|
|
||||||
const val DEFAULT_PORT = 443
|
|
||||||
const val DEFAULT_SECURITY = "auto"
|
|
||||||
const val DEFAULT_LEVEL = 8
|
|
||||||
const val DEFAULT_NETWORK = "tcp"
|
|
||||||
const val TLS = "tls"
|
|
||||||
const val REALITY = "reality"
|
|
||||||
const val HEADER_TYPE_HTTP = "http"
|
|
||||||
|
|
||||||
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
|
||||||
val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
|
||||||
val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
|
|
||||||
val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9")
|
|
||||||
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
|
||||||
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
|
||||||
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
|
|
||||||
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
|
||||||
|
|
||||||
//minimum list https://serverfault.com/a/304791
|
|
||||||
val ROUTED_IP_LIST = arrayListOf(
|
|
||||||
"0.0.0.0/5",
|
|
||||||
"8.0.0.0/7",
|
|
||||||
"11.0.0.0/8",
|
|
||||||
"12.0.0.0/6",
|
|
||||||
"16.0.0.0/4",
|
|
||||||
"32.0.0.0/3",
|
|
||||||
"64.0.0.0/2",
|
|
||||||
"128.0.0.0/3",
|
|
||||||
"160.0.0.0/5",
|
|
||||||
"168.0.0.0/6",
|
|
||||||
"172.0.0.0/12",
|
|
||||||
"172.32.0.0/11",
|
|
||||||
"172.64.0.0/10",
|
|
||||||
"172.128.0.0/9",
|
|
||||||
"173.0.0.0/8",
|
|
||||||
"174.0.0.0/7",
|
|
||||||
"176.0.0.0/4",
|
|
||||||
"192.0.0.0/9",
|
|
||||||
"192.128.0.0/11",
|
|
||||||
"192.160.0.0/13",
|
|
||||||
"192.169.0.0/16",
|
|
||||||
"192.170.0.0/15",
|
|
||||||
"192.172.0.0/14",
|
|
||||||
"192.176.0.0/12",
|
|
||||||
"192.192.0.0/10",
|
|
||||||
"193.0.0.0/8",
|
|
||||||
"194.0.0.0/7",
|
|
||||||
"196.0.0.0/6",
|
|
||||||
"200.0.0.0/5",
|
|
||||||
"208.0.0.0/4",
|
|
||||||
"240.0.0.0/4"
|
|
||||||
)
|
|
||||||
|
|
||||||
val PRIVATE_IP_LIST = arrayListOf(
|
|
||||||
"0.0.0.0/8",
|
|
||||||
"10.0.0.0/8",
|
|
||||||
"127.0.0.0/8",
|
|
||||||
"172.16.0.0/12",
|
|
||||||
"192.168.0.0/16",
|
|
||||||
"169.254.0.0/16",
|
|
||||||
"224.0.0.0/4"
|
|
||||||
)
|
|
||||||
|
|
||||||
val GEO_FILES_SOURCES = arrayListOf(
|
|
||||||
"Loyalsoldier/v2ray-rules-dat",
|
|
||||||
"runetfreedom/russia-v2ray-rules-dat",
|
|
||||||
"Chocolate4U/Iran-v2ray-rules"
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
|
@ -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,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,9 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ConfigResult(
|
|
||||||
var status: Boolean,
|
|
||||||
var guid: String? = null,
|
|
||||||
var content: String = "",
|
|
||||||
var socksPort: Int? = null,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
enum class EConfigType(val value: Int, val protocolScheme: String) {
|
|
||||||
VMESS(1, AppConfig.VMESS),
|
|
||||||
CUSTOM(2, AppConfig.CUSTOM),
|
|
||||||
SHADOWSOCKS(3, AppConfig.SHADOWSOCKS),
|
|
||||||
SOCKS(4, AppConfig.SOCKS),
|
|
||||||
VLESS(5, AppConfig.VLESS),
|
|
||||||
TROJAN(6, AppConfig.TROJAN),
|
|
||||||
WIREGUARD(7, AppConfig.WIREGUARD),
|
|
||||||
|
|
||||||
// TUIC(8, AppConfig.TUIC),
|
|
||||||
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
|
||||||
HTTP(10, AppConfig.HTTP);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,46 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class Hysteria2Bean(
|
|
||||||
val server: String?,
|
|
||||||
val auth: String?,
|
|
||||||
val lazy: Boolean? = true,
|
|
||||||
val obfs: ObfsBean? = null,
|
|
||||||
val socks5: Socks5Bean? = null,
|
|
||||||
val http: Socks5Bean? = null,
|
|
||||||
val tls: TlsBean? = null,
|
|
||||||
val transport: TransportBean? = null,
|
|
||||||
val bandwidth: BandwidthBean? = null,
|
|
||||||
) {
|
|
||||||
data class ObfsBean(
|
|
||||||
val type: String?,
|
|
||||||
val salamander: SalamanderBean?
|
|
||||||
) {
|
|
||||||
data class SalamanderBean(
|
|
||||||
val password: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Socks5Bean(
|
|
||||||
val listen: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TlsBean(
|
|
||||||
val sni: String?,
|
|
||||||
val insecure: Boolean?,
|
|
||||||
val pinSHA256: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TransportBean(
|
|
||||||
val type: String?,
|
|
||||||
val udp: TransportUdpBean?
|
|
||||||
) {
|
|
||||||
data class TransportUdpBean(
|
|
||||||
val hopInterval: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class BandwidthBean(
|
|
||||||
val down: String?,
|
|
||||||
val up: String?,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,12 +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
|
|
||||||
)
|
|
|
@ -1,20 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
enum class Language(val code: String) {
|
|
||||||
AUTO("auto"),
|
|
||||||
ENGLISH("en"),
|
|
||||||
CHINA("zh-rCN"),
|
|
||||||
TRADITIONAL_CHINESE("zh-rTW"),
|
|
||||||
VIETNAMESE("vi"),
|
|
||||||
RUSSIAN("ru"),
|
|
||||||
PERSIAN("fa"),
|
|
||||||
ARABIC("ar"),
|
|
||||||
BANGLA("bn"),
|
|
||||||
BAKHTIARI("bqi-rIR");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromCode(code: String): Language {
|
|
||||||
return entries.find { it.code == code } ?: AUTO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
enum class NetworkType(val type: String) {
|
|
||||||
TCP("tcp"),
|
|
||||||
KCP("kcp"),
|
|
||||||
WS("ws"),
|
|
||||||
HTTP_UPGRADE("httpupgrade"),
|
|
||||||
XHTTP("xhttp"),
|
|
||||||
HTTP("http"),
|
|
||||||
H2("h2"),
|
|
||||||
|
|
||||||
//QUIC("quic"),
|
|
||||||
GRPC("grpc");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,120 +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.util.Utils
|
|
||||||
|
|
||||||
data class ProfileItem(
|
|
||||||
val configVersion: Int = 4,
|
|
||||||
val configType: EConfigType,
|
|
||||||
var subscriptionId: String = "",
|
|
||||||
var addedTime: Long = System.currentTimeMillis(),
|
|
||||||
|
|
||||||
var remarks: String = "",
|
|
||||||
var server: String? = null,
|
|
||||||
var serverPort: String? = null,
|
|
||||||
|
|
||||||
var password: String? = null,
|
|
||||||
var method: String? = null,
|
|
||||||
var flow: String? = null,
|
|
||||||
var username: String? = null,
|
|
||||||
|
|
||||||
var network: String? = null,
|
|
||||||
var headerType: String? = null,
|
|
||||||
var host: String? = null,
|
|
||||||
var path: String? = null,
|
|
||||||
var seed: String? = null,
|
|
||||||
var quicSecurity: String? = null,
|
|
||||||
var quicKey: String? = null,
|
|
||||||
var mode: String? = null,
|
|
||||||
var serviceName: String? = null,
|
|
||||||
var authority: String? = null,
|
|
||||||
var xhttpMode: String? = null,
|
|
||||||
var xhttpExtra: String? = null,
|
|
||||||
|
|
||||||
var security: String? = null,
|
|
||||||
var sni: String? = null,
|
|
||||||
var alpn: String? = null,
|
|
||||||
var fingerPrint: String? = null,
|
|
||||||
var insecure: Boolean? = null,
|
|
||||||
|
|
||||||
var publicKey: String? = null,
|
|
||||||
var shortId: String? = null,
|
|
||||||
var spiderX: String? = null,
|
|
||||||
|
|
||||||
var secretKey: String? = null,
|
|
||||||
var preSharedKey: String? = null,
|
|
||||||
var localAddress: String? = null,
|
|
||||||
var reserved: String? = null,
|
|
||||||
var mtu: Int? = null,
|
|
||||||
|
|
||||||
var obfsPassword: String? = null,
|
|
||||||
var portHopping: String? = null,
|
|
||||||
var portHoppingInterval: String? = null,
|
|
||||||
var pinSHA256: String? = null,
|
|
||||||
var bandwidthDown: String? = null,
|
|
||||||
var bandwidthUp: String? = null,
|
|
||||||
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun create(configType: EConfigType): ProfileItem {
|
|
||||||
return ProfileItem(configType = configType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllOutboundTags(): MutableList<String> {
|
|
||||||
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerAddressAndPort(): String {
|
|
||||||
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
|
|
||||||
return "$LOOPBACK:$PORT_SOCKS"
|
|
||||||
}
|
|
||||||
return Utils.getIpv6Address(server) + ":" + serverPort
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other == null) return false
|
|
||||||
val obj = other as ProfileItem
|
|
||||||
|
|
||||||
return (this.server == obj.server
|
|
||||||
&& this.serverPort == obj.serverPort
|
|
||||||
&& this.password == obj.password
|
|
||||||
&& this.method == obj.method
|
|
||||||
&& this.flow == obj.flow
|
|
||||||
&& this.username == obj.username
|
|
||||||
|
|
||||||
&& this.network == obj.network
|
|
||||||
&& this.headerType == obj.headerType
|
|
||||||
&& this.host == obj.host
|
|
||||||
&& this.path == obj.path
|
|
||||||
&& this.seed == obj.seed
|
|
||||||
&& this.quicSecurity == obj.quicSecurity
|
|
||||||
&& this.quicKey == obj.quicKey
|
|
||||||
&& this.mode == obj.mode
|
|
||||||
&& this.serviceName == obj.serviceName
|
|
||||||
&& this.authority == obj.authority
|
|
||||||
&& this.xhttpMode == obj.xhttpMode
|
|
||||||
|
|
||||||
&& this.security == obj.security
|
|
||||||
&& this.sni == obj.sni
|
|
||||||
&& this.alpn == obj.alpn
|
|
||||||
&& this.fingerPrint == obj.fingerPrint
|
|
||||||
&& this.publicKey == obj.publicKey
|
|
||||||
&& this.shortId == obj.shortId
|
|
||||||
|
|
||||||
&& this.secretKey == obj.secretKey
|
|
||||||
&& this.localAddress == obj.localAddress
|
|
||||||
&& this.reserved == obj.reserved
|
|
||||||
&& this.mtu == obj.mtu
|
|
||||||
|
|
||||||
&& this.obfsPassword == obj.obfsPassword
|
|
||||||
&& this.portHopping == obj.portHopping
|
|
||||||
&& this.portHoppingInterval == obj.portHoppingInterval
|
|
||||||
&& this.pinSHA256 == obj.pinSHA256
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
enum class RoutingType(val fileName: String) {
|
|
||||||
WHITE("custom_routing_white"),
|
|
||||||
BLACK("custom_routing_black"),
|
|
||||||
GLOBAL("custom_routing_global"),
|
|
||||||
WHITE_IRAN("custom_routing_white_iran");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromIndex(index: Int): RoutingType {
|
|
||||||
return when (index) {
|
|
||||||
0 -> WHITE
|
|
||||||
1 -> BLACK
|
|
||||||
2 -> GLOBAL
|
|
||||||
3 -> WHITE_IRAN
|
|
||||||
else -> WHITE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class RulesetItem(
|
|
||||||
var remarks: String? = "",
|
|
||||||
var ip: List<String>? = null,
|
|
||||||
var domain: List<String>? = null,
|
|
||||||
var outboundTag: String = "",
|
|
||||||
var port: String? = null,
|
|
||||||
var network: String? = null,
|
|
||||||
var protocol: List<String>? = null,
|
|
||||||
var enabled: Boolean = true,
|
|
||||||
var locked: Boolean? = false,
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ServersCache(
|
|
||||||
val guid: String,
|
|
||||||
val profile: ProfileItem
|
|
||||||
)
|
|
|
@ -1,556 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
data class V2rayConfig(
|
|
||||||
var remarks: String? = null,
|
|
||||||
var stats: Any? = null,
|
|
||||||
val log: LogBean,
|
|
||||||
var policy: PolicyBean? = null,
|
|
||||||
val inbounds: ArrayList<InboundBean>,
|
|
||||||
var outbounds: ArrayList<OutboundBean>,
|
|
||||||
var dns: DnsBean? = null,
|
|
||||||
val routing: RoutingBean,
|
|
||||||
val api: Any? = null,
|
|
||||||
val transport: Any? = null,
|
|
||||||
val reverse: Any? = null,
|
|
||||||
var fakedns: Any? = null,
|
|
||||||
val browserForwarder: Any? = null,
|
|
||||||
var observatory: Any? = null,
|
|
||||||
var burstObservatory: Any? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class LogBean(
|
|
||||||
val access: String? = null,
|
|
||||||
val error: String? = null,
|
|
||||||
var loglevel: String? = null,
|
|
||||||
val dnsLog: Boolean? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class InboundBean(
|
|
||||||
var tag: String,
|
|
||||||
var port: Int,
|
|
||||||
var protocol: String,
|
|
||||||
var listen: String? = null,
|
|
||||||
val settings: Any? = null,
|
|
||||||
val sniffing: SniffingBean? = null,
|
|
||||||
val streamSettings: Any? = null,
|
|
||||||
val allocate: Any? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class InSettingsBean(
|
|
||||||
val auth: String? = null,
|
|
||||||
val udp: Boolean? = null,
|
|
||||||
val userLevel: Int? = null,
|
|
||||||
val address: String? = null,
|
|
||||||
val port: Int? = null,
|
|
||||||
val network: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SniffingBean(
|
|
||||||
var enabled: Boolean,
|
|
||||||
val destOverride: ArrayList<String>,
|
|
||||||
val metadataOnly: Boolean? = null,
|
|
||||||
var routeOnly: Boolean? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class OutboundBean(
|
|
||||||
var tag: String = "proxy",
|
|
||||||
var protocol: String,
|
|
||||||
var settings: OutSettingsBean? = null,
|
|
||||||
var streamSettings: StreamSettingsBean? = null,
|
|
||||||
val proxySettings: Any? = null,
|
|
||||||
val sendThrough: String? = null,
|
|
||||||
var mux: MuxBean? = MuxBean(false)
|
|
||||||
) {
|
|
||||||
data class OutSettingsBean(
|
|
||||||
var vnext: List<VnextBean>? = null,
|
|
||||||
var fragment: FragmentBean? = null,
|
|
||||||
var noises: List<NoiseBean>? = null,
|
|
||||||
var servers: List<ServersBean>? = null,
|
|
||||||
/*Blackhole*/
|
|
||||||
var response: Response? = null,
|
|
||||||
/*DNS*/
|
|
||||||
val network: String? = null,
|
|
||||||
var address: Any? = null,
|
|
||||||
val port: Int? = null,
|
|
||||||
/*Freedom*/
|
|
||||||
var domainStrategy: String? = null,
|
|
||||||
val redirect: String? = null,
|
|
||||||
val userLevel: Int? = null,
|
|
||||||
/*Loopback*/
|
|
||||||
val inboundTag: String? = null,
|
|
||||||
/*Wireguard*/
|
|
||||||
var secretKey: String? = null,
|
|
||||||
val peers: List<WireGuardBean>? = null,
|
|
||||||
var reserved: List<Int>? = null,
|
|
||||||
var mtu: Int? = null,
|
|
||||||
var obfsPassword: String? = null,
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class VnextBean(
|
|
||||||
var address: String = "",
|
|
||||||
var port: Int = AppConfig.DEFAULT_PORT,
|
|
||||||
var users: List<UsersBean>
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class UsersBean(
|
|
||||||
var id: String = "",
|
|
||||||
var alterId: Int? = null,
|
|
||||||
var security: String? = null,
|
|
||||||
var level: Int = AppConfig.DEFAULT_LEVEL,
|
|
||||||
var encryption: String? = null,
|
|
||||||
var flow: String? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FragmentBean(
|
|
||||||
var packets: String? = null,
|
|
||||||
var length: String? = null,
|
|
||||||
var interval: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class NoiseBean(
|
|
||||||
var type: String? = null,
|
|
||||||
var packet: String? = null,
|
|
||||||
var delay: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ServersBean(
|
|
||||||
var address: String = "",
|
|
||||||
var method: String? = null,
|
|
||||||
var ota: Boolean = false,
|
|
||||||
var password: String? = null,
|
|
||||||
var port: Int = AppConfig.DEFAULT_PORT,
|
|
||||||
var level: Int = AppConfig.DEFAULT_LEVEL,
|
|
||||||
val email: String? = null,
|
|
||||||
var flow: String? = null,
|
|
||||||
val ivCheck: Boolean? = null,
|
|
||||||
var users: List<SocksUsersBean>? = null
|
|
||||||
) {
|
|
||||||
data class SocksUsersBean(
|
|
||||||
var user: String = "",
|
|
||||||
var pass: String = "",
|
|
||||||
var level: Int = AppConfig.DEFAULT_LEVEL
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Response(var type: String)
|
|
||||||
|
|
||||||
data class WireGuardBean(
|
|
||||||
var publicKey: String = "",
|
|
||||||
var preSharedKey: String? = null,
|
|
||||||
var endpoint: String = ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class StreamSettingsBean(
|
|
||||||
var network: String = AppConfig.DEFAULT_NETWORK,
|
|
||||||
var security: String? = null,
|
|
||||||
var tcpSettings: TcpSettingsBean? = null,
|
|
||||||
var kcpSettings: KcpSettingsBean? = null,
|
|
||||||
var wsSettings: WsSettingsBean? = null,
|
|
||||||
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
|
||||||
var xhttpSettings: XhttpSettingsBean? = null,
|
|
||||||
var httpSettings: HttpSettingsBean? = null,
|
|
||||||
var tlsSettings: TlsSettingsBean? = null,
|
|
||||||
var quicSettings: QuicSettingBean? = null,
|
|
||||||
var realitySettings: TlsSettingsBean? = null,
|
|
||||||
var grpcSettings: GrpcSettingsBean? = null,
|
|
||||||
var hy2steriaSettings: Hy2steriaSettingsBean? = null,
|
|
||||||
val dsSettings: Any? = null,
|
|
||||||
var sockopt: SockoptBean? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class TcpSettingsBean(
|
|
||||||
var header: HeaderBean = HeaderBean(),
|
|
||||||
val acceptProxyProtocol: Boolean? = null
|
|
||||||
) {
|
|
||||||
data class HeaderBean(
|
|
||||||
var type: String = "none",
|
|
||||||
var request: RequestBean? = null,
|
|
||||||
var response: Any? = null
|
|
||||||
) {
|
|
||||||
data class RequestBean(
|
|
||||||
var path: List<String> = ArrayList(),
|
|
||||||
var headers: HeadersBean = HeadersBean(),
|
|
||||||
val version: String? = null,
|
|
||||||
val method: String? = null
|
|
||||||
) {
|
|
||||||
data class HeadersBean(
|
|
||||||
var Host: List<String>? = ArrayList(),
|
|
||||||
@SerializedName("User-Agent")
|
|
||||||
val userAgent: List<String>? = null,
|
|
||||||
@SerializedName("Accept-Encoding")
|
|
||||||
val acceptEncoding: List<String>? = null,
|
|
||||||
val Connection: List<String>? = null,
|
|
||||||
val Pragma: String? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class KcpSettingsBean(
|
|
||||||
var mtu: Int = 1350,
|
|
||||||
var tti: Int = 50,
|
|
||||||
var uplinkCapacity: Int = 12,
|
|
||||||
var downlinkCapacity: Int = 100,
|
|
||||||
var congestion: Boolean = false,
|
|
||||||
var readBufferSize: Int = 1,
|
|
||||||
var writeBufferSize: Int = 1,
|
|
||||||
var header: HeaderBean = HeaderBean(),
|
|
||||||
var seed: String? = null
|
|
||||||
) {
|
|
||||||
data class HeaderBean(
|
|
||||||
var type: String = "none",
|
|
||||||
var domain: String? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class WsSettingsBean(
|
|
||||||
var path: String? = null,
|
|
||||||
var headers: HeadersBean = HeadersBean(),
|
|
||||||
val maxEarlyData: Int? = null,
|
|
||||||
val useBrowserForwarding: Boolean? = null,
|
|
||||||
val acceptProxyProtocol: Boolean? = null
|
|
||||||
) {
|
|
||||||
data class HeadersBean(var Host: String = "")
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HttpupgradeSettingsBean(
|
|
||||||
var path: String? = null,
|
|
||||||
var host: String? = null,
|
|
||||||
val acceptProxyProtocol: Boolean? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class XhttpSettingsBean(
|
|
||||||
var path: String? = null,
|
|
||||||
var host: String? = null,
|
|
||||||
var mode: String? = null,
|
|
||||||
var extra: Any? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class HttpSettingsBean(
|
|
||||||
var host: List<String> = ArrayList(),
|
|
||||||
var path: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class SockoptBean(
|
|
||||||
var TcpNoDelay: Boolean? = null,
|
|
||||||
var tcpKeepAliveIdle: Int? = null,
|
|
||||||
var tcpFastOpen: Boolean? = null,
|
|
||||||
var tproxy: String? = null,
|
|
||||||
var mark: Int? = null,
|
|
||||||
var dialerProxy: String? = null,
|
|
||||||
var domainStrategy: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TlsSettingsBean(
|
|
||||||
var allowInsecure: Boolean = false,
|
|
||||||
var serverName: String? = null,
|
|
||||||
val alpn: List<String>? = null,
|
|
||||||
val minVersion: String? = null,
|
|
||||||
val maxVersion: String? = null,
|
|
||||||
val preferServerCipherSuites: Boolean? = null,
|
|
||||||
val cipherSuites: String? = null,
|
|
||||||
val fingerprint: String? = null,
|
|
||||||
val certificates: List<Any>? = null,
|
|
||||||
val disableSystemRoot: Boolean? = null,
|
|
||||||
val enableSessionResumption: Boolean? = null,
|
|
||||||
// REALITY settings
|
|
||||||
val show: Boolean = false,
|
|
||||||
var publicKey: String? = null,
|
|
||||||
var shortId: String? = null,
|
|
||||||
var spiderX: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class QuicSettingBean(
|
|
||||||
var security: String = "none",
|
|
||||||
var key: String = "",
|
|
||||||
var header: HeaderBean = HeaderBean()
|
|
||||||
) {
|
|
||||||
data class HeaderBean(var type: String = "none")
|
|
||||||
}
|
|
||||||
|
|
||||||
data class GrpcSettingsBean(
|
|
||||||
var serviceName: String = "",
|
|
||||||
var authority: String? = null,
|
|
||||||
var multiMode: Boolean? = null,
|
|
||||||
var idle_timeout: Int? = null,
|
|
||||||
var health_check_timeout: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Hy2steriaSettingsBean(
|
|
||||||
var password: String? = null,
|
|
||||||
var use_udp_extension: Boolean? = true,
|
|
||||||
var congestion: Hy2CongestionBean? = null
|
|
||||||
) {
|
|
||||||
data class Hy2CongestionBean(
|
|
||||||
var type: String? = "bbr",
|
|
||||||
var up_mbps: Int? = null,
|
|
||||||
var down_mbps: Int? = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
data class MuxBean(
|
|
||||||
var enabled: Boolean,
|
|
||||||
var concurrency: Int? = null,
|
|
||||||
var xudpConcurrency: Int? = null,
|
|
||||||
var xudpProxyUDP443: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getServerAddress(): String? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.vnext?.first()?.address
|
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.first()?.address
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerPort(): Int? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.vnext?.first()?.port
|
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.first()?.port
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerAddressAndPort(): String {
|
|
||||||
val address = getServerAddress().orEmpty()
|
|
||||||
val port = getServerPort()
|
|
||||||
return Utils.getIpv6Address(address) + ":" + port
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPassword(): String? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.vnext?.first()?.users?.first()?.id
|
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.first()?.password
|
|
||||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|
||||||
) {
|
|
||||||
return settings?.servers?.first()?.users?.first()?.pass
|
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
return settings?.secretKey
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSecurityEncryption(): String? {
|
|
||||||
return when {
|
|
||||||
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security
|
|
||||||
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption
|
|
||||||
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTransportSettingDetails(): List<String?>? {
|
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
) {
|
|
||||||
val transport = streamSettings?.network ?: return null
|
|
||||||
return when (transport) {
|
|
||||||
NetworkType.TCP.type -> {
|
|
||||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
tcpSetting.header.type,
|
|
||||||
tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(),
|
|
||||||
tcpSetting.header.request?.path?.joinToString(",").orEmpty()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP.type -> {
|
|
||||||
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
kcpSetting.header.type,
|
|
||||||
"",
|
|
||||||
kcpSetting.seed.orEmpty()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.WS.type -> {
|
|
||||||
val wsSetting = streamSettings?.wsSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
wsSetting.headers.Host,
|
|
||||||
wsSetting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HTTP_UPGRADE.type -> {
|
|
||||||
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
httpupgradeSetting.host,
|
|
||||||
httpupgradeSetting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.XHTTP.type -> {
|
|
||||||
val xhttpSettings = streamSettings?.xhttpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
xhttpSettings.host,
|
|
||||||
xhttpSettings.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.H2.type -> {
|
|
||||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
"",
|
|
||||||
h2Setting.host.joinToString(","),
|
|
||||||
h2Setting.path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// "quic" -> {
|
|
||||||
// val quicSetting = streamSettings?.quicSettings ?: return null
|
|
||||||
// listOf(
|
|
||||||
// quicSetting.header.type,
|
|
||||||
// quicSetting.security,
|
|
||||||
// quicSetting.key
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC.type -> {
|
|
||||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
|
||||||
listOf(
|
|
||||||
if (grpcSetting.multiMode == true) "multi" else "gun",
|
|
||||||
grpcSetting.authority.orEmpty(),
|
|
||||||
grpcSetting.serviceName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean {
|
|
||||||
val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also {
|
|
||||||
streamSettings = it
|
|
||||||
}
|
|
||||||
|
|
||||||
val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also {
|
|
||||||
stream.sockopt = it
|
|
||||||
}
|
|
||||||
|
|
||||||
return sockopt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DnsBean(
|
|
||||||
var servers: ArrayList<Any>? = null,
|
|
||||||
var hosts: Map<String, Any>? = null,
|
|
||||||
val clientIp: String? = null,
|
|
||||||
val disableCache: Boolean? = null,
|
|
||||||
val queryStrategy: String? = null,
|
|
||||||
val tag: String? = null
|
|
||||||
) {
|
|
||||||
data class ServersBean(
|
|
||||||
var address: String = "",
|
|
||||||
var port: Int? = null,
|
|
||||||
var domains: List<String>? = null,
|
|
||||||
var expectIPs: List<String>? = null,
|
|
||||||
val clientIp: String? = null,
|
|
||||||
val skipFallback: Boolean? = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RoutingBean(
|
|
||||||
var domainStrategy: String,
|
|
||||||
var domainMatcher: String? = null,
|
|
||||||
var rules: ArrayList<RulesBean>,
|
|
||||||
val balancers: List<Any>? = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class RulesBean(
|
|
||||||
var type: String = "field",
|
|
||||||
var ip: ArrayList<String>? = null,
|
|
||||||
var domain: ArrayList<String>? = null,
|
|
||||||
var outboundTag: String = "",
|
|
||||||
var balancerTag: String? = null,
|
|
||||||
var port: String? = null,
|
|
||||||
val sourcePort: String? = null,
|
|
||||||
val network: String? = null,
|
|
||||||
val source: List<String>? = null,
|
|
||||||
val user: List<String>? = null,
|
|
||||||
var inboundTag: List<String>? = null,
|
|
||||||
val protocol: List<String>? = null,
|
|
||||||
val attrs: String? = null,
|
|
||||||
val domainMatcher: String? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class PolicyBean(
|
|
||||||
var levels: Map<String, LevelBean>,
|
|
||||||
var system: Any? = null
|
|
||||||
) {
|
|
||||||
data class LevelBean(
|
|
||||||
var handshake: Int? = null,
|
|
||||||
var connIdle: Int? = null,
|
|
||||||
var uplinkOnly: Int? = null,
|
|
||||||
var downlinkOnly: Int? = null,
|
|
||||||
val statsUserUplink: Boolean? = null,
|
|
||||||
val statsUserDownlink: Boolean? = null,
|
|
||||||
var bufferSize: Int? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FakednsBean(
|
|
||||||
var ipPool: String = "198.18.0.0/15",
|
|
||||||
var poolSize: Int = 10000
|
|
||||||
) // roughly 10 times smaller than total ip pool
|
|
||||||
|
|
||||||
fun getProxyOutbound(): OutboundBean? {
|
|
||||||
outbounds.forEach { outbound ->
|
|
||||||
EConfigType.entries.forEach {
|
|
||||||
if (outbound.protocol.equals(it.name, true)) {
|
|
||||||
return outbound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllProxyOutbound(): List<OutboundBean> {
|
|
||||||
return outbounds.filter { outbound ->
|
|
||||||
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +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 = ""
|
|
||||||
)
|
|
|
@ -1,39 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VPN interface address configuration enum class
|
|
||||||
* Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration.
|
|
||||||
* Each option provides client and router addresses to establish point-to-point VPN tunnels.
|
|
||||||
*/
|
|
||||||
enum class VpnInterfaceAddressConfig(
|
|
||||||
val displayName: String,
|
|
||||||
val ipv4Client: String,
|
|
||||||
val ipv4Router: String,
|
|
||||||
val ipv6Client: String,
|
|
||||||
val ipv6Router: String
|
|
||||||
) {
|
|
||||||
OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"),
|
|
||||||
OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"),
|
|
||||||
OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"),
|
|
||||||
OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"),
|
|
||||||
OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"),
|
|
||||||
OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"),
|
|
||||||
OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2");
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Retrieves the VPN interface address configuration based on the specified index.
|
|
||||||
*
|
|
||||||
* @param index The configuration index (0-based) corresponding to user selection
|
|
||||||
* @return The VpnInterfaceAddressConfig instance at the specified index,
|
|
||||||
* or OPTION_1 (default) if the index is out of bounds
|
|
||||||
*/
|
|
||||||
fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig {
|
|
||||||
return if (index in values().indices) {
|
|
||||||
values()[index]
|
|
||||||
} else {
|
|
||||||
OPTION_1 // Default to the first configuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,212 +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 org.json.JSONObject
|
|
||||||
import java.io.Serializable
|
|
||||||
import java.net.URI
|
|
||||||
import java.net.URLConnection
|
|
||||||
|
|
||||||
val Context.v2RayApplication: AngApplication?
|
|
||||||
get() = applicationContext as? AngApplication
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a toast message with the given resource ID.
|
|
||||||
*
|
|
||||||
* @param message The resource ID of the message to show.
|
|
||||||
*/
|
|
||||||
fun Context.toast(message: Int) {
|
|
||||||
Toasty.normal(this, message).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a toast message with the given text.
|
|
||||||
*
|
|
||||||
* @param message The text of the message to show.
|
|
||||||
*/
|
|
||||||
fun Context.toast(message: CharSequence) {
|
|
||||||
Toasty.normal(this, message).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a toast message with the given resource ID.
|
|
||||||
*
|
|
||||||
* @param message The resource ID of the message to show.
|
|
||||||
*/
|
|
||||||
fun Context.toastSuccess(message: Int) {
|
|
||||||
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a toast message with the given text.
|
|
||||||
*
|
|
||||||
* @param message The text of the message to show.
|
|
||||||
*/
|
|
||||||
fun Context.toastSuccess(message: CharSequence) {
|
|
||||||
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a toast message with the given resource ID.
|
|
||||||
*
|
|
||||||
* @param message The resource ID of the message to show.
|
|
||||||
*/
|
|
||||||
fun Context.toastError(message: Int) {
|
|
||||||
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a toast message with the given text.
|
|
||||||
*
|
|
||||||
* @param message The text of the message to show.
|
|
||||||
*/
|
|
||||||
fun Context.toastError(message: CharSequence) {
|
|
||||||
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts a key-value pair into the JSONObject.
|
|
||||||
*
|
|
||||||
* @param pair The key-value pair to put.
|
|
||||||
*/
|
|
||||||
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
|
|
||||||
put(pair.first, pair.second)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts multiple key-value pairs into the JSONObject.
|
|
||||||
*
|
|
||||||
* @param pairs The map of key-value pairs to put.
|
|
||||||
*/
|
|
||||||
fun JSONObject.putOpt(pairs: Map<String, Any?>) {
|
|
||||||
pairs.forEach { put(it.key, it.value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const val THRESHOLD = 1000L
|
|
||||||
const val DIVISOR = 1024.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a Long value to a speed string.
|
|
||||||
*
|
|
||||||
* @return The speed string.
|
|
||||||
*/
|
|
||||||
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a Long value to a traffic string.
|
|
||||||
*
|
|
||||||
* @return The traffic string.
|
|
||||||
*/
|
|
||||||
fun Long.toTrafficString(): String {
|
|
||||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
|
||||||
var size = this.toDouble()
|
|
||||||
var unitIndex = 0
|
|
||||||
while (size >= THRESHOLD && unitIndex < units.size - 1) {
|
|
||||||
size /= DIVISOR
|
|
||||||
unitIndex++
|
|
||||||
}
|
|
||||||
return String.format("%.1f %s", size, units[unitIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
val URLConnection.responseLength: Long
|
|
||||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
contentLengthLong
|
|
||||||
} else {
|
|
||||||
contentLength.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
val URI.idnHost: String
|
|
||||||
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all whitespace from the string.
|
|
||||||
*
|
|
||||||
* @return The string without whitespace.
|
|
||||||
*/
|
|
||||||
fun String?.removeWhiteSpace(): String? = this?.replace(" ", "")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the string to a Long value, or returns 0 if the conversion fails.
|
|
||||||
*
|
|
||||||
* @return The Long value.
|
|
||||||
*/
|
|
||||||
fun String.toLongEx(): Long = toLongOrNull() ?: 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listens for package changes and executes a callback when a change occurs.
|
|
||||||
*
|
|
||||||
* @param onetime Whether to unregister the receiver after the first callback.
|
|
||||||
* @param callback The callback to execute when a package change occurs.
|
|
||||||
* @return The BroadcastReceiver that was registered.
|
|
||||||
*/
|
|
||||||
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
|
||||||
object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
callback()
|
|
||||||
if (onetime) context.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
}.apply {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
registerReceiver(this, IntentFilter().apply {
|
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
|
||||||
addDataScheme("package")
|
|
||||||
}, Context.RECEIVER_EXPORTED)
|
|
||||||
} else {
|
|
||||||
registerReceiver(this, IntentFilter().apply {
|
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
|
||||||
addDataScheme("package")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a serializable object from the Bundle.
|
|
||||||
*
|
|
||||||
* @param key The key of the serializable object.
|
|
||||||
* @return The serializable object, or null if not found.
|
|
||||||
*/
|
|
||||||
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
|
|
||||||
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a serializable object from the Intent.
|
|
||||||
*
|
|
||||||
* @param key The key of the serializable object.
|
|
||||||
* @return The serializable object, or null if not found.
|
|
||||||
*/
|
|
||||||
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
|
|
||||||
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the CharSequence is not null and not empty.
|
|
||||||
*
|
|
||||||
* @return True if the CharSequence is not null and not empty, false otherwise.
|
|
||||||
*/
|
|
||||||
fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty()
|
|
||||||
|
|
||||||
fun String.concatUrl(vararg paths: String): String {
|
|
||||||
val builder = StringBuilder(this.trimEnd('/'))
|
|
||||||
|
|
||||||
paths.forEach { path ->
|
|
||||||
val trimmedPath = path.trim('/')
|
|
||||||
if (trimmedPath.isNotEmpty()) {
|
|
||||||
builder.append('/').append(trimmedPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
|
|
||||||
object CustomFmt : FmtBase() {
|
|
||||||
/**
|
|
||||||
* Parses a JSON string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the JSON string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
|
||||||
|
|
||||||
val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
|
|
||||||
val outbound = fullConfig.getProxyOutbound()
|
|
||||||
|
|
||||||
config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
|
||||||
config.server = outbound?.getServerAddress()
|
|
||||||
config.serverPort = outbound?.getServerPort().toString()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.NetworkType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
open class FmtBase {
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a URI string.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @param userInfo the user information to include in the URI
|
|
||||||
* @param dicQuery the query parameters to include in the URI
|
|
||||||
* @return the converted URI string
|
|
||||||
*/
|
|
||||||
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
|
||||||
val query = if (dicQuery != null)
|
|
||||||
"?" + dicQuery.toList().joinToString(
|
|
||||||
separator = "&",
|
|
||||||
transform = { it.first + "=" + Utils.urlEncode(it.second) })
|
|
||||||
else ""
|
|
||||||
|
|
||||||
val url = String.format(
|
|
||||||
"%s@%s:%s",
|
|
||||||
Utils.urlEncode(userInfo ?: ""),
|
|
||||||
Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
|
|
||||||
config.serverPort
|
|
||||||
)
|
|
||||||
|
|
||||||
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts query parameters from a URI.
|
|
||||||
*
|
|
||||||
* @param uri the URI to extract query parameters from
|
|
||||||
* @return a map of query parameters
|
|
||||||
*/
|
|
||||||
fun getQueryParam(uri: URI): Map<String, String> {
|
|
||||||
return uri.rawQuery.split("&")
|
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates a ProfileItem object with values from query parameters.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to populate
|
|
||||||
* @param queryParam the query parameters to use for populating the ProfileItem
|
|
||||||
* @param allowInsecure whether to allow insecure connections
|
|
||||||
*/
|
|
||||||
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
|
|
||||||
config.network = queryParam["type"] ?: NetworkType.TCP.type
|
|
||||||
config.headerType = queryParam["headerType"]
|
|
||||||
config.host = queryParam["host"]
|
|
||||||
config.path = queryParam["path"]
|
|
||||||
|
|
||||||
config.seed = queryParam["seed"]
|
|
||||||
config.quicSecurity = queryParam["quicSecurity"]
|
|
||||||
config.quicKey = queryParam["key"]
|
|
||||||
config.mode = queryParam["mode"]
|
|
||||||
config.serviceName = queryParam["serviceName"]
|
|
||||||
config.authority = queryParam["authority"]
|
|
||||||
config.xhttpMode = queryParam["mode"]
|
|
||||||
config.xhttpExtra = queryParam["extra"]
|
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
|
||||||
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
|
|
||||||
config.security = null
|
|
||||||
}
|
|
||||||
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
|
||||||
allowInsecure
|
|
||||||
} else {
|
|
||||||
queryParam["allowInsecure"].orEmpty() == "1"
|
|
||||||
}
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.fingerPrint = queryParam["fp"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
config.publicKey = queryParam["pbk"]
|
|
||||||
config.shortId = queryParam["sid"]
|
|
||||||
config.spiderX = queryParam["spx"]
|
|
||||||
config.flow = queryParam["flow"]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a map of query parameters from a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to create query parameters from
|
|
||||||
* @return a map of query parameters
|
|
||||||
*/
|
|
||||||
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
|
||||||
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
|
||||||
config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() }
|
|
||||||
config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
|
|
||||||
config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
|
|
||||||
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
|
|
||||||
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
|
|
||||||
|
|
||||||
val networkType = NetworkType.fromString(config.network)
|
|
||||||
dicQuery["type"] = networkType.type
|
|
||||||
|
|
||||||
when (networkType) {
|
|
||||||
NetworkType.TCP -> {
|
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP -> {
|
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
|
||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.XHTTP -> {
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
|
||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
|
||||||
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
|
||||||
config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HTTP, NetworkType.H2 -> {
|
|
||||||
dicQuery["type"] = "http"
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
|
||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkType.QUIC -> {
|
|
||||||
// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
|
||||||
// config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
|
|
||||||
// config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC -> {
|
|
||||||
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
|
||||||
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
|
|
||||||
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dicQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerAddress(profileItem: ProfileItem): String {
|
|
||||||
if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
|
|
||||||
return profileItem.server.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
|
|
||||||
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
|
|
||||||
return domain
|
|
||||||
}
|
|
||||||
//Resolve and replace domain
|
|
||||||
val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
|
||||||
if (resolvedIps.isNullOrEmpty()) {
|
|
||||||
return domain
|
|
||||||
}
|
|
||||||
return resolvedIps.first()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
|
|
||||||
object HttpFmt : FmtBase() {
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
|
||||||
*
|
|
||||||
* @param profileItem the ProfileItem object to convert
|
|
||||||
* @return the converted OutboundBean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
|
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
|
||||||
server.address = getServerAddress(profileItem)
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
|
||||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
|
||||||
socksUsersBean.user = profileItem.username.orEmpty()
|
|
||||||
socksUsersBean.pass = profileItem.password.orEmpty()
|
|
||||||
server.users = listOf(socksUsersBean)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.Hysteria2Bean
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object Hysteria2Fmt : FmtBase() {
|
|
||||||
/**
|
|
||||||
* Parses a Hysteria2 URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the Hysteria2 URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
config.security = AppConfig.TLS
|
|
||||||
|
|
||||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.security = queryParam["security"] ?: AppConfig.TLS
|
|
||||||
config.insecure = if (queryParam["insecure"].isNullOrEmpty()) {
|
|
||||||
allowInsecure
|
|
||||||
} else {
|
|
||||||
queryParam["insecure"].orEmpty() == "1"
|
|
||||||
}
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
|
|
||||||
config.obfsPassword = queryParam["obfs-password"]
|
|
||||||
config.portHopping = queryParam["mport"]
|
|
||||||
config.pinSHA256 = queryParam["pinSHA256"]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a URI string.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @return the converted URI string
|
|
||||||
*/
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
|
|
||||||
config.security.let { if (it != null) dicQuery["security"] = it }
|
|
||||||
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
|
||||||
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
|
||||||
config.insecure.let { dicQuery["insecure"] = if (it == true) "1" else "0" }
|
|
||||||
|
|
||||||
if (config.obfsPassword.isNotNullEmpty()) {
|
|
||||||
dicQuery["obfs"] = "salamander"
|
|
||||||
dicQuery["obfs-password"] = config.obfsPassword.orEmpty()
|
|
||||||
}
|
|
||||||
if (config.portHopping.isNotNullEmpty()) {
|
|
||||||
dicQuery["mport"] = config.portHopping.orEmpty()
|
|
||||||
}
|
|
||||||
if (config.pinSHA256.isNotNullEmpty()) {
|
|
||||||
dicQuery["pinSHA256"] = config.pinSHA256.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a Hysteria2Bean object.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @param socksPort the port number for the socks5 proxy
|
|
||||||
* @return the converted Hysteria2Bean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
|
|
||||||
|
|
||||||
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
|
|
||||||
Hysteria2Bean.ObfsBean(
|
|
||||||
type = "salamander",
|
|
||||||
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
|
|
||||||
password = config.obfsPassword
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val transport = if (config.portHopping.isNullOrEmpty()) null else
|
|
||||||
Hysteria2Bean.TransportBean(
|
|
||||||
type = "udp",
|
|
||||||
udp = Hysteria2Bean.TransportBean.TransportUdpBean(
|
|
||||||
hopInterval = (config.portHoppingInterval ?: "30") + "s"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val bandwidth = if (config.bandwidthDown.isNullOrEmpty() || config.bandwidthUp.isNullOrEmpty()) null else
|
|
||||||
Hysteria2Bean.BandwidthBean(
|
|
||||||
down = config.bandwidthDown,
|
|
||||||
up = config.bandwidthUp,
|
|
||||||
)
|
|
||||||
|
|
||||||
val server =
|
|
||||||
if (config.portHopping.isNullOrEmpty())
|
|
||||||
config.getServerAddressAndPort()
|
|
||||||
else
|
|
||||||
Utils.getIpv6Address(config.server) + ":" + config.portHopping
|
|
||||||
|
|
||||||
val bean = Hysteria2Bean(
|
|
||||||
server = server,
|
|
||||||
auth = config.password,
|
|
||||||
obfs = obfs,
|
|
||||||
transport = transport,
|
|
||||||
bandwidth = bandwidth,
|
|
||||||
socks5 = Hysteria2Bean.Socks5Bean(
|
|
||||||
listen = "$LOOPBACK:${socksPort}",
|
|
||||||
),
|
|
||||||
http = Hysteria2Bean.Socks5Bean(
|
|
||||||
listen = "$LOOPBACK:${socksPort}",
|
|
||||||
),
|
|
||||||
tls = Hysteria2Bean.TlsBean(
|
|
||||||
sni = config.sni ?: config.server,
|
|
||||||
insecure = config.insecure,
|
|
||||||
pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return bean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
|
||||||
*
|
|
||||||
* @param profileItem the ProfileItem object to convert
|
|
||||||
* @return the converted OutboundBean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.NetworkType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object ShadowsocksFmt : FmtBase() {
|
|
||||||
/**
|
|
||||||
* Parses a Shadowsocks URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the Shadowsocks URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
return parseSip002(str) ?: parseLegacy(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a SIP002 Shadowsocks URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the SIP002 Shadowsocks URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parseSip002(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.idnHost.isEmpty()) return null
|
|
||||||
if (uri.port <= 0) return null
|
|
||||||
if (uri.userInfo.isNullOrEmpty()) return null
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
|
|
||||||
val result = if (uri.userInfo.contains(":")) {
|
|
||||||
uri.userInfo.split(":", limit = 2)
|
|
||||||
} else {
|
|
||||||
Utils.decode(uri.userInfo).split(":", limit = 2)
|
|
||||||
}
|
|
||||||
if (result.count() == 2) {
|
|
||||||
config.method = result.first()
|
|
||||||
config.password = result.last()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!uri.rawQuery.isNullOrEmpty()) {
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
if (queryParam["plugin"]?.contains("obfs=http") == true) {
|
|
||||||
val queryPairs = HashMap<String, String>()
|
|
||||||
for (pair in queryParam["plugin"]?.split(";") ?: listOf()) {
|
|
||||||
val idx = pair.split("=")
|
|
||||||
if (idx.count() == 2) {
|
|
||||||
queryPairs.put(idx.first(), idx.last())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.network = NetworkType.TCP.type
|
|
||||||
config.headerType = "http"
|
|
||||||
config.host = queryPairs["obfs-host"]
|
|
||||||
config.path = queryPairs["path"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a legacy Shadowsocks URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the legacy Shadowsocks URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parseLegacy(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
|
||||||
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
|
||||||
val indexSplit = result.indexOf("#")
|
|
||||||
if (indexSplit > 0) {
|
|
||||||
try {
|
|
||||||
config.remarks =
|
|
||||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = result.substring(0, indexSplit)
|
|
||||||
}
|
|
||||||
|
|
||||||
//part decode
|
|
||||||
val indexS = result.indexOf("@")
|
|
||||||
result = if (indexS > 0) {
|
|
||||||
Utils.decode(result.substring(0, indexS)) + result.substring(
|
|
||||||
indexS,
|
|
||||||
result.length
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Utils.decode(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
|
|
||||||
val match = legacyPattern.matchEntire(result) ?: return null
|
|
||||||
|
|
||||||
config.server = match.groupValues[3].removeSurrounding("[", "]")
|
|
||||||
config.serverPort = match.groupValues[4]
|
|
||||||
config.password = match.groupValues[2]
|
|
||||||
config.method = match.groupValues[1].lowercase()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a URI string.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @return the converted URI string
|
|
||||||
*/
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val pw = "${config.method}:${config.password}"
|
|
||||||
|
|
||||||
return toUri(config, Utils.encode(pw), null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
|
||||||
*
|
|
||||||
* @param profileItem the ProfileItem object to convert
|
|
||||||
* @return the converted OutboundBean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
|
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
|
||||||
server.address = getServerAddress(profileItem)
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
server.password = profileItem.password
|
|
||||||
server.method = profileItem.method
|
|
||||||
}
|
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.let {
|
|
||||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.let {
|
|
||||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object SocksFmt : FmtBase() {
|
|
||||||
/**
|
|
||||||
* Parses a Socks URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the Socks URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.SOCKS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.idnHost.isEmpty()) return null
|
|
||||||
if (uri.port <= 0) return null
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
|
|
||||||
if (uri.userInfo?.isEmpty() == false) {
|
|
||||||
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
|
|
||||||
if (result.count() == 2) {
|
|
||||||
config.username = result.first()
|
|
||||||
config.password = result.last()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a URI string.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @return the converted URI string
|
|
||||||
*/
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val pw =
|
|
||||||
if (config.username.isNotNullEmpty())
|
|
||||||
"${config.username}:${config.password}"
|
|
||||||
else
|
|
||||||
":"
|
|
||||||
|
|
||||||
return toUri(config, Utils.encode(pw), null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
|
||||||
*
|
|
||||||
* @param profileItem the ProfileItem object to convert
|
|
||||||
* @return the converted OutboundBean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
|
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
|
||||||
server.address = getServerAddress(profileItem)
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
|
||||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
|
||||||
socksUsersBean.user = profileItem.username.orEmpty()
|
|
||||||
socksUsersBean.pass = profileItem.password.orEmpty()
|
|
||||||
server.users = listOf(socksUsersBean)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.NetworkType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object TrojanFmt : FmtBase() {
|
|
||||||
/**
|
|
||||||
* Parses a Trojan URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the Trojan URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) {
|
|
||||||
config.network = NetworkType.TCP.type
|
|
||||||
config.security = AppConfig.TLS
|
|
||||||
config.insecure = allowInsecure
|
|
||||||
} else {
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
getItemFormQuery(config, queryParam, allowInsecure)
|
|
||||||
config.security = queryParam["security"] ?: AppConfig.TLS
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a URI string.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @return the converted URI string
|
|
||||||
*/
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val dicQuery = getQueryDic(config)
|
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
|
||||||
*
|
|
||||||
* @param profileItem the ProfileItem object to convert
|
|
||||||
* @return the converted OutboundBean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
|
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
|
||||||
server.address = getServerAddress(profileItem)
|
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
server.password = profileItem.password
|
|
||||||
server.flow = profileItem.flow
|
|
||||||
}
|
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.let {
|
|
||||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.let {
|
|
||||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object VlessFmt : FmtBase() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a Vless URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the Vless URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.VLESS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
config.method = queryParam["encryption"] ?: "none"
|
|
||||||
|
|
||||||
getItemFormQuery(config, queryParam, allowInsecure)
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a URI string.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @return the converted URI string
|
|
||||||
*/
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val dicQuery = getQueryDic(config)
|
|
||||||
dicQuery["encryption"] = config.method ?: "none"
|
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
|
||||||
*
|
|
||||||
* @param profileItem the ProfileItem object to convert
|
|
||||||
* @return the converted OutboundBean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
|
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
|
||||||
vnext.address = getServerAddress(profileItem)
|
|
||||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
vnext.users[0].id = profileItem.password.orEmpty()
|
|
||||||
vnext.users[0].encryption = profileItem.method
|
|
||||||
vnext.users[0].flow = profileItem.flow
|
|
||||||
}
|
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.let {
|
|
||||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.let {
|
|
||||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,192 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.NetworkType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.dto.VmessQRCode
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object VmessFmt : FmtBase() {
|
|
||||||
/**
|
|
||||||
* Parses a Vmess string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the Vmess string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
|
||||||
return parseVmessStd(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.VMESS)
|
|
||||||
|
|
||||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
|
||||||
result = Utils.decode(result)
|
|
||||||
if (TextUtils.isEmpty(result)) {
|
|
||||||
Log.w(AppConfig.TAG, "Toast decoding failed")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
|
|
||||||
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
|
||||||
if (TextUtils.isEmpty(vmessQRCode.add)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.port)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.id)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.net)
|
|
||||||
) {
|
|
||||||
Log.w(AppConfig.TAG, "Toast incorrect protocol")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
config.remarks = vmessQRCode.ps
|
|
||||||
config.server = vmessQRCode.add
|
|
||||||
config.serverPort = vmessQRCode.port
|
|
||||||
config.password = vmessQRCode.id
|
|
||||||
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
|
||||||
|
|
||||||
config.network = vmessQRCode.net ?: NetworkType.TCP.type
|
|
||||||
config.headerType = vmessQRCode.type
|
|
||||||
config.host = vmessQRCode.host
|
|
||||||
config.path = vmessQRCode.path
|
|
||||||
|
|
||||||
when (NetworkType.fromString(config.network)) {
|
|
||||||
NetworkType.KCP -> {
|
|
||||||
config.seed = vmessQRCode.path
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkType.QUIC -> {
|
|
||||||
// config.quicSecurity = vmessQRCode.host
|
|
||||||
// config.quicKey = vmessQRCode.path
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC -> {
|
|
||||||
config.mode = vmessQRCode.type
|
|
||||||
config.serviceName = vmessQRCode.path
|
|
||||||
config.authority = vmessQRCode.host
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.security = vmessQRCode.tls
|
|
||||||
config.insecure = allowInsecure
|
|
||||||
config.sni = vmessQRCode.sni
|
|
||||||
config.fingerPrint = vmessQRCode.fp
|
|
||||||
config.alpn = vmessQRCode.alpn
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a URI string.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @return the converted URI string
|
|
||||||
*/
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val vmessQRCode = VmessQRCode()
|
|
||||||
|
|
||||||
vmessQRCode.v = "2"
|
|
||||||
vmessQRCode.ps = config.remarks
|
|
||||||
vmessQRCode.add = config.server.orEmpty()
|
|
||||||
vmessQRCode.port = config.serverPort.orEmpty()
|
|
||||||
vmessQRCode.id = config.password.orEmpty()
|
|
||||||
vmessQRCode.scy = config.method.orEmpty()
|
|
||||||
vmessQRCode.aid = "0"
|
|
||||||
|
|
||||||
vmessQRCode.net = config.network.orEmpty()
|
|
||||||
vmessQRCode.type = config.headerType.orEmpty()
|
|
||||||
when (NetworkType.fromString(config.network)) {
|
|
||||||
NetworkType.KCP -> {
|
|
||||||
vmessQRCode.path = config.seed.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetworkType.QUIC -> {
|
|
||||||
// vmessQRCode.host = config.quicSecurity.orEmpty()
|
|
||||||
// vmessQRCode.path = config.quicKey.orEmpty()
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC -> {
|
|
||||||
vmessQRCode.type = config.mode.orEmpty()
|
|
||||||
vmessQRCode.path = config.serviceName.orEmpty()
|
|
||||||
vmessQRCode.host = config.authority.orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
|
|
||||||
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
|
|
||||||
|
|
||||||
vmessQRCode.tls = config.security.orEmpty()
|
|
||||||
vmessQRCode.sni = config.sni.orEmpty()
|
|
||||||
vmessQRCode.fp = config.fingerPrint.orEmpty()
|
|
||||||
vmessQRCode.alpn = config.alpn.orEmpty()
|
|
||||||
|
|
||||||
val json = JsonUtil.toJson(vmessQRCode)
|
|
||||||
return Utils.encode(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a standard Vmess URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the standard Vmess URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parseVmessStd(str: String): ProfileItem? {
|
|
||||||
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
|
||||||
val config = ProfileItem.create(EConfigType.VMESS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
config.password = uri.userInfo
|
|
||||||
config.method = AppConfig.DEFAULT_SECURITY
|
|
||||||
|
|
||||||
getItemFormQuery(config, queryParam, allowInsecure)
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
|
||||||
*
|
|
||||||
* @param profileItem the ProfileItem object to convert
|
|
||||||
* @return the converted OutboundBean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
|
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
|
||||||
vnext.address = getServerAddress(profileItem)
|
|
||||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
|
||||||
vnext.users[0].id = profileItem.password.orEmpty()
|
|
||||||
vnext.users[0].security = profileItem.method
|
|
||||||
}
|
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.let {
|
|
||||||
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.let {
|
|
||||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,149 +0,0 @@
|
||||||
package com.v2ray.ang.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.removeWhiteSpace
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object WireguardFmt : FmtBase() {
|
|
||||||
/**
|
|
||||||
* Parses a URI string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the URI string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parse(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
|
||||||
val queryParam = getQueryParam(uri)
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
|
||||||
config.server = uri.idnHost
|
|
||||||
config.serverPort = uri.port.toString()
|
|
||||||
|
|
||||||
config.secretKey = uri.userInfo.orEmpty()
|
|
||||||
config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
config.publicKey = queryParam["publickey"].orEmpty()
|
|
||||||
config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
|
|
||||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
|
||||||
config.reserved = queryParam["reserved"] ?: "0,0,0"
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a Wireguard configuration file string into a ProfileItem object.
|
|
||||||
*
|
|
||||||
* @param str the Wireguard configuration file string to parse
|
|
||||||
* @return the parsed ProfileItem object, or null if parsing fails
|
|
||||||
*/
|
|
||||||
fun parseWireguardConfFile(str: String): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
val interfaceParams: MutableMap<String, String> = mutableMapOf()
|
|
||||||
val peerParams: MutableMap<String, String> = mutableMapOf()
|
|
||||||
|
|
||||||
var currentSection: String? = null
|
|
||||||
|
|
||||||
str.lines().forEach { line ->
|
|
||||||
val trimmedLine = line.trim()
|
|
||||||
|
|
||||||
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
|
||||||
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
|
|
||||||
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
|
|
||||||
else -> {
|
|
||||||
if (currentSection != null) {
|
|
||||||
val parts = trimmedLine.split("=", limit = 2).map { it.trim() }
|
|
||||||
if (parts.size == 2) {
|
|
||||||
val key = parts[0].lowercase()
|
|
||||||
val value = parts[1]
|
|
||||||
when (currentSection) {
|
|
||||||
"Interface" -> interfaceParams[key] = value
|
|
||||||
"Peer" -> peerParams[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.secretKey = interfaceParams["privatekey"].orEmpty()
|
|
||||||
config.remarks = System.currentTimeMillis().toString()
|
|
||||||
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
|
||||||
config.publicKey = peerParams["publickey"].orEmpty()
|
|
||||||
config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
|
|
||||||
val endpoint = peerParams["endpoint"].orEmpty()
|
|
||||||
val endpointParts = endpoint.split(":", limit = 2)
|
|
||||||
if (endpointParts.size == 2) {
|
|
||||||
config.server = endpointParts[0]
|
|
||||||
config.serverPort = endpointParts[1]
|
|
||||||
} else {
|
|
||||||
config.server = endpoint
|
|
||||||
config.serverPort = ""
|
|
||||||
}
|
|
||||||
config.reserved = peerParams["reserved"] ?: "0,0,0"
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to an OutboundBean object.
|
|
||||||
*
|
|
||||||
* @param profileItem the ProfileItem object to convert
|
|
||||||
* @return the converted OutboundBean object, or null if conversion fails
|
|
||||||
*/
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
outboundBean?.settings?.let { wireguard ->
|
|
||||||
wireguard.secretKey = profileItem.secretKey
|
|
||||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
|
||||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
|
||||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
|
||||||
peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() }
|
|
||||||
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
|
||||||
}
|
|
||||||
wireguard.mtu = profileItem.mtu
|
|
||||||
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a ProfileItem object to a URI string.
|
|
||||||
*
|
|
||||||
* @param config the ProfileItem object to convert
|
|
||||||
* @return the converted URI string
|
|
||||||
*/
|
|
||||||
fun toUri(config: ProfileItem): String {
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
|
|
||||||
dicQuery["publickey"] = config.publicKey.orEmpty()
|
|
||||||
if (config.reserved != null) {
|
|
||||||
dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty()
|
|
||||||
}
|
|
||||||
dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty()
|
|
||||||
if (config.mtu != null) {
|
|
||||||
dicQuery["mtu"] = config.mtu.toString()
|
|
||||||
}
|
|
||||||
if (config.preSharedKey != null) {
|
|
||||||
dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
return toUri(config, config.secretKey, dicQuery)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,493 +0,0 @@
|
||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.HY2
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
|
||||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
|
||||||
import com.v2ray.ang.fmt.ShadowsocksFmt
|
|
||||||
import com.v2ray.ang.fmt.SocksFmt
|
|
||||||
import com.v2ray.ang.fmt.TrojanFmt
|
|
||||||
import com.v2ray.ang.fmt.VlessFmt
|
|
||||||
import com.v2ray.ang.fmt.VmessFmt
|
|
||||||
import com.v2ray.ang.fmt.WireguardFmt
|
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object AngConfigManager {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares the configuration to the clipboard.
|
|
||||||
*
|
|
||||||
* @param context The context.
|
|
||||||
* @param guid The GUID of the configuration.
|
|
||||||
* @return The result code.
|
|
||||||
*/
|
|
||||||
fun share2Clipboard(context: Context, guid: String): Int {
|
|
||||||
try {
|
|
||||||
val conf = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(conf)) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.setClipboard(context, conf)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to share config to clipboard", e)
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares non-custom configurations to the clipboard.
|
|
||||||
*
|
|
||||||
* @param context The context.
|
|
||||||
* @param serverList The list of server GUIDs.
|
|
||||||
* @return The number of configurations shared.
|
|
||||||
*/
|
|
||||||
fun shareNonCustomConfigsToClipboard(context: Context, serverList: List<String>): Int {
|
|
||||||
try {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (guid in serverList) {
|
|
||||||
val url = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(url)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sb.append(url)
|
|
||||||
sb.appendLine()
|
|
||||||
}
|
|
||||||
if (sb.count() > 0) {
|
|
||||||
Utils.setClipboard(context, sb.toString())
|
|
||||||
}
|
|
||||||
return sb.lines().count()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares the configuration as a QR code.
|
|
||||||
*
|
|
||||||
* @param guid The GUID of the configuration.
|
|
||||||
* @return The QR code bitmap.
|
|
||||||
*/
|
|
||||||
fun share2QRCode(guid: String): Bitmap? {
|
|
||||||
try {
|
|
||||||
val conf = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(conf)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return QRCodeDecoder.createQRCode(conf)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to share config as QR code", e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares the full content of the configuration to the clipboard.
|
|
||||||
*
|
|
||||||
* @param context The context.
|
|
||||||
* @param guid The GUID of the configuration.
|
|
||||||
* @return The result code.
|
|
||||||
*/
|
|
||||||
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
|
|
||||||
try {
|
|
||||||
if (guid == null) return -1
|
|
||||||
val result = V2rayConfigManager.getV2rayConfig(context, guid)
|
|
||||||
if (result.status) {
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid)
|
|
||||||
if (config?.configType == EConfigType.HYSTERIA2) {
|
|
||||||
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
|
|
||||||
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort)
|
|
||||||
Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
Utils.setClipboard(context, result.content)
|
|
||||||
} else {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares the configuration.
|
|
||||||
*
|
|
||||||
* @param guid The GUID of the configuration.
|
|
||||||
* @return The configuration string.
|
|
||||||
*/
|
|
||||||
private fun shareConfig(guid: String): String {
|
|
||||||
try {
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
|
|
||||||
|
|
||||||
return config.configType.protocolScheme + when (config.configType) {
|
|
||||||
EConfigType.VMESS -> VmessFmt.toUri(config)
|
|
||||||
EConfigType.CUSTOM -> ""
|
|
||||||
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
|
|
||||||
EConfigType.SOCKS -> SocksFmt.toUri(config)
|
|
||||||
EConfigType.HTTP -> ""
|
|
||||||
EConfigType.VLESS -> VlessFmt.toUri(config)
|
|
||||||
EConfigType.TROJAN -> TrojanFmt.toUri(config)
|
|
||||||
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
|
||||||
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports a batch of configurations.
|
|
||||||
*
|
|
||||||
* @param server The server string.
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
* @param append Whether to append the configurations.
|
|
||||||
* @return A pair containing the number of configurations and subscriptions imported.
|
|
||||||
*/
|
|
||||||
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
|
|
||||||
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseBatchConfig(server, subid, append)
|
|
||||||
}
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseCustomConfigServer(server, subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
var countSub = parseBatchSubscription(server)
|
|
||||||
if (countSub <= 0) {
|
|
||||||
countSub = parseBatchSubscription(Utils.decode(server))
|
|
||||||
}
|
|
||||||
if (countSub > 0) {
|
|
||||||
updateConfigViaSubAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
return count to countSub
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a batch of subscriptions.
|
|
||||||
*
|
|
||||||
* @param servers The servers string.
|
|
||||||
* @return The number of subscriptions parsed.
|
|
||||||
*/
|
|
||||||
private fun parseBatchSubscription(servers: String?): Int {
|
|
||||||
try {
|
|
||||||
if (servers == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = 0
|
|
||||||
servers.lines()
|
|
||||||
.distinct()
|
|
||||||
.forEach { str ->
|
|
||||||
if (Utils.isValidSubUrl(str)) {
|
|
||||||
count += importUrlAsSubscription(str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse batch subscription", e)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a batch of configurations.
|
|
||||||
*
|
|
||||||
* @param servers The servers string.
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
* @param append Whether to append the configurations.
|
|
||||||
* @return The number of configurations parsed.
|
|
||||||
*/
|
|
||||||
private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
|
|
||||||
try {
|
|
||||||
if (servers == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
val removedSelectedServer =
|
|
||||||
if (!TextUtils.isEmpty(subid) && !append) {
|
|
||||||
MmkvManager.decodeServerConfig(
|
|
||||||
MmkvManager.getSelectServer().orEmpty()
|
|
||||||
)?.let {
|
|
||||||
if (it.subscriptionId == subid) {
|
|
||||||
return@let it
|
|
||||||
}
|
|
||||||
return@let null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
|
||||||
var count = 0
|
|
||||||
servers.lines()
|
|
||||||
.distinct()
|
|
||||||
.reversed()
|
|
||||||
.forEach {
|
|
||||||
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
|
|
||||||
if (resId == 0) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a custom configuration server.
|
|
||||||
*
|
|
||||||
* @param server The server string.
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
* @return The number of configurations parsed.
|
|
||||||
*/
|
|
||||||
private fun parseCustomConfigServer(server: String?, subid: String): Int {
|
|
||||||
if (server == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (server.contains("inbounds")
|
|
||||||
&& server.contains("outbounds")
|
|
||||||
&& server.contains("routing")
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
val serverList: Array<Any> =
|
|
||||||
JsonUtil.fromJson(server, Array<Any>::class.java)
|
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
|
||||||
var count = 0
|
|
||||||
for (srv in serverList.reversed()) {
|
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// For compatibility
|
|
||||||
val config = CustomFmt.parse(server) ?: return 0
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
|
||||||
try {
|
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the configuration from a QR code or string.
|
|
||||||
*
|
|
||||||
* @param str The configuration string.
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
* @param subItem The subscription item.
|
|
||||||
* @param removedSelectedServer The removed selected server.
|
|
||||||
* @return The result code.
|
|
||||||
*/
|
|
||||||
private fun parseConfig(
|
|
||||||
str: String?,
|
|
||||||
subid: String,
|
|
||||||
subItem: SubscriptionItem?,
|
|
||||||
removedSelectedServer: ProfileItem?
|
|
||||||
): Int {
|
|
||||||
try {
|
|
||||||
if (str == null || TextUtils.isEmpty(str)) {
|
|
||||||
return R.string.toast_none_data
|
|
||||||
}
|
|
||||||
|
|
||||||
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
|
||||||
VmessFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
|
|
||||||
ShadowsocksFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
|
|
||||||
SocksFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
|
|
||||||
TrojanFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
|
|
||||||
VlessFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
|
|
||||||
WireguardFmt.parse(str)
|
|
||||||
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
|
|
||||||
Hysteria2Fmt.parse(str)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config == null) {
|
|
||||||
return R.string.toast_incorrect_protocol
|
|
||||||
}
|
|
||||||
//filter
|
|
||||||
if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) {
|
|
||||||
val matched = Regex(pattern = subItem.filter ?: "")
|
|
||||||
.containsMatchIn(input = config.remarks)
|
|
||||||
if (!matched) return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val guid = MmkvManager.encodeServerConfig("", config)
|
|
||||||
if (removedSelectedServer != null &&
|
|
||||||
config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort
|
|
||||||
) {
|
|
||||||
MmkvManager.setSelectServer(guid)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to parse config", e)
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the configuration via all subscriptions.
|
|
||||||
*
|
|
||||||
* @return The number of configurations updated.
|
|
||||||
*/
|
|
||||||
fun updateConfigViaSubAll(): Int {
|
|
||||||
var count = 0
|
|
||||||
try {
|
|
||||||
MmkvManager.decodeSubscriptions().forEach {
|
|
||||||
count += updateConfigViaSub(it)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the configuration via a subscription.
|
|
||||||
*
|
|
||||||
* @param it The subscription item.
|
|
||||||
* @return The number of configurations updated.
|
|
||||||
*/
|
|
||||||
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
|
|
||||||
try {
|
|
||||||
if (TextUtils.isEmpty(it.first)
|
|
||||||
|| TextUtils.isEmpty(it.second.remarks)
|
|
||||||
|| TextUtils.isEmpty(it.second.url)
|
|
||||||
) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (!it.second.enabled) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
val url = HttpUtil.toIdnUrl(it.second.url)
|
|
||||||
if (!Utils.isValidUrl(url)) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (!it.second.allowInsecureUrl) {
|
|
||||||
if (!Utils.isValidSubUrl(url)) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.i(AppConfig.TAG, url)
|
|
||||||
|
|
||||||
var configText = try {
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
if (configText.isEmpty()) {
|
|
||||||
configText = try {
|
|
||||||
HttpUtil.getUrlContentWithUserAgent(url)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (configText.isEmpty()) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return parseConfigViaSub(configText, it.first, false)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the configuration via a subscription.
|
|
||||||
*
|
|
||||||
* @param server The server string.
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
* @param append Whether to append the configurations.
|
|
||||||
* @return The number of configurations parsed.
|
|
||||||
*/
|
|
||||||
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
|
|
||||||
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseBatchConfig(server, subid, append)
|
|
||||||
}
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseCustomConfigServer(server, subid)
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports a URL as a subscription.
|
|
||||||
*
|
|
||||||
* @param url The URL.
|
|
||||||
* @return The number of subscriptions imported.
|
|
||||||
*/
|
|
||||||
private fun importUrlAsSubscription(url: String): Int {
|
|
||||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
|
||||||
subscriptions.forEach {
|
|
||||||
if (it.second.url == url) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(url))
|
|
||||||
val subItem = SubscriptionItem()
|
|
||||||
subItem.remarks = uri.fragment ?: "import sub"
|
|
||||||
subItem.url = url
|
|
||||||
MmkvManager.encodeSubscription("", subItem)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,242 +0,0 @@
|
||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.NetworkType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.extension.removeWhiteSpace
|
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
|
|
||||||
object MigrateManager {
|
|
||||||
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
|
||||||
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates server configurations to profile items.
|
|
||||||
*
|
|
||||||
* @return True if migration was successful, false otherwise.
|
|
||||||
*/
|
|
||||||
fun migrateServerConfig2Profile(): Boolean {
|
|
||||||
if (serverStorage.count().toInt() == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val serverList = serverStorage.allKeys() ?: return false
|
|
||||||
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + serverList.count())
|
|
||||||
|
|
||||||
for (guid in serverList) {
|
|
||||||
var configOld = decodeServerConfigOld(guid) ?: continue
|
|
||||||
var config = decodeServerConfig(guid)
|
|
||||||
if (config != null) {
|
|
||||||
serverStorage.remove(guid)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
config = migrateServerConfig2ProfileSub(configOld) ?: continue
|
|
||||||
config.subscriptionId = configOld.subscriptionId
|
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(guid, config)
|
|
||||||
|
|
||||||
//check and remove old
|
|
||||||
decodeServerConfig(guid) ?: continue
|
|
||||||
serverStorage.remove(guid)
|
|
||||||
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + config.remarks)
|
|
||||||
}
|
|
||||||
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-end")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates a server configuration to a profile item.
|
|
||||||
*
|
|
||||||
* @param configOld The old server configuration.
|
|
||||||
* @return The profile item.
|
|
||||||
*/
|
|
||||||
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
|
|
||||||
return when (configOld.getProxyOutbound()?.protocol) {
|
|
||||||
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
|
||||||
EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
|
||||||
EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld)
|
|
||||||
EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
|
||||||
|
|
||||||
EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld)
|
|
||||||
EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld)
|
|
||||||
EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld)
|
|
||||||
EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld)
|
|
||||||
|
|
||||||
EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld)
|
|
||||||
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates a common server configuration to a profile item.
|
|
||||||
*
|
|
||||||
* @param configOld The old server configuration.
|
|
||||||
* @return The profile item.
|
|
||||||
*/
|
|
||||||
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(configOld.configType)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
config.method = outbound.getSecurityEncryption()
|
|
||||||
config.password = outbound.getPassword()
|
|
||||||
config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
|
|
||||||
|
|
||||||
config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type
|
|
||||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
|
||||||
config.headerType = transportDetails[0].orEmpty()
|
|
||||||
config.host = transportDetails[1].orEmpty()
|
|
||||||
config.path = transportDetails[2].orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
config.seed = outbound?.streamSettings?.kcpSettings?.seed
|
|
||||||
config.quicSecurity = outbound?.streamSettings?.quicSettings?.security
|
|
||||||
config.quicKey = outbound?.streamSettings?.quicSettings?.key
|
|
||||||
config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun"
|
|
||||||
config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName
|
|
||||||
config.authority = outbound?.streamSettings?.grpcSettings?.authority
|
|
||||||
|
|
||||||
config.security = outbound.streamSettings?.security
|
|
||||||
val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings
|
|
||||||
config.insecure = tlsSettings?.allowInsecure
|
|
||||||
config.sni = tlsSettings?.serverName
|
|
||||||
config.fingerPrint = tlsSettings?.fingerprint
|
|
||||||
config.alpn = tlsSettings?.alpn?.joinToString(",").removeWhiteSpace().toString()
|
|
||||||
|
|
||||||
config.publicKey = tlsSettings?.publicKey
|
|
||||||
config.shortId = tlsSettings?.shortId
|
|
||||||
config.spiderX = tlsSettings?.spiderX
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates a SOCKS server configuration to a profile item.
|
|
||||||
*
|
|
||||||
* @param configOld The old server configuration.
|
|
||||||
* @return The profile item.
|
|
||||||
*/
|
|
||||||
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.SOCKS)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
|
|
||||||
config.password = outbound.getPassword()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates an HTTP server configuration to a profile item.
|
|
||||||
*
|
|
||||||
* @param configOld The old server configuration.
|
|
||||||
* @return The profile item.
|
|
||||||
*/
|
|
||||||
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.HTTP)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
|
|
||||||
config.password = outbound.getPassword()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates a WireGuard server configuration to a profile item.
|
|
||||||
*
|
|
||||||
* @param configOld The old server configuration.
|
|
||||||
* @return The profile item.
|
|
||||||
*/
|
|
||||||
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
|
|
||||||
outbound.settings?.let { wireguard ->
|
|
||||||
config.secretKey = wireguard.secretKey
|
|
||||||
config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString()
|
|
||||||
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
|
|
||||||
config.mtu = wireguard.mtu
|
|
||||||
config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString()
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates a Hysteria2 server configuration to a profile item.
|
|
||||||
*
|
|
||||||
* @param configOld The old server configuration.
|
|
||||||
* @return The profile item.
|
|
||||||
*/
|
|
||||||
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
config.password = outbound.getPassword()
|
|
||||||
|
|
||||||
config.security = AppConfig.TLS
|
|
||||||
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
|
|
||||||
config.insecure = tlsSetting.allowInsecure
|
|
||||||
config.sni = tlsSetting.serverName
|
|
||||||
config.alpn = tlsSetting.alpn?.joinToString(",").removeWhiteSpace().orEmpty()
|
|
||||||
|
|
||||||
}
|
|
||||||
config.obfsPassword = outbound.settings?.obfsPassword
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates a custom server configuration to a profile item.
|
|
||||||
*
|
|
||||||
* @param configOld The old server configuration.
|
|
||||||
* @return The profile item.
|
|
||||||
*/
|
|
||||||
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
|
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
|
||||||
|
|
||||||
val outbound = configOld.getProxyOutbound() ?: return null
|
|
||||||
config.remarks = configOld.remarks
|
|
||||||
config.server = outbound.getServerAddress()
|
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the old server configuration.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @return The old server configuration.
|
|
||||||
*/
|
|
||||||
private fun decodeServerConfigOld(guid: String): ServerConfig? {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val json = serverStorage.decodeString(guid)
|
|
||||||
if (json.isNullOrBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return JsonUtil.fromJson(json, ServerConfig::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,588 +0,0 @@
|
||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
|
|
||||||
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
|
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
|
||||||
import com.v2ray.ang.dto.ServerAffiliationInfo
|
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
object MmkvManager {
|
|
||||||
|
|
||||||
//region private
|
|
||||||
|
|
||||||
//private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
|
|
||||||
private const val ID_MAIN = "MAIN"
|
|
||||||
private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
|
|
||||||
private const val ID_SERVER_RAW = "SERVER_RAW"
|
|
||||||
private const val ID_SERVER_AFF = "SERVER_AFF"
|
|
||||||
private const val ID_SUB = "SUB"
|
|
||||||
private const val ID_ASSET = "ASSET"
|
|
||||||
private const val ID_SETTING = "SETTING"
|
|
||||||
private const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
|
|
||||||
private const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
|
|
||||||
private const val KEY_SUB_IDS = "SUB_IDS"
|
|
||||||
|
|
||||||
//private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Server
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the selected server GUID.
|
|
||||||
*
|
|
||||||
* @return The selected server GUID.
|
|
||||||
*/
|
|
||||||
fun getSelectServer(): String? {
|
|
||||||
return mainStorage.decodeString(KEY_SELECTED_SERVER)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the selected server GUID.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
*/
|
|
||||||
fun setSelectServer(guid: String) {
|
|
||||||
mainStorage.encode(KEY_SELECTED_SERVER, guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the server list.
|
|
||||||
*
|
|
||||||
* @param serverList The list of server GUIDs.
|
|
||||||
*/
|
|
||||||
fun encodeServerList(serverList: MutableList<String>) {
|
|
||||||
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the server list.
|
|
||||||
*
|
|
||||||
* @return The list of server GUIDs.
|
|
||||||
*/
|
|
||||||
fun decodeServerList(): MutableList<String> {
|
|
||||||
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
|
||||||
return if (json.isNullOrBlank()) {
|
|
||||||
mutableListOf()
|
|
||||||
} else {
|
|
||||||
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the server configuration.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @return The server configuration.
|
|
||||||
*/
|
|
||||||
fun decodeServerConfig(guid: String): ProfileItem? {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val json = profileFullStorage.decodeString(guid)
|
|
||||||
if (json.isNullOrBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return JsonUtil.fromJson(json, ProfileItem::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fun decodeProfileConfig(guid: String): ProfileLiteItem? {
|
|
||||||
// if (guid.isBlank()) {
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// val json = profileStorage.decodeString(guid)
|
|
||||||
// if (json.isNullOrBlank()) {
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the server configuration.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @param config The server configuration.
|
|
||||||
* @return The server GUID.
|
|
||||||
*/
|
|
||||||
fun encodeServerConfig(guid: String, config: ProfileItem): String {
|
|
||||||
val key = guid.ifBlank { Utils.getUuid() }
|
|
||||||
profileFullStorage.encode(key, JsonUtil.toJson(config))
|
|
||||||
val serverList = decodeServerList()
|
|
||||||
if (!serverList.contains(key)) {
|
|
||||||
serverList.add(0, key)
|
|
||||||
encodeServerList(serverList)
|
|
||||||
if (getSelectServer().isNullOrBlank()) {
|
|
||||||
mainStorage.encode(KEY_SELECTED_SERVER, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// val profile = ProfileLiteItem(
|
|
||||||
// configType = config.configType,
|
|
||||||
// subscriptionId = config.subscriptionId,
|
|
||||||
// remarks = config.remarks,
|
|
||||||
// server = config.getProxyOutbound()?.getServerAddress(),
|
|
||||||
// serverPort = config.getProxyOutbound()?.getServerPort(),
|
|
||||||
// )
|
|
||||||
// profileStorage.encode(key, JsonUtil.toJson(profile))
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the server configuration.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
*/
|
|
||||||
fun removeServer(guid: String) {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (getSelectServer() == guid) {
|
|
||||||
mainStorage.remove(KEY_SELECTED_SERVER)
|
|
||||||
}
|
|
||||||
val serverList = decodeServerList()
|
|
||||||
serverList.remove(guid)
|
|
||||||
encodeServerList(serverList)
|
|
||||||
profileFullStorage.remove(guid)
|
|
||||||
//profileStorage.remove(guid)
|
|
||||||
serverAffStorage.remove(guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the server configurations via subscription ID.
|
|
||||||
*
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
*/
|
|
||||||
fun removeServerViaSubid(subid: String) {
|
|
||||||
if (subid.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
profileFullStorage.allKeys()?.forEach { key ->
|
|
||||||
decodeServerConfig(key)?.let { config ->
|
|
||||||
if (config.subscriptionId == subid) {
|
|
||||||
removeServer(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the server affiliation information.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @return The server affiliation information.
|
|
||||||
*/
|
|
||||||
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val json = serverAffStorage.decodeString(guid)
|
|
||||||
if (json.isNullOrBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the server test delay in milliseconds.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @param testResult The test delay in milliseconds.
|
|
||||||
*/
|
|
||||||
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
|
|
||||||
aff.testDelayMillis = testResult
|
|
||||||
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all test delay results.
|
|
||||||
*
|
|
||||||
* @param keys The list of server GUIDs.
|
|
||||||
*/
|
|
||||||
fun clearAllTestDelayResults(keys: List<String>?) {
|
|
||||||
keys?.forEach { key ->
|
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
|
||||||
aff.testDelayMillis = 0
|
|
||||||
serverAffStorage.encode(key, JsonUtil.toJson(aff))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all server configurations.
|
|
||||||
*
|
|
||||||
* @return The number of server configurations removed.
|
|
||||||
*/
|
|
||||||
fun removeAllServer(): Int {
|
|
||||||
val count = profileFullStorage.allKeys()?.count() ?: 0
|
|
||||||
mainStorage.clearAll()
|
|
||||||
profileFullStorage.clearAll()
|
|
||||||
//profileStorage.clearAll()
|
|
||||||
serverAffStorage.clearAll()
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes invalid server configurations.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @return The number of server configurations removed.
|
|
||||||
*/
|
|
||||||
fun removeInvalidServer(guid: String): Int {
|
|
||||||
var count = 0
|
|
||||||
if (guid.isNotEmpty()) {
|
|
||||||
decodeServerAffiliationInfo(guid)?.let { aff ->
|
|
||||||
if (aff.testDelayMillis < 0L) {
|
|
||||||
removeServer(guid)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
serverAffStorage.allKeys()?.forEach { key ->
|
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
|
||||||
if (aff.testDelayMillis < 0L) {
|
|
||||||
removeServer(key)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the raw server configuration.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @param config The raw server configuration.
|
|
||||||
*/
|
|
||||||
fun encodeServerRaw(guid: String, config: String) {
|
|
||||||
serverRawStorage.encode(guid, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the raw server configuration.
|
|
||||||
*
|
|
||||||
* @param guid The server GUID.
|
|
||||||
* @return The raw server configuration.
|
|
||||||
*/
|
|
||||||
fun decodeServerRaw(guid: String): String? {
|
|
||||||
return serverRawStorage.decodeString(guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Subscriptions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the subscription list.
|
|
||||||
*/
|
|
||||||
private fun initSubsList() {
|
|
||||||
val subsList = decodeSubsList()
|
|
||||||
if (subsList.isNotEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
subStorage.allKeys()?.forEach { key ->
|
|
||||||
subsList.add(key)
|
|
||||||
}
|
|
||||||
encodeSubsList(subsList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the subscriptions.
|
|
||||||
*
|
|
||||||
* @return The list of subscriptions.
|
|
||||||
*/
|
|
||||||
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
|
|
||||||
initSubsList()
|
|
||||||
|
|
||||||
val subscriptions = mutableListOf<Pair<String, SubscriptionItem>>()
|
|
||||||
decodeSubsList().forEach { key ->
|
|
||||||
val json = subStorage.decodeString(key)
|
|
||||||
if (!json.isNullOrBlank()) {
|
|
||||||
subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return subscriptions
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the subscription.
|
|
||||||
*
|
|
||||||
* @param subid The subscription ID.
|
|
||||||
*/
|
|
||||||
fun removeSubscription(subid: String) {
|
|
||||||
subStorage.remove(subid)
|
|
||||||
val subsList = decodeSubsList()
|
|
||||||
subsList.remove(subid)
|
|
||||||
encodeSubsList(subsList)
|
|
||||||
|
|
||||||
removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the subscription.
|
|
||||||
*
|
|
||||||
* @param guid The subscription GUID.
|
|
||||||
* @param subItem The subscription item.
|
|
||||||
*/
|
|
||||||
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
|
|
||||||
val key = guid.ifBlank { Utils.getUuid() }
|
|
||||||
subStorage.encode(key, JsonUtil.toJson(subItem))
|
|
||||||
|
|
||||||
val subsList = decodeSubsList()
|
|
||||||
if (!subsList.contains(key)) {
|
|
||||||
subsList.add(key)
|
|
||||||
encodeSubsList(subsList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the subscription.
|
|
||||||
*
|
|
||||||
* @param subscriptionId The subscription ID.
|
|
||||||
* @return The subscription item.
|
|
||||||
*/
|
|
||||||
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
|
|
||||||
val json = subStorage.decodeString(subscriptionId) ?: return null
|
|
||||||
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the subscription list.
|
|
||||||
*
|
|
||||||
* @param subsList The list of subscription IDs.
|
|
||||||
*/
|
|
||||||
fun encodeSubsList(subsList: MutableList<String>) {
|
|
||||||
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the subscription list.
|
|
||||||
*
|
|
||||||
* @return The list of subscription IDs.
|
|
||||||
*/
|
|
||||||
fun decodeSubsList(): MutableList<String> {
|
|
||||||
val json = mainStorage.decodeString(KEY_SUB_IDS)
|
|
||||||
return if (json.isNullOrBlank()) {
|
|
||||||
mutableListOf()
|
|
||||||
} else {
|
|
||||||
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Asset
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the asset URLs.
|
|
||||||
*
|
|
||||||
* @return The list of asset URLs.
|
|
||||||
*/
|
|
||||||
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
|
||||||
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
|
||||||
assetStorage.allKeys()?.forEach { key ->
|
|
||||||
val json = assetStorage.decodeString(key)
|
|
||||||
if (!json.isNullOrBlank()) {
|
|
||||||
assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the asset URL.
|
|
||||||
*
|
|
||||||
* @param assetid The asset ID.
|
|
||||||
*/
|
|
||||||
fun removeAssetUrl(assetid: String) {
|
|
||||||
assetStorage.remove(assetid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the asset.
|
|
||||||
*
|
|
||||||
* @param assetid The asset ID.
|
|
||||||
* @param assetItem The asset item.
|
|
||||||
*/
|
|
||||||
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
|
|
||||||
val key = assetid.ifBlank { Utils.getUuid() }
|
|
||||||
assetStorage.encode(key, JsonUtil.toJson(assetItem))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the asset.
|
|
||||||
*
|
|
||||||
* @param assetid The asset ID.
|
|
||||||
* @return The asset item.
|
|
||||||
*/
|
|
||||||
fun decodeAsset(assetid: String): AssetUrlItem? {
|
|
||||||
val json = assetStorage.decodeString(assetid) ?: return null
|
|
||||||
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Routing
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the routing rulesets.
|
|
||||||
*
|
|
||||||
* @return The list of routing rulesets.
|
|
||||||
*/
|
|
||||||
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
|
||||||
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
|
||||||
if (ruleset.isNullOrEmpty()) return null
|
|
||||||
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the routing rulesets.
|
|
||||||
*
|
|
||||||
* @param rulesetList The list of routing rulesets.
|
|
||||||
*/
|
|
||||||
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
|
|
||||||
if (rulesetList.isNullOrEmpty())
|
|
||||||
encodeSettings(PREF_ROUTING_RULESET, "")
|
|
||||||
else
|
|
||||||
encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the settings.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param value The settings value.
|
|
||||||
* @return Whether the encoding was successful.
|
|
||||||
*/
|
|
||||||
fun encodeSettings(key: String, value: String?): Boolean {
|
|
||||||
return settingsStorage.encode(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the settings.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param value The settings value.
|
|
||||||
* @return Whether the encoding was successful.
|
|
||||||
*/
|
|
||||||
fun encodeSettings(key: String, value: Int): Boolean {
|
|
||||||
return settingsStorage.encode(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the settings.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param value The settings value.
|
|
||||||
* @return Whether the encoding was successful.
|
|
||||||
*/
|
|
||||||
fun encodeSettings(key: String, value: Boolean): Boolean {
|
|
||||||
return settingsStorage.encode(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the settings.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param value The settings value.
|
|
||||||
* @return Whether the encoding was successful.
|
|
||||||
*/
|
|
||||||
fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
|
|
||||||
return settingsStorage.encode(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the settings string.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @return The settings value.
|
|
||||||
*/
|
|
||||||
fun decodeSettingsString(key: String): String? {
|
|
||||||
return settingsStorage.decodeString(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the settings string.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param defaultValue The default value.
|
|
||||||
* @return The settings value.
|
|
||||||
*/
|
|
||||||
fun decodeSettingsString(key: String, defaultValue: String?): String? {
|
|
||||||
return settingsStorage.decodeString(key, defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the settings boolean.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @return The settings value.
|
|
||||||
*/
|
|
||||||
fun decodeSettingsBool(key: String): Boolean {
|
|
||||||
return settingsStorage.decodeBool(key, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the settings boolean.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @param defaultValue The default value.
|
|
||||||
* @return The settings value.
|
|
||||||
*/
|
|
||||||
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
|
|
||||||
return settingsStorage.decodeBool(key, defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the settings string set.
|
|
||||||
*
|
|
||||||
* @param key The settings key.
|
|
||||||
* @return The settings value.
|
|
||||||
*/
|
|
||||||
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
|
|
||||||
return settingsStorage.decodeStringSet(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
//region Others
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the start on boot setting.
|
|
||||||
*
|
|
||||||
* @param startOnBoot Whether to start on boot.
|
|
||||||
*/
|
|
||||||
fun encodeStartOnBoot(startOnBoot: Boolean) {
|
|
||||||
encodeSettings(PREF_IS_BOOTED, startOnBoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the start on boot setting.
|
|
||||||
*
|
|
||||||
* @return Whether to start on boot.
|
|
||||||
*/
|
|
||||||
fun decodeStartOnBoot(): Boolean {
|
|
||||||
return decodeSettingsBool(PREF_IS_BOOTED, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
//endregion
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,373 +0,0 @@
|
||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.AssetManager
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.Language
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.RoutingType
|
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.dto.VpnInterfaceAddressConfig
|
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerList
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.Collections
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object SettingsManager {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize routing rulesets.
|
|
||||||
* @param context The application context.
|
|
||||||
*/
|
|
||||||
fun initRoutingRulesets(context: Context) {
|
|
||||||
val exist = MmkvManager.decodeRoutingRulesets()
|
|
||||||
if (exist.isNullOrEmpty()) {
|
|
||||||
val rulesetList = getPresetRoutingRulesets(context)
|
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get preset routing rulesets.
|
|
||||||
* @param context The application context.
|
|
||||||
* @param index The index of the routing type.
|
|
||||||
* @return A mutable list of RulesetItem.
|
|
||||||
*/
|
|
||||||
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
|
|
||||||
val fileName = RoutingType.fromIndex(index).fileName
|
|
||||||
val assets = Utils.readTextFromAssets(context, fileName)
|
|
||||||
if (TextUtils.isEmpty(assets)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset routing rulesets from presets.
|
|
||||||
* @param context The application context.
|
|
||||||
* @param index The index of the routing type.
|
|
||||||
*/
|
|
||||||
fun resetRoutingRulesetsFromPresets(context: Context, index: Int) {
|
|
||||||
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
|
|
||||||
resetRoutingRulesetsCommon(rulesetList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset routing rulesets.
|
|
||||||
* @param content The content of the rulesets.
|
|
||||||
* @return True if successful, false otherwise.
|
|
||||||
*/
|
|
||||||
fun resetRoutingRulesets(content: String?): Boolean {
|
|
||||||
if (content.isNullOrEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java).toMutableList()
|
|
||||||
if (rulesetList.isNullOrEmpty()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
resetRoutingRulesetsCommon(rulesetList)
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common method to reset routing rulesets.
|
|
||||||
* @param rulesetList The list of rulesets.
|
|
||||||
*/
|
|
||||||
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
|
|
||||||
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
|
|
||||||
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
|
|
||||||
if (key.locked == true) {
|
|
||||||
rulesetNew.add(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rulesetNew.addAll(rulesetList)
|
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetNew)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a routing ruleset by index.
|
|
||||||
* @param index The index of the ruleset.
|
|
||||||
* @return The RulesetItem.
|
|
||||||
*/
|
|
||||||
fun getRoutingRuleset(index: Int): RulesetItem? {
|
|
||||||
if (index < 0) return null
|
|
||||||
|
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
|
||||||
if (rulesetList.isNullOrEmpty()) return null
|
|
||||||
|
|
||||||
return rulesetList[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a routing ruleset.
|
|
||||||
* @param index The index of the ruleset.
|
|
||||||
* @param ruleset The RulesetItem to save.
|
|
||||||
*/
|
|
||||||
fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
|
|
||||||
if (ruleset == null) return
|
|
||||||
|
|
||||||
var rulesetList = MmkvManager.decodeRoutingRulesets()
|
|
||||||
if (rulesetList.isNullOrEmpty()) {
|
|
||||||
rulesetList = mutableListOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 0 || index >= rulesetList.count()) {
|
|
||||||
rulesetList.add(0, ruleset)
|
|
||||||
} else {
|
|
||||||
rulesetList[index] = ruleset
|
|
||||||
}
|
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a routing ruleset by index.
|
|
||||||
* @param index The index of the ruleset.
|
|
||||||
*/
|
|
||||||
fun removeRoutingRuleset(index: Int) {
|
|
||||||
if (index < 0) return
|
|
||||||
|
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
|
||||||
if (rulesetList.isNullOrEmpty()) return
|
|
||||||
|
|
||||||
rulesetList.removeAt(index)
|
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if routing rulesets bypass LAN.
|
|
||||||
* @return True if bypassing LAN, false otherwise.
|
|
||||||
*/
|
|
||||||
fun routingRulesetsBypassLan(): Boolean {
|
|
||||||
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1"
|
|
||||||
if (vpnBypassLan == "1") {
|
|
||||||
return true
|
|
||||||
} else if (vpnBypassLan == "2") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val guid = MmkvManager.getSelectServer() ?: return false
|
|
||||||
val config = decodeServerConfig(guid) ?: return false
|
|
||||||
if (config.configType == EConfigType.CUSTOM) {
|
|
||||||
val raw = MmkvManager.decodeServerRaw(guid) ?: return false
|
|
||||||
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
|
|
||||||
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }.any {
|
|
||||||
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
|
|
||||||
}
|
|
||||||
return exist == true
|
|
||||||
}
|
|
||||||
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
|
||||||
val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any {
|
|
||||||
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
|
|
||||||
}
|
|
||||||
return exist == true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swap routing rulesets.
|
|
||||||
* @param fromPosition The position to swap from.
|
|
||||||
* @param toPosition The position to swap to.
|
|
||||||
*/
|
|
||||||
fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
|
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
|
||||||
if (rulesetList.isNullOrEmpty()) return
|
|
||||||
|
|
||||||
Collections.swap(rulesetList, fromPosition, toPosition)
|
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swap subscriptions.
|
|
||||||
* @param fromPosition The position to swap from.
|
|
||||||
* @param toPosition The position to swap to.
|
|
||||||
*/
|
|
||||||
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
|
|
||||||
val subsList = MmkvManager.decodeSubsList()
|
|
||||||
if (subsList.isNullOrEmpty()) return
|
|
||||||
|
|
||||||
Collections.swap(subsList, fromPosition, toPosition)
|
|
||||||
MmkvManager.encodeSubsList(subsList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get server via remarks.
|
|
||||||
* @param remarks The remarks of the server.
|
|
||||||
* @return The ProfileItem.
|
|
||||||
*/
|
|
||||||
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
|
||||||
if (remarks.isNullOrEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val serverList = decodeServerList()
|
|
||||||
for (guid in serverList) {
|
|
||||||
val profile = decodeServerConfig(guid)
|
|
||||||
if (profile != null && profile.remarks == remarks) {
|
|
||||||
return profile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the SOCKS port.
|
|
||||||
* @return The SOCKS port.
|
|
||||||
*/
|
|
||||||
fun getSocksPort(): Int {
|
|
||||||
return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the HTTP port.
|
|
||||||
* @return The HTTP port.
|
|
||||||
*/
|
|
||||||
fun getHttpPort(): Int {
|
|
||||||
return getSocksPort() + if (Utils.isXray()) 0 else 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize assets.
|
|
||||||
* @param context The application context.
|
|
||||||
* @param assets The AssetManager.
|
|
||||||
*/
|
|
||||||
fun initAssets(context: Context, assets: AssetManager) {
|
|
||||||
val extFolder = Utils.userAssetPath(context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val geo = arrayOf("geosite.dat", "geoip.dat")
|
|
||||||
assets.list("")
|
|
||||||
?.filter { geo.contains(it) }
|
|
||||||
?.filter { !File(extFolder, it).exists() }
|
|
||||||
?.forEach {
|
|
||||||
val target = File(extFolder, it)
|
|
||||||
assets.open(it).use { input ->
|
|
||||||
FileOutputStream(target).use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get domestic DNS servers from preference.
|
|
||||||
* @return A list of domestic DNS servers.
|
|
||||||
*/
|
|
||||||
fun getDomesticDnsServers(): List<String> {
|
|
||||||
val domesticDns =
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
|
|
||||||
val ret = domesticDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) }
|
|
||||||
if (ret.isEmpty()) {
|
|
||||||
return listOf(AppConfig.DNS_DIRECT)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get remote DNS servers from preference.
|
|
||||||
* @return A list of remote DNS servers.
|
|
||||||
*/
|
|
||||||
fun getRemoteDnsServers(): List<String> {
|
|
||||||
val remoteDns =
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
|
||||||
val ret = remoteDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) }
|
|
||||||
if (ret.isEmpty()) {
|
|
||||||
return listOf(AppConfig.DNS_PROXY)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get VPN DNS servers from preference.
|
|
||||||
* @return A list of VPN DNS servers.
|
|
||||||
*/
|
|
||||||
fun getVpnDnsServers(): List<String> {
|
|
||||||
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
|
|
||||||
return vpnDns.split(",").filter { Utils.isPureIpAddress(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get delay test URL.
|
|
||||||
* @param second Whether to use the second URL.
|
|
||||||
* @return The delay test URL.
|
|
||||||
*/
|
|
||||||
fun getDelayTestUrl(second: Boolean = false): String {
|
|
||||||
return if (second) {
|
|
||||||
AppConfig.DELAY_TEST_URL2
|
|
||||||
} else {
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
|
||||||
?: AppConfig.DELAY_TEST_URL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the locale.
|
|
||||||
* @return The locale.
|
|
||||||
*/
|
|
||||||
fun getLocale(): Locale {
|
|
||||||
val langCode =
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
|
|
||||||
val language = Language.fromCode(langCode)
|
|
||||||
|
|
||||||
return when (language) {
|
|
||||||
Language.AUTO -> Utils.getSysLocale()
|
|
||||||
Language.ENGLISH -> Locale.ENGLISH
|
|
||||||
Language.CHINA -> Locale.CHINA
|
|
||||||
Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
|
|
||||||
Language.VIETNAMESE -> Locale("vi")
|
|
||||||
Language.RUSSIAN -> Locale("ru")
|
|
||||||
Language.PERSIAN -> Locale("fa")
|
|
||||||
Language.ARABIC -> Locale("ar")
|
|
||||||
Language.BANGLA -> Locale("bn")
|
|
||||||
Language.BAKHTIARI -> Locale("bqi", "IR")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set night mode.
|
|
||||||
*/
|
|
||||||
fun setNightMode() {
|
|
||||||
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
|
||||||
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
|
||||||
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
|
||||||
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the currently selected VPN interface address configuration.
|
|
||||||
* This method reads the user's preference for VPN interface addressing and returns
|
|
||||||
* the corresponding configuration containing IPv4 and IPv6 addresses.
|
|
||||||
*
|
|
||||||
* @return The selected VpnInterfaceAddressConfig instance, or the default configuration
|
|
||||||
* if no valid selection is found or if the stored index is invalid.
|
|
||||||
*/
|
|
||||||
fun getCurrentVpnInterfaceAddressConfig(): VpnInterfaceAddressConfig {
|
|
||||||
val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt()
|
|
||||||
return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,189 +0,0 @@
|
||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.IPAPIInfo
|
|
||||||
import com.v2ray.ang.extension.responseLength
|
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import libv2ray.Libv2ray
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.Socket
|
|
||||||
import java.net.UnknownHostException
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
object SpeedtestManager {
|
|
||||||
|
|
||||||
private val tcpTestingSockets = ArrayList<Socket?>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the TCP connection time to a given URL and port.
|
|
||||||
*
|
|
||||||
* @param url The URL to connect to.
|
|
||||||
* @param port The port to connect to.
|
|
||||||
* @return The connection time in milliseconds, or -1 if the connection failed.
|
|
||||||
*/
|
|
||||||
suspend fun tcping(url: String, port: Int): Long {
|
|
||||||
var time = -1L
|
|
||||||
for (k in 0 until 2) {
|
|
||||||
val one = socketConnectTime(url, port)
|
|
||||||
if (!coroutineContext.isActive) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (one != -1L && (time == -1L || one < time)) {
|
|
||||||
time = one
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return time
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the real ping time using the V2Ray library.
|
|
||||||
*
|
|
||||||
* @param config The configuration string for the V2Ray library.
|
|
||||||
* @return The ping time in milliseconds, or -1 if the ping failed.
|
|
||||||
*/
|
|
||||||
fun realPing(config: String): Long {
|
|
||||||
return try {
|
|
||||||
Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to measure outbound delay", e)
|
|
||||||
-1L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the ping time to a given URL using the system ping command.
|
|
||||||
*
|
|
||||||
* @param url The URL to ping.
|
|
||||||
* @return The ping time in milliseconds as a string, or "-1ms" if the ping failed.
|
|
||||||
*/
|
|
||||||
fun ping(url: String): String {
|
|
||||||
try {
|
|
||||||
val command = "/system/bin/ping -c 3 $url"
|
|
||||||
val process = Runtime.getRuntime().exec(command)
|
|
||||||
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
|
||||||
if (!TextUtils.isEmpty(allText)) {
|
|
||||||
val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
|
|
||||||
val temps =
|
|
||||||
tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
|
||||||
if (temps.count() > 0 && temps[0].length < 10) {
|
|
||||||
return temps[0].toFloat().toInt().toString() + "ms"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to ping URL: $url", e)
|
|
||||||
}
|
|
||||||
return "-1ms"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the time taken to establish a TCP connection to a given URL and port.
|
|
||||||
*
|
|
||||||
* @param url The URL to connect to.
|
|
||||||
* @param port The port to connect to.
|
|
||||||
* @return The connection time in milliseconds, or -1 if the connection failed.
|
|
||||||
*/
|
|
||||||
fun socketConnectTime(url: String, port: Int): Long {
|
|
||||||
try {
|
|
||||||
val socket = Socket()
|
|
||||||
synchronized(this) {
|
|
||||||
tcpTestingSockets.add(socket)
|
|
||||||
}
|
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
socket.connect(InetSocketAddress(url, port), 3000)
|
|
||||||
val time = System.currentTimeMillis() - start
|
|
||||||
synchronized(this) {
|
|
||||||
tcpTestingSockets.remove(socket)
|
|
||||||
}
|
|
||||||
socket.close()
|
|
||||||
return time
|
|
||||||
} catch (e: UnknownHostException) {
|
|
||||||
Log.e(AppConfig.TAG, "Unknown host: $url", e)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(AppConfig.TAG, "socketConnectTime IOException: $e")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes all TCP sockets that are currently being tested.
|
|
||||||
*/
|
|
||||||
fun closeAllTcpSockets() {
|
|
||||||
synchronized(this) {
|
|
||||||
tcpTestingSockets.forEach {
|
|
||||||
it?.close()
|
|
||||||
}
|
|
||||||
tcpTestingSockets.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests the connection to a given URL and port.
|
|
||||||
*
|
|
||||||
* @param context The Context in which the test is running.
|
|
||||||
* @param port The port to connect to.
|
|
||||||
* @return A pair containing the elapsed time in milliseconds and the result message.
|
|
||||||
*/
|
|
||||||
fun testConnection(context: Context, port: Int): Pair<Long, String> {
|
|
||||||
var result: String
|
|
||||||
var elapsed = -1L
|
|
||||||
|
|
||||||
val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
|
|
||||||
try {
|
|
||||||
val start = SystemClock.elapsedRealtime()
|
|
||||||
val code = conn.responseCode
|
|
||||||
elapsed = SystemClock.elapsedRealtime() - start
|
|
||||||
|
|
||||||
if (code == 204 || code == 200 && conn.responseLength == 0L) {
|
|
||||||
result = context.getString(R.string.connection_test_available, elapsed)
|
|
||||||
} else {
|
|
||||||
throw IOException(
|
|
||||||
context.getString(
|
|
||||||
R.string.connection_test_error_status_code,
|
|
||||||
code
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(AppConfig.TAG, "Connection test IOException", e)
|
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Connection test Exception", e)
|
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
|
||||||
} finally {
|
|
||||||
conn.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair(elapsed, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRemoteIPInfo(): String? {
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null
|
|
||||||
|
|
||||||
var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
|
|
||||||
var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query
|
|
||||||
var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode
|
|
||||||
|
|
||||||
return "(${country ?: "unknown"}) $ip"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the version of the V2Ray library.
|
|
||||||
*
|
|
||||||
* @return The version of the V2Ray library.
|
|
||||||
*/
|
|
||||||
fun getLibVersion(): String {
|
|
||||||
return Libv2ray.checkVersionX()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
package com.v2ray.ang.handler
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.BuildConfig
|
|
||||||
import com.v2ray.ang.dto.CheckUpdateResult
|
|
||||||
import com.v2ray.ang.dto.GitHubRelease
|
|
||||||
import com.v2ray.ang.extension.concatUrl
|
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
object UpdateCheckerManager {
|
|
||||||
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
|
|
||||||
val url = if (includePreRelease) {
|
|
||||||
AppConfig.APP_API_URL
|
|
||||||
} else {
|
|
||||||
AppConfig.APP_API_URL.concatUrl("latest")
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = HttpUtil.getUrlContent(url, 5000)
|
|
||||||
if (response.isNullOrEmpty()) {
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response")
|
|
||||||
}
|
|
||||||
|
|
||||||
val latestRelease = if (includePreRelease) {
|
|
||||||
JsonUtil.fromJson(response, Array<GitHubRelease>::class.java)
|
|
||||||
.firstOrNull()
|
|
||||||
?: throw IllegalStateException("No pre-release found")
|
|
||||||
} else {
|
|
||||||
JsonUtil.fromJson(response, GitHubRelease::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
val latestVersion = latestRelease.tagName.removePrefix("v")
|
|
||||||
Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})")
|
|
||||||
|
|
||||||
return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) {
|
|
||||||
val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0])
|
|
||||||
CheckUpdateResult(
|
|
||||||
hasUpdate = true,
|
|
||||||
latestVersion = latestVersion,
|
|
||||||
releaseNotes = latestRelease.body,
|
|
||||||
downloadUrl = downloadUrl,
|
|
||||||
isPreRelease = latestRelease.prerelease
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
CheckUpdateResult(hasUpdate = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
|
|
||||||
?: throw IllegalStateException("Failed to create connection")
|
|
||||||
|
|
||||||
try {
|
|
||||||
val apkFile = File(context.cacheDir, "update.apk")
|
|
||||||
Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
|
|
||||||
|
|
||||||
FileOutputStream(apkFile).use { outputStream ->
|
|
||||||
connection.inputStream.use { inputStream ->
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.i(AppConfig.TAG, "APK download completed")
|
|
||||||
return@withContext apkFile
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
|
|
||||||
return@withContext null
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
connection.disconnect()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
|
|
||||||
return@withContext null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun compareVersions(version1: String, version2: String): Int {
|
|
||||||
val v1 = version1.split(".")
|
|
||||||
val v2 = version2.split(".")
|
|
||||||
|
|
||||||
for (i in 0 until maxOf(v1.size, v2.size)) {
|
|
||||||
val num1 = if (i < v1.size) v1[i].toInt() else 0
|
|
||||||
val num2 = if (i < v2.size) v2[i].toInt() else 0
|
|
||||||
if (num1 != num2) return num1 - num2
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDownloadUrl(release: GitHubRelease, abi: String): String {
|
|
||||||
return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl
|
|
||||||
?: release.assets.firstOrNull()?.browserDownloadUrl
|
|
||||||
?: throw IllegalStateException("No compatible APK found")
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,68 +0,0 @@
|
||||||
package com.v2ray.ang.helper
|
|
||||||
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.view.View
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class CustomDividerItemDecoration(
|
|
||||||
private val divider: Drawable,
|
|
||||||
private val orientation: Int
|
|
||||||
) : RecyclerView.ItemDecoration() {
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
|
||||||
if (orientation == RecyclerView.VERTICAL) {
|
|
||||||
drawVerticalDividers(canvas, parent)
|
|
||||||
} else {
|
|
||||||
drawHorizontalDividers(canvas, parent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun drawVerticalDividers(canvas: Canvas, parent: RecyclerView) {
|
|
||||||
val left = parent.paddingLeft
|
|
||||||
val right = parent.width - parent.paddingRight
|
|
||||||
|
|
||||||
val childCount = parent.childCount
|
|
||||||
for (i in 0 until childCount - 1) {
|
|
||||||
val child = parent.getChildAt(i)
|
|
||||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
|
||||||
|
|
||||||
val top = child.bottom + params.bottomMargin
|
|
||||||
val bottom = top + divider.intrinsicHeight
|
|
||||||
|
|
||||||
divider.setBounds(left, top, right, bottom)
|
|
||||||
divider.draw(canvas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun drawHorizontalDividers(canvas: Canvas, parent: RecyclerView) {
|
|
||||||
val top = parent.paddingTop
|
|
||||||
val bottom = parent.height - parent.paddingBottom
|
|
||||||
|
|
||||||
val childCount = parent.childCount
|
|
||||||
for (i in 0 until childCount - 1) {
|
|
||||||
val child = parent.getChildAt(i)
|
|
||||||
val params = child.layoutParams as RecyclerView.LayoutParams
|
|
||||||
|
|
||||||
val left = child.right + params.rightMargin
|
|
||||||
val right = left + divider.intrinsicWidth
|
|
||||||
|
|
||||||
divider.setBounds(left, top, right, bottom)
|
|
||||||
divider.draw(canvas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemOffsets(
|
|
||||||
outRect: Rect,
|
|
||||||
view: View,
|
|
||||||
parent: RecyclerView,
|
|
||||||
state: RecyclerView.State
|
|
||||||
) {
|
|
||||||
if (orientation == RecyclerView.VERTICAL) {
|
|
||||||
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
|
||||||
} else {
|
|
||||||
outRect.set(0, 0, divider.intrinsicWidth, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,41 +13,48 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.v2ray.ang.helper
|
|
||||||
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
|
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
||||||
*
|
*
|
||||||
* @author Paul Burke (ipaulpro)
|
* @author Paul Burke (ipaulpro)
|
||||||
*/
|
*/
|
||||||
interface ItemTouchHelperAdapter {
|
public interface ItemTouchHelperAdapter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
||||||
* an item is shifted, and **not** at the end of a "drop" event.<br></br>
|
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
|
||||||
* <br></br>
|
* <br/>
|
||||||
* Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
|
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
|
||||||
* adjusting the underlying data to reflect this move.
|
* adjusting the underlying data to reflect this move.
|
||||||
*
|
*
|
||||||
* @param fromPosition The start position of the moved item.
|
* @param fromPosition The start position of the moved item.
|
||||||
* @param toPosition Then resolved position of the moved item.
|
* @param toPosition Then resolved position of the moved item.
|
||||||
* @return True if the item was moved to the new adapter position.
|
* @return True if the item was moved to the new adapter position.
|
||||||
* @see RecyclerView.getAdapterPositionFor
|
*
|
||||||
* @see RecyclerView.ViewHolder.getAdapterPosition
|
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||||
|
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||||
*/
|
*/
|
||||||
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
|
boolean onItemMove(int fromPosition, int toPosition);
|
||||||
|
|
||||||
|
|
||||||
fun onItemMoveCompleted()
|
void onItemMoveCompleted();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item has been dismissed by a swipe.<br></br>
|
* Called when an item has been dismissed by a swipe.<br/>
|
||||||
* <br></br>
|
* <br/>
|
||||||
* Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
|
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
|
||||||
* adjusting the underlying data to reflect this removal.
|
* adjusting the underlying data to reflect this removal.
|
||||||
*
|
*
|
||||||
* @param position The position of the item dismissed.
|
* @param position The position of the item dismissed.
|
||||||
* @see RecyclerView.getAdapterPositionFor
|
*
|
||||||
* @see RecyclerView.ViewHolder.getAdapterPosition
|
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||||
|
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||||
*/
|
*/
|
||||||
fun onItemDismiss(position: Int)
|
void onItemDismiss(int position);
|
||||||
}
|
}
|
|
@ -13,26 +13,29 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.v2ray.ang.helper
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to notify an item ViewHolder of relevant callbacks from [ ].
|
* Interface to notify an item ViewHolder of relevant callbacks from {@link
|
||||||
|
* ItemTouchHelper.Callback}.
|
||||||
*
|
*
|
||||||
* @author Paul Burke (ipaulpro)
|
* @author Paul Burke (ipaulpro)
|
||||||
*/
|
*/
|
||||||
interface ItemTouchHelperViewHolder {
|
public interface ItemTouchHelperViewHolder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the [ItemTouchHelper] first registers an item as being moved or swiped.
|
* Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
|
||||||
* Implementations should update the item view to indicate it's active state.
|
* Implementations should update the item view to indicate it's active state.
|
||||||
*/
|
*/
|
||||||
fun onItemSelected()
|
void onItemSelected();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the [ItemTouchHelper] has completed the move or swipe, and the active item
|
* Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
|
||||||
* state should be cleared.
|
* state should be cleared.
|
||||||
*/
|
*/
|
||||||
fun onItemClear()
|
void onItemClear();
|
||||||
}
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Paul Burke
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for manual initiation of a drag.
|
||||||
|
*/
|
||||||
|
public interface OnStartDragListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a view is requesting a start of a drag.
|
||||||
|
*
|
||||||
|
* @param viewHolder The holder of the view to drag.
|
||||||
|
*/
|
||||||
|
void onStartDrag(RecyclerView.ViewHolder viewHolder);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Paul Burke
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
||||||
|
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
|
||||||
|
* </br/>
|
||||||
|
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
|
||||||
|
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement
|
||||||
|
* {@link ItemTouchHelperViewHolder}.
|
||||||
|
*
|
||||||
|
* @author Paul Burke (ipaulpro)
|
||||||
|
*/
|
||||||
|
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||||
|
|
||||||
|
public static final float ALPHA_FULL = 1.0f;
|
||||||
|
|
||||||
|
private final ItemTouchHelperAdapter mAdapter;
|
||||||
|
|
||||||
|
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
||||||
|
mAdapter = adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLongPressDragEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isItemViewSwipeEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
// Set movement flags based on the layout manager
|
||||||
|
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
||||||
|
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||||
|
final int swipeFlags = 0;
|
||||||
|
return makeMovementFlags(dragFlags, swipeFlags);
|
||||||
|
} else {
|
||||||
|
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||||
|
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
||||||
|
return makeMovementFlags(dragFlags, swipeFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
||||||
|
if (source.getItemViewType() != target.getItemViewType()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the adapter of the move
|
||||||
|
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
|
||||||
|
// Notify the adapter of the dismissal
|
||||||
|
mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX,
|
||||||
|
float dY, int actionState, boolean isCurrentlyActive) {
|
||||||
|
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||||
|
// Fade out the view as it is swiped out of the parent's bounds
|
||||||
|
final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
|
||||||
|
viewHolder.itemView.setAlpha(alpha);
|
||||||
|
viewHolder.itemView.setTranslationX(dX);
|
||||||
|
} else {
|
||||||
|
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||||
|
// We only want the active item to change
|
||||||
|
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||||
|
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||||
|
// Let the view holder know that this item is being moved or dragged
|
||||||
|
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||||
|
itemViewHolder.onItemSelected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onSelectedChanged(viewHolder, actionState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
super.clearView(recyclerView, viewHolder);
|
||||||
|
|
||||||
|
mAdapter.onItemMoveCompleted();
|
||||||
|
|
||||||
|
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
||||||
|
|
||||||
|
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||||
|
// Tell the view holder it's time to restore the idle state
|
||||||
|
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||||
|
itemViewHolder.onItemClear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,147 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2015 Paul Burke
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.v2ray.ang.helper
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.sign
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and
|
|
||||||
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br></br>
|
|
||||||
*
|
|
||||||
* Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement
|
|
||||||
* [ItemTouchHelperViewHolder].
|
|
||||||
*
|
|
||||||
* @author Paul Burke (ipaulpro)
|
|
||||||
*/
|
|
||||||
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
|
|
||||||
private var mReturnAnimator: ValueAnimator? = null
|
|
||||||
|
|
||||||
override fun isLongPressDragEnabled(): Boolean = true
|
|
||||||
|
|
||||||
override fun isItemViewSwipeEnabled(): Boolean = true
|
|
||||||
|
|
||||||
override fun getMovementFlags(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder
|
|
||||||
): Int {
|
|
||||||
val dragFlags: Int
|
|
||||||
val swipeFlags: Int
|
|
||||||
if (recyclerView.layoutManager is GridLayoutManager) {
|
|
||||||
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
|
|
||||||
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
|
|
||||||
} else {
|
|
||||||
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
|
||||||
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
|
|
||||||
}
|
|
||||||
return makeMovementFlags(dragFlags, swipeFlags)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMove(
|
|
||||||
recyclerView: RecyclerView,
|
|
||||||
source: RecyclerView.ViewHolder,
|
|
||||||
target: RecyclerView.ViewHolder
|
|
||||||
): Boolean {
|
|
||||||
return if (source.itemViewType != target.itemViewType) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
mAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
|
||||||
// Do not delete; simply return item to original position
|
|
||||||
returnViewToOriginalPosition(viewHolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChildDraw(
|
|
||||||
c: Canvas, recyclerView: RecyclerView,
|
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
|
||||||
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
|
|
||||||
) {
|
|
||||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
|
||||||
val maxSwipeDistance = viewHolder.itemView.width * SWIPE_THRESHOLD
|
|
||||||
val swipeAmount = abs(dX)
|
|
||||||
val direction = sign(dX)
|
|
||||||
|
|
||||||
// Limit maximum swipe distance
|
|
||||||
val translationX = min(swipeAmount, maxSwipeDistance) * direction
|
|
||||||
val alpha = ALPHA_FULL - min(swipeAmount, maxSwipeDistance) / maxSwipeDistance
|
|
||||||
|
|
||||||
viewHolder.itemView.translationX = translationX
|
|
||||||
viewHolder.itemView.alpha = alpha
|
|
||||||
|
|
||||||
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
|
|
||||||
returnViewToOriginalPosition(viewHolder)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun returnViewToOriginalPosition(viewHolder: RecyclerView.ViewHolder) {
|
|
||||||
mReturnAnimator?.takeIf { it.isRunning }?.cancel()
|
|
||||||
|
|
||||||
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.translationX, 0f).apply {
|
|
||||||
addUpdateListener { animation ->
|
|
||||||
val value = animation.animatedValue as Float
|
|
||||||
viewHolder.itemView.translationX = value
|
|
||||||
viewHolder.itemView.alpha = 1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD)
|
|
||||||
}
|
|
||||||
interpolator = DecelerateInterpolator()
|
|
||||||
duration = ANIMATION_DURATION
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
|
||||||
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is ItemTouchHelperViewHolder) {
|
|
||||||
viewHolder.onItemSelected()
|
|
||||||
}
|
|
||||||
super.onSelectedChanged(viewHolder, actionState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
|
||||||
super.clearView(recyclerView, viewHolder)
|
|
||||||
viewHolder.itemView.alpha = ALPHA_FULL
|
|
||||||
if (viewHolder is ItemTouchHelperViewHolder) {
|
|
||||||
viewHolder.onItemClear()
|
|
||||||
}
|
|
||||||
mAdapter.onItemMoveCompleted()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
|
|
||||||
return 1.1f // Set a value greater than 1 to prevent default swipe delete
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
|
|
||||||
return defaultValue * 10 // Increase swipe escape velocity to make swipe harder to trigger
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ALPHA_FULL = 1.0f
|
|
||||||
private const val SWIPE_THRESHOLD = 0.25f
|
|
||||||
private const val ANIMATION_DURATION: Long = 200
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.content.pm.ResolveInfo
|
|
||||||
|
|
||||||
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
|
|
||||||
init {
|
|
||||||
check(resolveInfo.providerInfo != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val componentInfo get() = resolveInfo.providerInfo!!
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
|
|
||||||
abstract class Plugin {
|
|
||||||
abstract val id: String
|
|
||||||
abstract val label: CharSequence
|
|
||||||
abstract val version: Int
|
|
||||||
abstract val versionName: String
|
|
||||||
open val icon: Drawable? get() = null
|
|
||||||
open val defaultConfig: String? get() = null
|
|
||||||
open val packageName: String get() = ""
|
|
||||||
open val directBootAware: Boolean get() = true
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
return id == (other as Plugin).id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() = id.hashCode()
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
object PluginContract {
|
|
||||||
|
|
||||||
const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN"
|
|
||||||
const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY"
|
|
||||||
const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id"
|
|
||||||
const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path"
|
|
||||||
const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable"
|
|
||||||
|
|
||||||
const val COLUMN_PATH = "path"
|
|
||||||
const val COLUMN_MODE = "mode"
|
|
||||||
const val SCHEME = "plugin"
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import com.v2ray.ang.AngApplication
|
|
||||||
|
|
||||||
class PluginList : ArrayList<Plugin>() {
|
|
||||||
init {
|
|
||||||
addAll(
|
|
||||||
AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }.map { NativePlugin(it) })
|
|
||||||
}
|
|
||||||
|
|
||||||
val lookup = mutableMapOf<String, Plugin>().apply {
|
|
||||||
for (plugin in this@PluginList.toList()) {
|
|
||||||
fun check(old: Plugin?) {
|
|
||||||
if (old != null && old != plugin) {
|
|
||||||
this@PluginList.remove(old)
|
|
||||||
}
|
|
||||||
/* if (old != null && old !== plugin) {
|
|
||||||
val packages = this@PluginList.filter { it.id == plugin.id }
|
|
||||||
.joinToString { it.packageName }
|
|
||||||
val message = "Conflicting plugins found from: $packages"
|
|
||||||
Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
|
|
||||||
throw IllegalStateException(message)
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
check(put(plugin.id, plugin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,233 +0,0 @@
|
||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-AngApplication@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ComponentInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.pm.ProviderInfo
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.system.Os
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import com.v2ray.ang.AngApplication
|
|
||||||
import com.v2ray.ang.extension.listenForPackageChanges
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
object PluginManager {
|
|
||||||
|
|
||||||
class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin)
|
|
||||||
|
|
||||||
private var receiver: BroadcastReceiver? = null
|
|
||||||
private var cachedPlugins: PluginList? = null
|
|
||||||
fun fetchPlugins() = synchronized(this) {
|
|
||||||
if (receiver == null) receiver = AngApplication.application.listenForPackageChanges {
|
|
||||||
synchronized(this) {
|
|
||||||
receiver = null
|
|
||||||
cachedPlugins = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (cachedPlugins == null) cachedPlugins = PluginList()
|
|
||||||
cachedPlugins!!
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildUri(id: String, authority: String) = Uri.Builder()
|
|
||||||
.scheme(PluginContract.SCHEME)
|
|
||||||
.authority(authority)
|
|
||||||
.path("/$id")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
data class InitResult(
|
|
||||||
val path: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Throws(Throwable::class)
|
|
||||||
fun init(pluginId: String): InitResult? {
|
|
||||||
if (pluginId.isEmpty()) return null
|
|
||||||
var throwable: Throwable? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
val result = initNative(pluginId)
|
|
||||||
if (result != null) return result
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
if (throwable == null) throwable = t //Logs.w(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw throwable ?: PluginNotFoundException(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNative(pluginId: String): InitResult? {
|
|
||||||
var flags = PackageManager.GET_META_DATA
|
|
||||||
if (Build.VERSION.SDK_INT >= 24) {
|
|
||||||
flags =
|
|
||||||
flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
|
|
||||||
}
|
|
||||||
var providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }
|
|
||||||
if (providers.isEmpty()) {
|
|
||||||
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }
|
|
||||||
}
|
|
||||||
if (providers.isEmpty()) {
|
|
||||||
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }
|
|
||||||
}
|
|
||||||
if (providers.isEmpty()) {
|
|
||||||
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags
|
|
||||||
)
|
|
||||||
.filter { it.providerInfo.exported }
|
|
||||||
}
|
|
||||||
if (providers.isEmpty()) {
|
|
||||||
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
|
||||||
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
|
|
||||||
).filter {
|
|
||||||
it.providerInfo.exported &&
|
|
||||||
it.providerInfo.metaData.containsKey(METADATA_KEY_ID) &&
|
|
||||||
it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId
|
|
||||||
}
|
|
||||||
if (providers.size > 1) {
|
|
||||||
providers = listOf(providers[0]) // What if there is more than one?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (providers.isEmpty()) return null
|
|
||||||
if (providers.size > 1) {
|
|
||||||
val message =
|
|
||||||
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
|
|
||||||
AngApplication.application.toast(message)
|
|
||||||
throw IllegalStateException(message)
|
|
||||||
}
|
|
||||||
val provider = providers.single().providerInfo
|
|
||||||
var failure: Throwable? = null
|
|
||||||
try {
|
|
||||||
initNativeFaster(provider)?.also { return InitResult(it) }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
// Logs.w("Initializing native plugin faster mode failed")
|
|
||||||
failure = t
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = Uri.Builder().apply {
|
|
||||||
scheme(ContentResolver.SCHEME_CONTENT)
|
|
||||||
authority(provider.authority)
|
|
||||||
}.build()
|
|
||||||
try {
|
|
||||||
return initNativeFast(
|
|
||||||
AngApplication.application.contentResolver,
|
|
||||||
pluginId,
|
|
||||||
uri
|
|
||||||
)?.let { InitResult(it) }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
// Logs.w("Initializing native plugin fast mode failed")
|
|
||||||
failure?.also { t.addSuppressed(it) }
|
|
||||||
failure = t
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return initNativeSlow(
|
|
||||||
AngApplication.application.contentResolver,
|
|
||||||
pluginId,
|
|
||||||
uri
|
|
||||||
)?.let { InitResult(it) }
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
failure?.also { t.addSuppressed(it) }
|
|
||||||
throw t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNativeFaster(provider: ProviderInfo): String? {
|
|
||||||
return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)
|
|
||||||
?.let { relativePath ->
|
|
||||||
File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
|
|
||||||
check(canExecute())
|
|
||||||
}.absolutePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? {
|
|
||||||
return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf())
|
|
||||||
?.getString(PluginContract.EXTRA_ENTRY)?.also {
|
|
||||||
check(File(it).canExecute())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Recycle")
|
|
||||||
private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? {
|
|
||||||
var initialized = false
|
|
||||||
fun entryNotFound(): Nothing =
|
|
||||||
throw IndexOutOfBoundsException("Plugin entry binary not found")
|
|
||||||
|
|
||||||
val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin")
|
|
||||||
(cr.query(
|
|
||||||
uri,
|
|
||||||
arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
?: return null).use { cursor ->
|
|
||||||
if (!cursor.moveToFirst()) entryNotFound()
|
|
||||||
pluginDir.deleteRecursively()
|
|
||||||
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
|
|
||||||
val pluginDirPath = pluginDir.absolutePath + '/'
|
|
||||||
do {
|
|
||||||
val path = cursor.getString(0)
|
|
||||||
val file = File(pluginDir, path)
|
|
||||||
check(file.absolutePath.startsWith(pluginDirPath))
|
|
||||||
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
|
|
||||||
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
|
|
||||||
}
|
|
||||||
Os.chmod(
|
|
||||||
file.absolutePath, when (cursor.getType(1)) {
|
|
||||||
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
|
|
||||||
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
|
|
||||||
else -> throw IllegalArgumentException("File mode should be of type int")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (path == pluginId) initialized = true
|
|
||||||
} while (cursor.moveToNext())
|
|
||||||
}
|
|
||||||
if (!initialized) entryNotFound()
|
|
||||||
return File(pluginDir, pluginId).absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
|
|
||||||
is String -> value
|
|
||||||
// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
|
|
||||||
// .getString(value)
|
|
||||||
|
|
||||||
null -> null
|
|
||||||
else -> error("meta-data $key has invalid type ${value.javaClass}")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
/******************************************************************************
|
|
||||||
* *
|
|
||||||
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
|
||||||
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
|
||||||
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
|
||||||
* *
|
|
||||||
* This program is free software: you can redistribute it and/or modify *
|
|
||||||
* it under the terms of the GNU General Public License as published by *
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or *
|
|
||||||
* (at your option) any later version. *
|
|
||||||
* *
|
|
||||||
* This program is distributed in the hope that it will be useful, *
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
||||||
* GNU General Public License for more details. *
|
|
||||||
* *
|
|
||||||
* You should have received a copy of the GNU General Public License *
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
||||||
* *
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
package com.v2ray.ang.plugin
|
|
||||||
|
|
||||||
import android.content.pm.ComponentInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.pm.ResolveInfo
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Build
|
|
||||||
import com.v2ray.ang.AngApplication
|
|
||||||
import com.v2ray.ang.plugin.PluginManager.loadString
|
|
||||||
|
|
||||||
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
|
|
||||||
protected abstract val componentInfo: ComponentInfo
|
|
||||||
|
|
||||||
override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
|
|
||||||
override val version by lazy {
|
|
||||||
getPackageInfo(componentInfo.packageName).versionCode
|
|
||||||
}
|
|
||||||
override val versionName: String by lazy {
|
|
||||||
getPackageInfo(componentInfo.packageName).versionName!!
|
|
||||||
}
|
|
||||||
override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
|
|
||||||
override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
|
|
||||||
override val packageName: String get() = componentInfo.packageName
|
|
||||||
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
|
|
||||||
|
|
||||||
fun getPackageInfo(packageName: String) = AngApplication.application.packageManager.getPackageInfo(
|
|
||||||
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
|
||||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
|
|
||||||
)!!
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package com.v2ray.ang.receiver
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
|
||||||
/**
|
|
||||||
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
|
||||||
* It checks if the context is not null and the action is ACTION_BOOT_COMPLETED.
|
|
||||||
* If the conditions are met, it starts the V2Ray service.
|
|
||||||
*
|
|
||||||
* @param context The Context in which the receiver is running.
|
|
||||||
* @param intent The Intent being received.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
|
||||||
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
|
||||||
V2RayServiceManager.startVService(context)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,252 +0,0 @@
|
||||||
package com.v2ray.ang.service
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.extension.toSpeedString
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.ui.MainActivity
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
object NotificationService {
|
|
||||||
private const val NOTIFICATION_ID = 1
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
|
|
||||||
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
|
||||||
|
|
||||||
private var lastQueryTime = 0L
|
|
||||||
private var mBuilder: NotificationCompat.Builder? = null
|
|
||||||
private var speedNotificationJob: Job? = null
|
|
||||||
private var mNotificationManager: NotificationManager? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the speed notification.
|
|
||||||
* @param currentConfig The current profile configuration.
|
|
||||||
*/
|
|
||||||
fun startSpeedNotification(currentConfig: ProfileItem?) {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
|
|
||||||
if (speedNotificationJob != null || V2RayServiceManager.isRunning() == false) return
|
|
||||||
|
|
||||||
lastQueryTime = System.currentTimeMillis()
|
|
||||||
var lastZeroSpeed = false
|
|
||||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
|
||||||
outboundTags?.remove(TAG_DIRECT)
|
|
||||||
|
|
||||||
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
while (isActive) {
|
|
||||||
val queryTime = System.currentTimeMillis()
|
|
||||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
|
||||||
var proxyTotal = 0L
|
|
||||||
val text = StringBuilder()
|
|
||||||
outboundTags?.forEach {
|
|
||||||
val up = V2RayServiceManager.queryStats(it, AppConfig.UPLINK)
|
|
||||||
val down = V2RayServiceManager.queryStats(it, AppConfig.DOWNLINK)
|
|
||||||
if (up + down > 0) {
|
|
||||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
|
||||||
proxyTotal += up + down
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val directUplink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
|
||||||
val directDownlink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
|
|
||||||
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
|
||||||
if (!zeroSpeed || !lastZeroSpeed) {
|
|
||||||
if (proxyTotal == 0L) {
|
|
||||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
|
||||||
}
|
|
||||||
appendSpeedString(
|
|
||||||
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
|
||||||
directDownlink / sinceLastQueryInSeconds
|
|
||||||
)
|
|
||||||
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
|
||||||
}
|
|
||||||
lastZeroSpeed = zeroSpeed
|
|
||||||
lastQueryTime = queryTime
|
|
||||||
delay(3000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the notification.
|
|
||||||
* @param currentConfig The current profile configuration.
|
|
||||||
*/
|
|
||||||
fun showNotification(currentConfig: ProfileItem?) {
|
|
||||||
val service = getService() ?: return
|
|
||||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
} else {
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
}
|
|
||||||
|
|
||||||
val startMainIntent = Intent(service, MainActivity::class.java)
|
|
||||||
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
|
|
||||||
|
|
||||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
stopV2RayIntent.`package` = ANG_PACKAGE
|
|
||||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
|
||||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
|
|
||||||
|
|
||||||
val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
restartV2RayIntent.`package` = ANG_PACKAGE
|
|
||||||
restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
|
|
||||||
val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
|
|
||||||
|
|
||||||
val channelId =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
createNotificationChannel()
|
|
||||||
} else {
|
|
||||||
// If earlier version channel ID is not used
|
|
||||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
mBuilder = NotificationCompat.Builder(service, channelId)
|
|
||||||
.setSmallIcon(R.drawable.ic_stat_name)
|
|
||||||
.setContentTitle(currentConfig?.remarks)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setContentIntent(contentPendingIntent)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_delete_24dp,
|
|
||||||
service.getString(R.string.notification_action_stop_v2ray),
|
|
||||||
stopV2RayPendingIntent
|
|
||||||
)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_delete_24dp,
|
|
||||||
service.getString(R.string.title_service_restart),
|
|
||||||
restartV2RayPendingIntent
|
|
||||||
)
|
|
||||||
|
|
||||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
|
|
||||||
|
|
||||||
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the notification.
|
|
||||||
*/
|
|
||||||
fun cancelNotification() {
|
|
||||||
val service = getService() ?: return
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
|
||||||
} else {
|
|
||||||
service.stopForeground(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
mBuilder = null
|
|
||||||
speedNotificationJob?.cancel()
|
|
||||||
speedNotificationJob = null
|
|
||||||
mNotificationManager = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the speed notification.
|
|
||||||
* @param currentConfig The current profile configuration.
|
|
||||||
*/
|
|
||||||
fun stopSpeedNotification(currentConfig: ProfileItem?) {
|
|
||||||
speedNotificationJob?.let {
|
|
||||||
it.cancel()
|
|
||||||
speedNotificationJob = null
|
|
||||||
updateNotification(currentConfig?.remarks, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a notification channel for Android O and above.
|
|
||||||
* @return The channel ID.
|
|
||||||
*/
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private fun createNotificationChannel(): String {
|
|
||||||
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
|
||||||
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
|
||||||
val chan = NotificationChannel(
|
|
||||||
channelId,
|
|
||||||
channelName, NotificationManager.IMPORTANCE_HIGH
|
|
||||||
)
|
|
||||||
chan.lightColor = Color.DKGRAY
|
|
||||||
chan.importance = NotificationManager.IMPORTANCE_NONE
|
|
||||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
|
||||||
getNotificationManager()?.createNotificationChannel(chan)
|
|
||||||
return channelId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the notification with the given content text and traffic data.
|
|
||||||
* @param contentText The content text.
|
|
||||||
* @param proxyTraffic The proxy traffic.
|
|
||||||
* @param directTraffic The direct traffic.
|
|
||||||
*/
|
|
||||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
|
||||||
if (mBuilder != null) {
|
|
||||||
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
|
|
||||||
} else if (proxyTraffic > directTraffic) {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
|
|
||||||
} else {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
|
|
||||||
}
|
|
||||||
mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
|
|
||||||
mBuilder?.setContentText(contentText)
|
|
||||||
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the notification manager.
|
|
||||||
* @return The notification manager.
|
|
||||||
*/
|
|
||||||
private fun getNotificationManager(): NotificationManager? {
|
|
||||||
if (mNotificationManager == null) {
|
|
||||||
val service = getService() ?: return null
|
|
||||||
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
}
|
|
||||||
return mNotificationManager
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends the speed string to the given text.
|
|
||||||
* @param text The text to append to.
|
|
||||||
* @param name The name of the tag.
|
|
||||||
* @param up The uplink speed.
|
|
||||||
* @param down The downlink speed.
|
|
||||||
*/
|
|
||||||
private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
|
|
||||||
var n = name ?: "no tag"
|
|
||||||
n = n.substring(0, min(n.length, 6))
|
|
||||||
text.append(n)
|
|
||||||
for (i in n.length..6 step 2) {
|
|
||||||
text.append("\t")
|
|
||||||
}
|
|
||||||
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the service instance.
|
|
||||||
* @return The service instance.
|
|
||||||
*/
|
|
||||||
private fun getService(): Service? {
|
|
||||||
return V2RayServiceManager.serviceControl?.get()?.getService()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
package com.v2ray.ang.service
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class ProcessService {
|
|
||||||
private var process: Process? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs a process with the given command.
|
|
||||||
* @param context The context.
|
|
||||||
* @param cmd The command to run.
|
|
||||||
*/
|
|
||||||
fun runProcess(context: Context, cmd: MutableList<String>) {
|
|
||||||
Log.i(AppConfig.TAG, cmd.toString())
|
|
||||||
|
|
||||||
try {
|
|
||||||
val proBuilder = ProcessBuilder(cmd)
|
|
||||||
proBuilder.redirectErrorStream(true)
|
|
||||||
process = proBuilder
|
|
||||||
.directory(context.filesDir)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
Thread.sleep(50L)
|
|
||||||
Log.i(AppConfig.TAG, "runProcess check")
|
|
||||||
process?.waitFor()
|
|
||||||
Log.i(AppConfig.TAG, "runProcess exited")
|
|
||||||
}
|
|
||||||
Log.i(AppConfig.TAG, process.toString())
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, e.toString(), e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the running process.
|
|
||||||
*/
|
|
||||||
fun stopProcess() {
|
|
||||||
try {
|
|
||||||
Log.i(AppConfig.TAG, "runProcess destroy")
|
|
||||||
process?.destroy()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to destroy process", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package com.v2ray.ang.service
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
|
|
||||||
interface ServiceControl {
|
|
||||||
/**
|
|
||||||
* Gets the service instance.
|
|
||||||
* @return The service instance.
|
|
||||||
*/
|
|
||||||
fun getService(): Service
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the service.
|
|
||||||
*/
|
|
||||||
fun startService()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the service.
|
|
||||||
*/
|
|
||||||
fun stopService()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Protects the VPN socket.
|
|
||||||
* @param socket The socket to protect.
|
|
||||||
* @return True if the socket is protected, false otherwise.
|
|
||||||
*/
|
|
||||||
fun vpnProtect(socket: Int): Boolean
|
|
||||||
}
|
|
|
@ -1,377 +0,0 @@
|
||||||
package com.v2ray.ang.service
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.handler.SpeedtestManager
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.MessageUtil
|
|
||||||
import com.v2ray.ang.util.PluginUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import go.Seq
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import libv2ray.CoreCallbackHandler
|
|
||||||
import libv2ray.CoreController
|
|
||||||
import libv2ray.Libv2ray
|
|
||||||
import java.lang.ref.SoftReference
|
|
||||||
|
|
||||||
object V2RayServiceManager {
|
|
||||||
|
|
||||||
private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback())
|
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
|
||||||
private var currentConfig: ProfileItem? = null
|
|
||||||
|
|
||||||
var serviceControl: SoftReference<ServiceControl>? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
|
||||||
Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the V2Ray service from a toggle action.
|
|
||||||
* @param context The context from which the service is started.
|
|
||||||
* @return True if the service was started successfully, false otherwise.
|
|
||||||
*/
|
|
||||||
fun startVServiceFromToggle(context: Context): Boolean {
|
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
|
||||||
context.toast(R.string.app_tile_first_use)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
startContextService(context)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the V2Ray service.
|
|
||||||
* @param context The context from which the service is started.
|
|
||||||
* @param guid The GUID of the server configuration to use (optional).
|
|
||||||
*/
|
|
||||||
fun startVService(context: Context, guid: String? = null) {
|
|
||||||
if (guid != null) {
|
|
||||||
MmkvManager.setSelectServer(guid)
|
|
||||||
}
|
|
||||||
startContextService(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the V2Ray service.
|
|
||||||
* @param context The context from which the service is stopped.
|
|
||||||
*/
|
|
||||||
fun stopVService(context: Context) {
|
|
||||||
context.toast(R.string.toast_services_stop)
|
|
||||||
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the V2Ray service is running.
|
|
||||||
* @return True if the service is running, false otherwise.
|
|
||||||
*/
|
|
||||||
fun isRunning() = coreController.isRunning
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the name of the currently running server.
|
|
||||||
* @return The name of the running server.
|
|
||||||
*/
|
|
||||||
fun getRunningServerName() = currentConfig?.remarks.orEmpty()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the context service for V2Ray.
|
|
||||||
* Chooses between VPN service or Proxy-only service based on user settings.
|
|
||||||
* @param context The context from which the service is started.
|
|
||||||
*/
|
|
||||||
private fun startContextService(context: Context) {
|
|
||||||
if (coreController.isRunning) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val guid = MmkvManager.getSelectServer() ?: return
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
|
||||||
if (config.configType != EConfigType.CUSTOM
|
|
||||||
&& !Utils.isValidUrl(config.server)
|
|
||||||
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
|
||||||
) return
|
|
||||||
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
|
||||||
// if (!result.status) return
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) == true) {
|
|
||||||
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
|
||||||
} else {
|
|
||||||
context.toast(R.string.toast_services_start)
|
|
||||||
}
|
|
||||||
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) {
|
|
||||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
|
||||||
} else {
|
|
||||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
|
||||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
|
||||||
* Starts the V2Ray core service.
|
|
||||||
*/
|
|
||||||
fun startCoreLoop(): Boolean {
|
|
||||||
if (coreController.isRunning) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val service = getService() ?: return false
|
|
||||||
val guid = MmkvManager.getSelectServer() ?: return false
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return false
|
|
||||||
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
|
||||||
if (!result.status)
|
|
||||||
return false
|
|
||||||
|
|
||||||
try {
|
|
||||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
|
||||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
|
||||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
|
||||||
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
currentConfig = config
|
|
||||||
|
|
||||||
try {
|
|
||||||
coreController.startLoop(result.content)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to start Core loop", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coreController.isRunning == false) {
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
|
||||||
NotificationService.cancelNotification()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
|
||||||
NotificationService.showNotification(currentConfig)
|
|
||||||
NotificationService.startSpeedNotification(currentConfig)
|
|
||||||
|
|
||||||
PluginUtil.runPlugin(service, config, result.socksPort)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to startup service", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the V2Ray core service.
|
|
||||||
* Unregisters broadcast receivers, stops notifications, and shuts down plugins.
|
|
||||||
* @return True if the core was stopped successfully, false otherwise.
|
|
||||||
*/
|
|
||||||
fun stopCoreLoop(): Boolean {
|
|
||||||
val service = getService() ?: return false
|
|
||||||
|
|
||||||
if (coreController.isRunning) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
coreController.stopLoop()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
|
||||||
NotificationService.cancelNotification()
|
|
||||||
|
|
||||||
try {
|
|
||||||
service.unregisterReceiver(mMsgReceive)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
|
|
||||||
}
|
|
||||||
PluginUtil.stopPlugin()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the statistics for a given tag and link.
|
|
||||||
* @param tag The tag to query.
|
|
||||||
* @param link The link to query.
|
|
||||||
* @return The statistics value.
|
|
||||||
*/
|
|
||||||
fun queryStats(tag: String, link: String): Long {
|
|
||||||
return coreController.queryStats(tag, link)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures the connection delay for the current V2Ray configuration.
|
|
||||||
* Tests with primary URL first, then falls back to alternative URL if needed.
|
|
||||||
* Also fetches remote IP information if the delay test was successful.
|
|
||||||
*/
|
|
||||||
private fun measureV2rayDelay() {
|
|
||||||
if (coreController.isRunning == false) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val service = getService() ?: return@launch
|
|
||||||
var time = -1L
|
|
||||||
var errorStr = ""
|
|
||||||
|
|
||||||
try {
|
|
||||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
|
|
||||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
|
||||||
}
|
|
||||||
if (time == -1L) {
|
|
||||||
try {
|
|
||||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
|
|
||||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = if (time >= 0) {
|
|
||||||
service.getString(R.string.connection_test_available, time)
|
|
||||||
} else {
|
|
||||||
service.getString(R.string.connection_test_error, errorStr)
|
|
||||||
}
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
|
||||||
|
|
||||||
// Only fetch IP info if the delay test was successful
|
|
||||||
if (time >= 0) {
|
|
||||||
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current service instance.
|
|
||||||
* @return The current service instance, or null if not available.
|
|
||||||
*/
|
|
||||||
private fun getService(): Service? {
|
|
||||||
return serviceControl?.get()?.getService()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core callback handler implementation for handling V2Ray core events.
|
|
||||||
* Handles startup, shutdown, socket protection, and status emission.
|
|
||||||
*/
|
|
||||||
private class CoreCallback : CoreCallbackHandler {
|
|
||||||
/**
|
|
||||||
* Called when V2Ray core starts up.
|
|
||||||
* @return 0 for success, any other value for failure.
|
|
||||||
*/
|
|
||||||
override fun startup(): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when V2Ray core shuts down.
|
|
||||||
* @return 0 for success, any other value for failure.
|
|
||||||
*/
|
|
||||||
override fun shutdown(): Long {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
|
||||||
return try {
|
|
||||||
serviceControl.stopService()
|
|
||||||
0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when V2Ray core emits status information.
|
|
||||||
* @param l Status code.
|
|
||||||
* @param s Status message.
|
|
||||||
* @return Always returns 0.
|
|
||||||
*/
|
|
||||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast receiver for handling messages sent to the service.
|
|
||||||
* Handles registration, service control, and screen events.
|
|
||||||
*/
|
|
||||||
private class ReceiveMessageHandler : BroadcastReceiver() {
|
|
||||||
/**
|
|
||||||
* Handles received broadcast messages.
|
|
||||||
* Processes service control messages and screen state changes.
|
|
||||||
* @param ctx The context in which the receiver is running.
|
|
||||||
* @param intent The intent being received.
|
|
||||||
*/
|
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return
|
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
|
||||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
|
||||||
if (coreController.isRunning) {
|
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
|
||||||
} else {
|
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START -> {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
|
||||||
Log.i(AppConfig.TAG, "Stop Service")
|
|
||||||
serviceControl.stopService()
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
|
||||||
Log.i(AppConfig.TAG, "Restart Service")
|
|
||||||
serviceControl.stopService()
|
|
||||||
Thread.sleep(500L)
|
|
||||||
startVService(serviceControl.getService())
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY -> {
|
|
||||||
measureV2rayDelay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (intent?.action) {
|
|
||||||
Intent.ACTION_SCREEN_OFF -> {
|
|
||||||
Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats")
|
|
||||||
NotificationService.stopSpeedNotification(currentConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent.ACTION_SCREEN_ON -> {
|
|
||||||
Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats")
|
|
||||||
NotificationService.startSpeedNotification(currentConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
package com.v2ray.ang.service
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.IBinder
|
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
|
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
|
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.extension.serializable
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SpeedtestManager
|
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
|
||||||
import com.v2ray.ang.util.MessageUtil
|
|
||||||
import com.v2ray.ang.util.PluginUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import go.Seq
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.cancelChildren
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import libv2ray.Libv2ray
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class V2RayTestService : Service() {
|
|
||||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the V2Ray environment.
|
|
||||||
*/
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
Seq.setContext(this)
|
|
||||||
Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the start command for the service.
|
|
||||||
* @param intent The intent.
|
|
||||||
* @param flags The flags.
|
|
||||||
* @param startId The start ID.
|
|
||||||
* @return The start mode.
|
|
||||||
*/
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
|
||||||
MSG_MEASURE_CONFIG -> {
|
|
||||||
val guid = intent.serializable<String>("content") ?: ""
|
|
||||||
realTestScope.launch {
|
|
||||||
val result = startRealPing(guid)
|
|
||||||
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MSG_MEASURE_CONFIG_CANCEL -> {
|
|
||||||
realTestScope.coroutineContext[Job]?.cancelChildren()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onStartCommand(intent, flags, startId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the service.
|
|
||||||
* @param intent The intent.
|
|
||||||
* @return The binder.
|
|
||||||
*/
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the real ping test.
|
|
||||||
* @param guid The GUID of the configuration.
|
|
||||||
* @return The ping result.
|
|
||||||
*/
|
|
||||||
private fun startRealPing(guid: String): Long {
|
|
||||||
val retFailure = -1L
|
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
|
|
||||||
if (config.configType == EConfigType.HYSTERIA2) {
|
|
||||||
val delay = PluginUtil.realPingHy2(this, config)
|
|
||||||
return delay
|
|
||||||
} else {
|
|
||||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
|
|
||||||
if (!configResult.status) {
|
|
||||||
return retFailure
|
|
||||||
}
|
|
||||||
return SpeedtestManager.realPing(configResult.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,201 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.BuildConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityAboutBinding
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SpeedtestManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import com.v2ray.ang.util.ZipUtil
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class AboutActivity : BaseActivity() {
|
|
||||||
|
|
||||||
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
|
|
||||||
private val extDir by lazy { File(Utils.backupPath(this)) }
|
|
||||||
|
|
||||||
private val requestPermissionLauncher =
|
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
|
||||||
if (isGranted) {
|
|
||||||
try {
|
|
||||||
showFileChooser()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.title_about)
|
|
||||||
|
|
||||||
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
|
|
||||||
|
|
||||||
binding.layoutBackup.setOnClickListener {
|
|
||||||
val ret = backupConfiguration(extDir.absolutePath)
|
|
||||||
if (ret.first) {
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.layoutShare.setOnClickListener {
|
|
||||||
val ret = backupConfiguration(cacheDir.absolutePath)
|
|
||||||
if (ret.first) {
|
|
||||||
startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
Intent(Intent.ACTION_SEND).setType("application/zip")
|
|
||||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
.putExtra(
|
|
||||||
Intent.EXTRA_STREAM,
|
|
||||||
FileProvider.getUriForFile(
|
|
||||||
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
|
|
||||||
)
|
|
||||||
), getString(R.string.title_configuration_share)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.layoutRestore.setOnClickListener {
|
|
||||||
val permission =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
|
||||||
} else {
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
|
||||||
try {
|
|
||||||
showFileChooser()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
requestPermissionLauncher.launch(permission)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.layoutSoureCcode.setOnClickListener {
|
|
||||||
Utils.openUri(this, AppConfig.APP_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.layoutFeedback.setOnClickListener {
|
|
||||||
Utils.openUri(this, AppConfig.APP_ISSUES_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.layoutOssLicenses.setOnClickListener {
|
|
||||||
val webView = android.webkit.WebView(this)
|
|
||||||
webView.loadUrl("file:///android_asset/open_source_licenses.html")
|
|
||||||
android.app.AlertDialog.Builder(this)
|
|
||||||
.setTitle("Open source licenses")
|
|
||||||
.setView(webView)
|
|
||||||
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.layoutTgChannel.setOnClickListener {
|
|
||||||
Utils.openUri(this, AppConfig.TG_CHANNEL_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.layoutPrivacyPolicy.setOnClickListener {
|
|
||||||
Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY)
|
|
||||||
}
|
|
||||||
|
|
||||||
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
|
||||||
binding.tvVersion.text = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
|
|
||||||
val dateFormated = SimpleDateFormat(
|
|
||||||
"yyyy-MM-dd-HH-mm-ss",
|
|
||||||
Locale.getDefault()
|
|
||||||
).format(System.currentTimeMillis())
|
|
||||||
val folderName = "${getString(R.string.app_name)}_${dateFormated}"
|
|
||||||
val backupDir = this.cacheDir.absolutePath + "/$folderName"
|
|
||||||
val outputZipFilePath = "$outputZipFilePos/$folderName.zip"
|
|
||||||
|
|
||||||
val count = MMKV.backupAllToDirectory(backupDir)
|
|
||||||
if (count <= 0) {
|
|
||||||
return Pair(false, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ZipUtil.zipFromFolder(backupDir, outputZipFilePath)) {
|
|
||||||
return Pair(true, outputZipFilePath)
|
|
||||||
} else {
|
|
||||||
return Pair(false, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreConfiguration(zipFile: File): Boolean {
|
|
||||||
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
|
|
||||||
|
|
||||||
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val count = MMKV.restoreAllFromDirectory(backupDir)
|
|
||||||
return count > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showFileChooser() {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
type = "*/*"
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
|
||||||
} catch (ex: android.content.ActivityNotFoundException) {
|
|
||||||
Log.e(AppConfig.TAG, "File chooser activity not found", ex)
|
|
||||||
toast(R.string.toast_require_file_manager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val chooseFile =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
val uri = result.data?.data
|
|
||||||
if (result.resultCode == RESULT_OK && uri != null) {
|
|
||||||
try {
|
|
||||||
val targetFile =
|
|
||||||
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
|
||||||
contentResolver.openInputStream(uri).use { input ->
|
|
||||||
targetFile.outputStream().use { fileOut ->
|
|
||||||
input?.copyTo(fileOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (restoreConfiguration(targetFile)) {
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Error during file restore", e)
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.helper.CustomDividerItemDecoration
|
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
if (!Utils.getDarkModeStatus(this)) {
|
|
||||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
|
||||||
isAppearanceLightStatusBars = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
android.R.id.home -> {
|
|
||||||
// Handles the home button press by delegating to the onBackPressedDispatcher.
|
|
||||||
// This ensures consistent back navigation behavior.
|
|
||||||
onBackPressedDispatcher.onBackPressed()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
|
||||||
super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, SettingsManager.getLocale()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a custom divider to a RecyclerView.
|
|
||||||
*
|
|
||||||
* @param recyclerView The target RecyclerView to which the divider will be added.
|
|
||||||
* @param context The context used to access resources.
|
|
||||||
* @param drawableResId The resource ID of the drawable to be used as the divider.
|
|
||||||
* @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL).
|
|
||||||
*/
|
|
||||||
fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, context: Context?, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) {
|
|
||||||
// Get the drawable from resources
|
|
||||||
val drawable = ContextCompat.getDrawable(context!!, drawableResId)
|
|
||||||
requireNotNull(drawable) { "Drawable resource not found" }
|
|
||||||
|
|
||||||
// Create a DividerItemDecoration with the specified orientation
|
|
||||||
val dividerItemDecoration = CustomDividerItemDecoration(drawable, orientation)
|
|
||||||
|
|
||||||
// Add the divider to the RecyclerView
|
|
||||||
recyclerView.addItemDecoration(dividerItemDecoration)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.BuildConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityCheckUpdateBinding
|
|
||||||
import com.v2ray.ang.dto.CheckUpdateResult
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SpeedtestManager
|
|
||||||
import com.v2ray.ang.handler.UpdateCheckerManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class CheckUpdateActivity : BaseActivity() {
|
|
||||||
|
|
||||||
private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.update_check_for_update)
|
|
||||||
|
|
||||||
binding.layoutCheckUpdate.setOnClickListener {
|
|
||||||
checkForUpdates(binding.checkPreRelease.isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
|
|
||||||
}
|
|
||||||
binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)
|
|
||||||
|
|
||||||
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
|
||||||
binding.tvVersion.text = it
|
|
||||||
}
|
|
||||||
|
|
||||||
checkForUpdates(binding.checkPreRelease.isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkForUpdates(includePreRelease: Boolean) {
|
|
||||||
toast(R.string.update_checking_for_update)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
|
|
||||||
if (result.hasUpdate) {
|
|
||||||
showUpdateDialog(result)
|
|
||||||
} else {
|
|
||||||
toastSuccess(R.string.update_already_latest_version)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
|
|
||||||
toastError(e.message ?: getString(R.string.toast_failure))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showUpdateDialog(result: CheckUpdateResult) {
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setTitle(getString(R.string.update_new_version_found, result.latestVersion))
|
|
||||||
.setMessage(result.releaseNotes)
|
|
||||||
.setPositiveButton(R.string.update_now) { _, _ ->
|
|
||||||
result.downloadUrl?.let {
|
|
||||||
Utils.openUri(this, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,156 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|
||||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
private var logsetsAll: MutableList<String> = mutableListOf()
|
|
||||||
var logsets: MutableList<String> = mutableListOf()
|
|
||||||
private val adapter by lazy { LogcatRecyclerAdapter(this) }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.title_logcat)
|
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
|
|
||||||
binding.refreshLayout.setOnRefreshListener(this)
|
|
||||||
|
|
||||||
logsets.add(getString(R.string.pull_down_to_refresh))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLogcat() {
|
|
||||||
|
|
||||||
try {
|
|
||||||
binding.refreshLayout.isRefreshing = true
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
val lst = LinkedHashSet<String>()
|
|
||||||
lst.add("logcat")
|
|
||||||
lst.add("-d")
|
|
||||||
lst.add("-v")
|
|
||||||
lst.add("time")
|
|
||||||
lst.add("-s")
|
|
||||||
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
|
||||||
val process = withContext(Dispatchers.IO) {
|
|
||||||
Runtime.getRuntime().exec(lst.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
logsetsAll = allText.toMutableList()
|
|
||||||
logsets = allText.toMutableList()
|
|
||||||
refreshData()
|
|
||||||
binding.refreshLayout.isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to get logcat", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearLogcat() {
|
|
||||||
try {
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
val lst = LinkedHashSet<String>()
|
|
||||||
lst.add("logcat")
|
|
||||||
lst.add("-c")
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
|
||||||
process.waitFor()
|
|
||||||
}
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
logsetsAll.clear()
|
|
||||||
logsets.clear()
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to clear logcat", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
|
||||||
|
|
||||||
val searchItem = menu.findItem(R.id.search_view)
|
|
||||||
if (searchItem != null) {
|
|
||||||
val searchView = searchItem.actionView as SearchView
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
filterLogs(newText)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
searchView.setOnCloseListener {
|
|
||||||
filterLogs("")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.copy_all -> {
|
|
||||||
Utils.setClipboard(this, logsets.joinToString("\n"))
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.clear_all -> {
|
|
||||||
clearLogcat()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filterLogs(content: String?): Boolean {
|
|
||||||
val key = content?.trim()
|
|
||||||
logsets = if (key.isNullOrEmpty()) {
|
|
||||||
logsetsAll.toMutableList()
|
|
||||||
} else {
|
|
||||||
logsetsAll.filter { it.contains(key) }.toMutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshData()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRefresh() {
|
|
||||||
getLogcat()
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
fun refreshData() {
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
|
||||||
|
|
||||||
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
|
||||||
private var mActivity: LogcatActivity = activity
|
|
||||||
|
|
||||||
|
|
||||||
override fun getItemCount() = mActivity.logsets.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
|
||||||
try {
|
|
||||||
val log = mActivity.logsets[position]
|
|
||||||
if (log.isEmpty()) {
|
|
||||||
holder.itemSubSettingBinding.logTag.text = ""
|
|
||||||
holder.itemSubSettingBinding.logContent.text = ""
|
|
||||||
} else {
|
|
||||||
val content = log.split("):", limit = 2)
|
|
||||||
holder.itemSubSettingBinding.logTag.text = content.first().split("(", limit = 2).first().trim()
|
|
||||||
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Error binding log view data", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
|
||||||
return MainViewHolder(
|
|
||||||
ItemRecyclerLogcatBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerLogcatBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,695 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.net.Uri
|
|
||||||
import android.net.VpnService
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.GravityCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import com.google.android.material.navigation.NavigationView
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
|
||||||
import com.v2ray.ang.handler.MigrateManager
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
|
||||||
private val binding by lazy {
|
|
||||||
ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val adapter by lazy { MainRecyclerAdapter(this) }
|
|
||||||
private val requestVpnPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
startV2Ray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val requestSubSettingActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
initGroupTab()
|
|
||||||
}
|
|
||||||
private val tabGroupListener = object : TabLayout.OnTabSelectedListener {
|
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
|
||||||
val selectId = tab?.tag.toString()
|
|
||||||
if (selectId != mainViewModel.subscriptionId) {
|
|
||||||
mainViewModel.subscriptionIdChanged(selectId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
|
||||||
val mainViewModel: MainViewModel by viewModels()
|
|
||||||
|
|
||||||
// register activity result for requesting permission
|
|
||||||
private val requestPermissionLauncher =
|
|
||||||
registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { isGranted: Boolean ->
|
|
||||||
if (isGranted) {
|
|
||||||
when (pendingAction) {
|
|
||||||
Action.IMPORT_QR_CODE_CONFIG ->
|
|
||||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
|
|
||||||
Action.READ_CONTENT_FROM_URI ->
|
|
||||||
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
type = "*/*"
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
}, getString(R.string.title_file_chooser)))
|
|
||||||
|
|
||||||
Action.POST_NOTIFICATIONS -> {}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
pendingAction = Action.NONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private var pendingAction: Action = Action.NONE
|
|
||||||
|
|
||||||
enum class Action {
|
|
||||||
NONE,
|
|
||||||
IMPORT_QR_CODE_CONFIG,
|
|
||||||
READ_CONTENT_FROM_URI,
|
|
||||||
POST_NOTIFICATIONS
|
|
||||||
}
|
|
||||||
|
|
||||||
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
val uri = it.data?.data
|
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
|
||||||
readContentFromUri(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
title = getString(R.string.title_server)
|
|
||||||
setSupportActionBar(binding.toolbar)
|
|
||||||
|
|
||||||
binding.fab.setOnClickListener {
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
|
||||||
V2RayServiceManager.stopVService(this)
|
|
||||||
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
|
||||||
val intent = VpnService.prepare(this)
|
|
||||||
if (intent == null) {
|
|
||||||
startV2Ray()
|
|
||||||
} else {
|
|
||||||
requestVpnPermission.launch(intent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startV2Ray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.layoutTest.setOnClickListener {
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
|
||||||
setTestState(getString(R.string.connection_test_testing))
|
|
||||||
mainViewModel.testCurrentServerRealPing()
|
|
||||||
} else {
|
|
||||||
// tv_test_state.text = getString(R.string.connection_test_fail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) {
|
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
|
|
||||||
} else {
|
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
|
|
||||||
}
|
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
|
||||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
|
||||||
|
|
||||||
val toggle = ActionBarDrawerToggle(
|
|
||||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
|
||||||
)
|
|
||||||
binding.drawerLayout.addDrawerListener(toggle)
|
|
||||||
toggle.syncState()
|
|
||||||
binding.navView.setNavigationItemSelectedListener(this)
|
|
||||||
|
|
||||||
initGroupTab()
|
|
||||||
setupViewModel()
|
|
||||||
migrateLegacy()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
pendingAction = Action.POST_NOTIFICATIONS
|
|
||||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
|
||||||
} else {
|
|
||||||
isEnabled = false
|
|
||||||
onBackPressedDispatcher.onBackPressed()
|
|
||||||
isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun setupViewModel() {
|
|
||||||
mainViewModel.updateListAction.observe(this) { index ->
|
|
||||||
if (index >= 0) {
|
|
||||||
adapter.notifyItemChanged(index)
|
|
||||||
} else {
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) }
|
|
||||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
|
||||||
adapter.isRunning = isRunning
|
|
||||||
if (isRunning) {
|
|
||||||
binding.fab.setImageResource(R.drawable.ic_stop_24dp)
|
|
||||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
|
|
||||||
setTestState(getString(R.string.connection_connected))
|
|
||||||
binding.layoutTest.isFocusable = true
|
|
||||||
} else {
|
|
||||||
binding.fab.setImageResource(R.drawable.ic_play_24dp)
|
|
||||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
|
||||||
setTestState(getString(R.string.connection_not_connected))
|
|
||||||
binding.layoutTest.isFocusable = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mainViewModel.startListenBroadcast()
|
|
||||||
mainViewModel.initAssets(assets)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun migrateLegacy() {
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = MigrateManager.migrateServerConfig2Profile()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (result) {
|
|
||||||
toast(getString(R.string.migration_success))
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
} else {
|
|
||||||
//toast(getString(R.string.migration_fail))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initGroupTab() {
|
|
||||||
binding.tabGroup.removeOnTabSelectedListener(tabGroupListener)
|
|
||||||
binding.tabGroup.removeAllTabs()
|
|
||||||
binding.tabGroup.isVisible = false
|
|
||||||
|
|
||||||
val (listId, listRemarks) = mainViewModel.getSubscriptions(this)
|
|
||||||
if (listId == null || listRemarks == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (it in listRemarks.indices) {
|
|
||||||
val tab = binding.tabGroup.newTab()
|
|
||||||
tab.text = listRemarks[it]
|
|
||||||
tab.tag = listId[it]
|
|
||||||
binding.tabGroup.addTab(tab)
|
|
||||||
}
|
|
||||||
val selectIndex =
|
|
||||||
listId.indexOf(mainViewModel.subscriptionId).takeIf { it >= 0 } ?: (listId.count() - 1)
|
|
||||||
binding.tabGroup.selectTab(binding.tabGroup.getTabAt(selectIndex))
|
|
||||||
binding.tabGroup.addOnTabSelectedListener(tabGroupListener)
|
|
||||||
binding.tabGroup.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startV2Ray() {
|
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
|
||||||
toast(R.string.title_file_chooser)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
V2RayServiceManager.startVService(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restartV2Ray() {
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
|
||||||
V2RayServiceManager.stopVService(this)
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
delay(500)
|
|
||||||
startV2Ray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
}
|
|
||||||
|
|
||||||
public override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.menu_main, menu)
|
|
||||||
|
|
||||||
val searchItem = menu.findItem(R.id.search_view)
|
|
||||||
if (searchItem != null) {
|
|
||||||
val searchView = searchItem.actionView as SearchView
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
mainViewModel.filterConfig(newText.orEmpty())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
searchView.setOnCloseListener {
|
|
||||||
mainViewModel.filterConfig("")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.import_qrcode -> {
|
|
||||||
importQRcode()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_clipboard -> {
|
|
||||||
importClipboard()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_local -> {
|
|
||||||
importConfigLocal()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_manually_vmess -> {
|
|
||||||
importManually(EConfigType.VMESS.value)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_manually_vless -> {
|
|
||||||
importManually(EConfigType.VLESS.value)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_manually_ss -> {
|
|
||||||
importManually(EConfigType.SHADOWSOCKS.value)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_manually_socks -> {
|
|
||||||
importManually(EConfigType.SOCKS.value)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_manually_http -> {
|
|
||||||
importManually(EConfigType.HTTP.value)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_manually_trojan -> {
|
|
||||||
importManually(EConfigType.TROJAN.value)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_manually_wireguard -> {
|
|
||||||
importManually(EConfigType.WIREGUARD.value)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_manually_hysteria2 -> {
|
|
||||||
importManually(EConfigType.HYSTERIA2.value)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.export_all -> {
|
|
||||||
exportAll()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.ping_all -> {
|
|
||||||
toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
|
|
||||||
mainViewModel.testAllTcping()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.real_ping_all -> {
|
|
||||||
toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
|
|
||||||
mainViewModel.testAllRealPing()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.service_restart -> {
|
|
||||||
restartV2Ray()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.del_all_config -> {
|
|
||||||
delAllConfig()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.del_duplicate_config -> {
|
|
||||||
delDuplicateConfig()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.del_invalid_config -> {
|
|
||||||
delInvalidConfig()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.sort_by_test_results -> {
|
|
||||||
sortByTestResults()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.sub_update -> {
|
|
||||||
importConfigViaSub()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importManually(createConfigType: Int) {
|
|
||||||
startActivity(
|
|
||||||
Intent()
|
|
||||||
.putExtra("createConfigType", createConfigType)
|
|
||||||
.putExtra("subscriptionId", mainViewModel.subscriptionId)
|
|
||||||
.setClass(this, ServerActivity::class.java)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* import config from qrcode
|
|
||||||
*/
|
|
||||||
private fun importQRcode(): Boolean {
|
|
||||||
val permission = Manifest.permission.CAMERA
|
|
||||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
} else {
|
|
||||||
pendingAction = Action.IMPORT_QR_CODE_CONFIG
|
|
||||||
requestPermissionLauncher.launch(permission)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* import config from clipboard
|
|
||||||
*/
|
|
||||||
private fun importClipboard()
|
|
||||||
: Boolean {
|
|
||||||
try {
|
|
||||||
val clipboard = Utils.getClipboard(this)
|
|
||||||
importBatchConfig(clipboard)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to import config from clipboard", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importBatchConfig(server: String?) {
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
|
|
||||||
delay(500L)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
when {
|
|
||||||
count > 0 -> {
|
|
||||||
toast(getString(R.string.title_import_config_count, count))
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
}
|
|
||||||
|
|
||||||
countSub > 0 -> initGroupTab()
|
|
||||||
else -> toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
Log.e(AppConfig.TAG, "Failed to import batch config", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* import config from local config file
|
|
||||||
*/
|
|
||||||
private fun importConfigLocal(): Boolean {
|
|
||||||
try {
|
|
||||||
showFileChooser()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to import config from local file", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* import config from sub
|
|
||||||
*/
|
|
||||||
private fun importConfigViaSub(): Boolean {
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val count = mainViewModel.updateConfigViaSubAll()
|
|
||||||
delay(500L)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (count > 0) {
|
|
||||||
toast(getString(R.string.title_update_config_count, count))
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
} else {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exportAll() {
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.exportAllServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (ret > 0)
|
|
||||||
toast(getString(R.string.title_export_config_count, ret))
|
|
||||||
else
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun delAllConfig() {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.removeAllServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
toast(getString(R.string.title_del_config_count, ret))
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun delDuplicateConfig() {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.removeDuplicateServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
toast(getString(R.string.title_del_duplicate_config_count, ret))
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun delInvalidConfig() {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.removeInvalidServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
toast(getString(R.string.title_del_config_count, ret))
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sortByTestResults() {
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
mainViewModel.sortByTestResults()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* show file chooser
|
|
||||||
*/
|
|
||||||
private fun showFileChooser() {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
intent.type = "*/*"
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
|
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
|
||||||
} else {
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
pendingAction = Action.READ_CONTENT_FROM_URI
|
|
||||||
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
|
||||||
} else {
|
|
||||||
requestPermissionLauncher.launch(permission)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* read content from uri
|
|
||||||
*/
|
|
||||||
private fun readContentFromUri(uri: Uri) {
|
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
|
||||||
} else {
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
try {
|
|
||||||
contentResolver.openInputStream(uri).use { input ->
|
|
||||||
importBatchConfig(input?.bufferedReader()?.readText())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to read content from URI", e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
requestPermissionLauncher.launch(permission)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setTestState(content: String?) {
|
|
||||||
binding.tvTestState.text = content
|
|
||||||
}
|
|
||||||
|
|
||||||
// val mConnection = object : ServiceConnection {
|
|
||||||
// override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
|
||||||
// sendMsg(AppConfig.MSG_REGISTER_CLIENT, "")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
|
|
||||||
moveTaskToBack(false)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
|
||||||
// Handle navigation view item clicks here.
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
|
|
||||||
R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java))
|
|
||||||
R.id.routing_setting -> requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
|
|
||||||
R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java))
|
|
||||||
R.id.settings -> startActivity(
|
|
||||||
Intent(this, SettingsActivity::class.java)
|
|
||||||
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
|
||||||
)
|
|
||||||
|
|
||||||
R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
|
|
||||||
R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
|
|
||||||
R.id.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java))
|
|
||||||
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,362 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.v2ray.ang.AngApplication.Companion.application
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
|
||||||
companion object {
|
|
||||||
private const val VIEW_TYPE_ITEM = 1
|
|
||||||
private const val VIEW_TYPE_FOOTER = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mActivity: MainActivity = activity
|
|
||||||
private val share_method: Array<out String> by lazy {
|
|
||||||
mActivity.resources.getStringArray(R.array.share_method)
|
|
||||||
}
|
|
||||||
private val share_method_more: Array<out String> by lazy {
|
|
||||||
mActivity.resources.getStringArray(R.array.share_method_more)
|
|
||||||
}
|
|
||||||
var isRunning = false
|
|
||||||
private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the total number of items in the adapter (servers count + footer view)
|
|
||||||
* @return The total item count
|
|
||||||
*/
|
|
||||||
override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
|
||||||
if (holder is MainViewHolder) {
|
|
||||||
val guid = mActivity.mainViewModel.serversCache[position].guid
|
|
||||||
val profile = mActivity.mainViewModel.serversCache[position].profile
|
|
||||||
val isCustom = profile.configType == EConfigType.CUSTOM
|
|
||||||
|
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
|
||||||
|
|
||||||
//Name address
|
|
||||||
holder.itemMainBinding.tvName.text = profile.remarks
|
|
||||||
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
|
||||||
holder.itemMainBinding.tvType.text = profile.configType.name
|
|
||||||
|
|
||||||
//TestResult
|
|
||||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
|
||||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
|
||||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
|
||||||
} else {
|
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
|
||||||
}
|
|
||||||
|
|
||||||
//layoutIndicator
|
|
||||||
if (guid == MmkvManager.getSelectServer()) {
|
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
|
|
||||||
} else {
|
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//subscription remarks
|
|
||||||
val subRemarks = getSubscriptionRemarks(profile)
|
|
||||||
holder.itemMainBinding.tvSubscription.text = subRemarks
|
|
||||||
holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
|
|
||||||
//layout
|
|
||||||
if (doubleColumnDisplay) {
|
|
||||||
holder.itemMainBinding.layoutShare.visibility = View.GONE
|
|
||||||
holder.itemMainBinding.layoutEdit.visibility = View.GONE
|
|
||||||
holder.itemMainBinding.layoutRemove.visibility = View.GONE
|
|
||||||
holder.itemMainBinding.layoutMore.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
//share method
|
|
||||||
val shareOptions = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutMore.setOnClickListener {
|
|
||||||
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holder.itemMainBinding.layoutShare.visibility = View.VISIBLE
|
|
||||||
holder.itemMainBinding.layoutEdit.visibility = View.VISIBLE
|
|
||||||
holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE
|
|
||||||
holder.itemMainBinding.layoutMore.visibility = View.GONE
|
|
||||||
|
|
||||||
//share method
|
|
||||||
val shareOptions = if (isCustom) share_method.asList().takeLast(1) else share_method.asList()
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
|
||||||
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
|
||||||
editServer(guid, profile)
|
|
||||||
}
|
|
||||||
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
|
||||||
removeServer(guid, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
|
||||||
setSelectServer(guid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if (holder is FooterViewHolder) {
|
|
||||||
// if (true) {
|
|
||||||
// holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
|
||||||
// } else {
|
|
||||||
// holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
|
||||||
// Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the server address information
|
|
||||||
* Hides part of IP or domain information for privacy protection
|
|
||||||
* @param profile The server configuration
|
|
||||||
* @return Formatted address string
|
|
||||||
*/
|
|
||||||
private fun getAddress(profile: ProfileItem): String {
|
|
||||||
// Hide xxx:xxx:***/xxx.xxx.xxx.***
|
|
||||||
return "${
|
|
||||||
profile.server?.let {
|
|
||||||
if (it.contains(":"))
|
|
||||||
it.split(":").take(2).joinToString(":", postfix = ":***")
|
|
||||||
else
|
|
||||||
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
|
|
||||||
}
|
|
||||||
} : ${profile.serverPort}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the subscription remarks information
|
|
||||||
* @param profile The server configuration
|
|
||||||
* @return Subscription remarks string, or empty string if none
|
|
||||||
*/
|
|
||||||
private fun getSubscriptionRemarks(profile: ProfileItem): String {
|
|
||||||
val subRemarks =
|
|
||||||
if (mActivity.mainViewModel.subscriptionId.isEmpty())
|
|
||||||
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
|
|
||||||
else
|
|
||||||
null
|
|
||||||
return subRemarks?.toString() ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares server configuration
|
|
||||||
* Displays a dialog with sharing options and executes the selected action
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
* @param profile The server configuration
|
|
||||||
* @param position The position in the list
|
|
||||||
* @param shareOptions The list of share options
|
|
||||||
* @param skip The number of options to skip
|
|
||||||
*/
|
|
||||||
private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List<String>, skip: Int) {
|
|
||||||
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
|
||||||
try {
|
|
||||||
when (i + skip) {
|
|
||||||
0 -> showQRCode(guid)
|
|
||||||
1 -> share2Clipboard(guid)
|
|
||||||
2 -> shareFullContent(guid)
|
|
||||||
3 -> editServer(guid, profile)
|
|
||||||
4 -> removeServer(guid, position)
|
|
||||||
else -> mActivity.toast("else")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Error when sharing server", e)
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays QR code for the server configuration
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
*/
|
|
||||||
private fun showQRCode(guid: String) {
|
|
||||||
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
|
||||||
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
|
||||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares server configuration to clipboard
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
*/
|
|
||||||
private fun share2Clipboard(guid: String) {
|
|
||||||
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
|
|
||||||
mActivity.toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
mActivity.toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares full server configuration content to clipboard
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
*/
|
|
||||||
private fun shareFullContent(guid: String) {
|
|
||||||
mActivity.lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (result == 0) {
|
|
||||||
mActivity.toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
mActivity.toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edits server configuration
|
|
||||||
* Opens appropriate editing interface based on configuration type
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
* @param profile The server configuration
|
|
||||||
*/
|
|
||||||
private fun editServer(guid: String, profile: ProfileItem) {
|
|
||||||
val intent = Intent().putExtra("guid", guid)
|
|
||||||
.putExtra("isRunning", isRunning)
|
|
||||||
.putExtra("createConfigType", profile.configType.value)
|
|
||||||
if (profile.configType == EConfigType.CUSTOM) {
|
|
||||||
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
|
||||||
} else {
|
|
||||||
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes server configuration
|
|
||||||
* Handles confirmation dialog and related checks
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
* @param position The position in the list
|
|
||||||
*/
|
|
||||||
private fun removeServer(guid: String, position: Int) {
|
|
||||||
if (guid != MmkvManager.getSelectServer()) {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
|
||||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
removeServerSub(guid, position)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
removeServerSub(guid, position)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
application.toast(R.string.toast_action_not_allowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the actual server removal process
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
* @param position The position in the list
|
|
||||||
*/
|
|
||||||
private fun removeServerSub(guid: String, position: Int) {
|
|
||||||
mActivity.mainViewModel.removeServer(guid)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the selected server
|
|
||||||
* Updates UI and restarts service if needed
|
|
||||||
* @param guid The server unique identifier to select
|
|
||||||
*/
|
|
||||||
private fun setSelectServer(guid: String) {
|
|
||||||
val selected = MmkvManager.getSelectServer()
|
|
||||||
if (guid != selected) {
|
|
||||||
MmkvManager.setSelectServer(guid)
|
|
||||||
if (!TextUtils.isEmpty(selected)) {
|
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
|
|
||||||
}
|
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
|
||||||
if (isRunning) {
|
|
||||||
V2RayServiceManager.stopVService(mActivity)
|
|
||||||
mActivity.lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
delay(500)
|
|
||||||
V2RayServiceManager.startVService(mActivity)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
|
||||||
return when (viewType) {
|
|
||||||
VIEW_TYPE_ITEM ->
|
|
||||||
MainViewHolder(ItemRecyclerMainBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
|
|
||||||
else ->
|
|
||||||
FooterViewHolder(ItemRecyclerFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
return if (position == mActivity.mainViewModel.serversCache.size) {
|
|
||||||
VIEW_TYPE_FOOTER
|
|
||||||
} else {
|
|
||||||
VIEW_TYPE_ITEM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
fun onItemSelected() {
|
|
||||||
itemView.setBackgroundColor(Color.LTGRAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onItemClear() {
|
|
||||||
itemView.setBackgroundColor(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainViewHolder(val itemMainBinding: ItemRecyclerMainBinding) :
|
|
||||||
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
|
|
||||||
|
|
||||||
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
|
||||||
BaseViewHolder(itemFooterBinding.root)
|
|
||||||
|
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
|
||||||
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
|
||||||
notifyItemMoved(fromPosition, toPosition)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemMoveCompleted() {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,279 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityBypassListBinding
|
|
||||||
import com.v2ray.ang.dto.AppInfo
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.extension.v2RayApplication
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import es.dmoral.toasty.Toasty
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.text.Collator
|
|
||||||
|
|
||||||
class PerAppProxyActivity : BaseActivity() {
|
|
||||||
private val binding by lazy { ActivityBypassListBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
private var adapter: PerAppProxyAdapter? = null
|
|
||||||
private var appsAll: List<AppInfo>? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.per_app_proxy_settings)
|
|
||||||
|
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
|
||||||
val apps = withContext(Dispatchers.IO) {
|
|
||||||
val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
|
|
||||||
|
|
||||||
if (blacklist != null) {
|
|
||||||
appsList.forEach { app ->
|
|
||||||
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
|
|
||||||
}
|
|
||||||
appsList.sortedWith { p1, p2 ->
|
|
||||||
when {
|
|
||||||
p1.isSelected > p2.isSelected -> -1
|
|
||||||
p1.isSelected == p2.isSelected -> 0
|
|
||||||
else -> 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val collator = Collator.getInstance()
|
|
||||||
appsList.sortedWith(compareBy(collator) { it.appName })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appsAll = apps
|
|
||||||
adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
Log.e(ANG_PACKAGE, "Error loading apps", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
|
||||||
}
|
|
||||||
binding.switchPerAppProxy.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
|
|
||||||
|
|
||||||
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
|
||||||
}
|
|
||||||
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
|
||||||
|
|
||||||
binding.layoutSwitchBypassAppsTips.setOnClickListener {
|
|
||||||
Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
adapter?.let {
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.menu_bypass_list, menu)
|
|
||||||
|
|
||||||
val searchItem = menu.findItem(R.id.search_view)
|
|
||||||
if (searchItem != null) {
|
|
||||||
val searchView = searchItem.actionView as SearchView
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
filterProxyApp(newText.orEmpty())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.select_all -> adapter?.let { it ->
|
|
||||||
val pkgNames = it.apps.map { it.packageName }
|
|
||||||
if (it.blacklist.containsAll(pkgNames)) {
|
|
||||||
it.apps.forEach {
|
|
||||||
val packageName = it.packageName
|
|
||||||
adapter?.blacklist?.remove(packageName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
it.apps.forEach {
|
|
||||||
val packageName = it.packageName
|
|
||||||
adapter?.blacklist?.add(packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.notifyDataSetChanged()
|
|
||||||
true
|
|
||||||
} == true
|
|
||||||
|
|
||||||
R.id.select_proxy_app -> {
|
|
||||||
selectProxyApp()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_proxy_app -> {
|
|
||||||
importProxyApp()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.export_proxy_app -> {
|
|
||||||
exportProxyApp()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun selectProxyApp() {
|
|
||||||
toast(R.string.msg_downloading_content)
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
|
|
||||||
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
var content = HttpUtil.getUrlContent(url, 5000)
|
|
||||||
if (content.isNullOrEmpty()) {
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
|
|
||||||
}
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
Log.i(AppConfig.TAG, content)
|
|
||||||
selectProxyApp(content, true)
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importProxyApp() {
|
|
||||||
val content = Utils.getClipboard(applicationContext)
|
|
||||||
if (TextUtils.isEmpty(content)) return
|
|
||||||
selectProxyApp(content, false)
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exportProxyApp() {
|
|
||||||
var lst = binding.switchBypassApps.isChecked.toString()
|
|
||||||
|
|
||||||
adapter?.blacklist?.forEach block@{
|
|
||||||
lst = lst + System.getProperty("line.separator") + it
|
|
||||||
}
|
|
||||||
Utils.setClipboard(applicationContext, lst)
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun selectProxyApp(content: String, force: Boolean): Boolean {
|
|
||||||
try {
|
|
||||||
val proxyApps = if (TextUtils.isEmpty(content)) {
|
|
||||||
Utils.readTextFromAssets(v2RayApplication, "proxy_packagename.txt")
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(proxyApps)) return false
|
|
||||||
|
|
||||||
adapter?.blacklist?.clear()
|
|
||||||
|
|
||||||
if (binding.switchBypassApps.isChecked) {
|
|
||||||
adapter?.let { it ->
|
|
||||||
it.apps.forEach block@{
|
|
||||||
val packageName = it.packageName
|
|
||||||
Log.i(AppConfig.TAG, packageName)
|
|
||||||
if (!inProxyApps(proxyApps, packageName, force)) {
|
|
||||||
adapter?.blacklist?.add(packageName)
|
|
||||||
println(packageName)
|
|
||||||
return@block
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
adapter?.let { it ->
|
|
||||||
it.apps.forEach block@{
|
|
||||||
val packageName = it.packageName
|
|
||||||
Log.i(AppConfig.TAG, packageName)
|
|
||||||
if (inProxyApps(proxyApps, packageName, force)) {
|
|
||||||
adapter?.blacklist?.add(packageName)
|
|
||||||
println(packageName)
|
|
||||||
return@block
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Error selecting proxy app", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun inProxyApps(proxyApps: String, packageName: String, force: Boolean): Boolean {
|
|
||||||
if (force) {
|
|
||||||
if (packageName == "com.google.android.webview") return false
|
|
||||||
if (packageName.startsWith("com.google")) return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxyApps.indexOf(packageName) >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filterProxyApp(content: String): Boolean {
|
|
||||||
val apps = ArrayList<AppInfo>()
|
|
||||||
|
|
||||||
val key = content.uppercase()
|
|
||||||
if (key.isNotEmpty()) {
|
|
||||||
appsAll?.forEach {
|
|
||||||
if (it.appName.uppercase().indexOf(key) >= 0
|
|
||||||
|| it.packageName.uppercase().indexOf(key) >= 0
|
|
||||||
) {
|
|
||||||
apps.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appsAll?.forEach {
|
|
||||||
apps.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
refreshData()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
fun refreshData() {
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class RoutingEditActivity : BaseActivity() {
|
|
||||||
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
|
|
||||||
private val position by lazy { intent.getIntExtra("position", -1) }
|
|
||||||
|
|
||||||
private val outbound_tag: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.outbound_tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
title = getString(R.string.routing_settings_rule_title)
|
|
||||||
|
|
||||||
val rulesetItem = SettingsManager.getRoutingRuleset(position)
|
|
||||||
if (rulesetItem != null) {
|
|
||||||
bindingServer(rulesetItem)
|
|
||||||
} else {
|
|
||||||
clearServer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
|
|
||||||
binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
|
|
||||||
binding.chkLocked.isChecked = rulesetItem.locked == true
|
|
||||||
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
|
|
||||||
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
|
|
||||||
binding.etPort.text = Utils.getEditable(rulesetItem.port)
|
|
||||||
binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
|
|
||||||
binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
|
|
||||||
val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag)
|
|
||||||
binding.spOutboundTag.setSelection(outbound)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearServer(): Boolean {
|
|
||||||
binding.etRemarks.text = null
|
|
||||||
binding.spOutboundTag.setSelection(0)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveServer(): Boolean {
|
|
||||||
val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem()
|
|
||||||
|
|
||||||
rulesetItem.apply {
|
|
||||||
remarks = binding.etRemarks.text.toString()
|
|
||||||
locked = binding.chkLocked.isChecked
|
|
||||||
domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() }
|
|
||||||
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
|
||||||
ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() }
|
|
||||||
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
|
||||||
protocol = binding.etProtocol.text.toString().takeIf { it.isNotEmpty() }
|
|
||||||
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
|
||||||
port = binding.etPort.text.toString().takeIf { it.isNotEmpty() }
|
|
||||||
network = binding.etNetwork.text.toString().takeIf { it.isNotEmpty() }
|
|
||||||
outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rulesetItem.remarks.isNullOrEmpty()) {
|
|
||||||
toast(R.string.sub_setting_remarks)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsManager.saveRoutingRuleset(position, rulesetItem)
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun deleteServer(): Boolean {
|
|
||||||
if (position >= 0) {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
SettingsManager.removeRoutingRuleset(position)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.action_server, menu)
|
|
||||||
val del_config = menu.findItem(R.id.del_config)
|
|
||||||
|
|
||||||
if (position < 0) {
|
|
||||||
del_config?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.del_config -> {
|
|
||||||
deleteServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.save_config -> {
|
|
||||||
saveServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,204 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class RoutingSettingActivity : BaseActivity() {
|
|
||||||
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
var rulesets: MutableList<RulesetItem> = mutableListOf()
|
|
||||||
private val adapter by lazy { RoutingSettingRecyclerAdapter(this) }
|
|
||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
|
||||||
private val routing_domain_strategy: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.routing_domain_strategy)
|
|
||||||
}
|
|
||||||
private val preset_rulesets: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.preset_rulesets)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val requestCameraPermissionLauncher = registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { isGranted: Boolean ->
|
|
||||||
if (isGranted) {
|
|
||||||
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.routing_settings_title)
|
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
|
||||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
|
||||||
|
|
||||||
binding.tvDomainStrategySummary.text = getDomainStrategy()
|
|
||||||
binding.layoutDomainStrategy.setOnClickListener {
|
|
||||||
setDomainStrategy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.menu_routing_setting, menu)
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
|
||||||
R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true }
|
|
||||||
R.id.import_predefined_rulesets -> importPredefined().let { true }
|
|
||||||
R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true }
|
|
||||||
R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true }
|
|
||||||
R.id.export_rulesets_to_clipboard -> export2Clipboard().let { true }
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDomainStrategy(): String {
|
|
||||||
return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setDomainStrategy() {
|
|
||||||
android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i ->
|
|
||||||
try {
|
|
||||||
val value = routing_domain_strategy[i]
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
|
|
||||||
binding.tvDomainStrategySummary.text = value
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to set domain strategy", e)
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importPredefined() {
|
|
||||||
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
try {
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
refreshData()
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do nothing
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importFromClipboard() {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val clipboard = try {
|
|
||||||
Utils.getClipboard(this)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
return@setPositiveButton
|
|
||||||
}
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = SettingsManager.resetRoutingRulesets(clipboard)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (result) {
|
|
||||||
refreshData()
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do nothing
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun export2Clipboard() {
|
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
|
||||||
if (rulesetList.isNullOrEmpty()) {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
} else {
|
|
||||||
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
importRulesetsFromQRcode(it.data?.getStringExtra("SCAN_RESULT"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importRulesetsFromQRcode(qrcode: String?): Boolean {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = SettingsManager.resetRoutingRulesets(qrcode)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (result) {
|
|
||||||
refreshData()
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do nothing
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
fun refreshData() {
|
|
||||||
rulesets.clear()
|
|
||||||
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerRoutingSettingBinding
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
|
||||||
|
|
||||||
class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
|
|
||||||
ItemTouchHelperAdapter {
|
|
||||||
|
|
||||||
private var mActivity: RoutingSettingActivity = activity
|
|
||||||
override fun getItemCount() = mActivity.rulesets.size
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
|
||||||
val ruleset = mActivity.rulesets[position]
|
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks
|
|
||||||
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString()
|
|
||||||
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
|
|
||||||
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
|
|
||||||
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true
|
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
|
||||||
mActivity.startActivity(
|
|
||||||
Intent(mActivity, RoutingEditActivity::class.java)
|
|
||||||
.putExtra("position", position)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
|
||||||
if (!it.isPressed) return@setOnCheckedChangeListener
|
|
||||||
ruleset.enabled = isChecked
|
|
||||||
SettingsManager.saveRoutingRuleset(position, ruleset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
|
||||||
return MainViewHolder(
|
|
||||||
ItemRecyclerRoutingSettingBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainViewHolder(val itemRoutingSettingBinding: ItemRecyclerRoutingSettingBinding) :
|
|
||||||
BaseViewHolder(itemRoutingSettingBinding.root), ItemTouchHelperViewHolder
|
|
||||||
|
|
||||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
fun onItemSelected() {
|
|
||||||
itemView.setBackgroundColor(Color.LTGRAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onItemClear() {
|
|
||||||
itemView.setBackgroundColor(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
|
||||||
SettingsManager.swapRoutingRuleset(fromPosition, toPosition)
|
|
||||||
notifyItemMoved(fromPosition, toPosition)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemMoveCompleted() {
|
|
||||||
mActivity.refreshData()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
|
||||||
|
|
||||||
class ScScannerActivity : BaseActivity() {
|
|
||||||
|
|
||||||
private val requestCameraPermissionLauncher = registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { isGranted: Boolean ->
|
|
||||||
if (isGranted) {
|
|
||||||
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_none)
|
|
||||||
importQRcode()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importQRcode(): Boolean {
|
|
||||||
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
|
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
|
|
||||||
|
|
||||||
if (count + countSub > 0) {
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,652 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.DEFAULT_PORT
|
|
||||||
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
|
||||||
import com.v2ray.ang.AppConfig.REALITY
|
|
||||||
import com.v2ray.ang.AppConfig.TLS
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.NetworkType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class ServerActivity : BaseActivity() {
|
|
||||||
|
|
||||||
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
|
|
||||||
private val isRunning by lazy {
|
|
||||||
intent.getBooleanExtra("isRunning", false)
|
|
||||||
&& editGuid.isNotEmpty()
|
|
||||||
&& editGuid == MmkvManager.getSelectServer()
|
|
||||||
}
|
|
||||||
private val createConfigType by lazy {
|
|
||||||
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value))
|
|
||||||
?: EConfigType.VMESS
|
|
||||||
}
|
|
||||||
private val subscriptionId by lazy {
|
|
||||||
intent.getStringExtra("subscriptionId")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val securitys: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.securitys)
|
|
||||||
}
|
|
||||||
private val shadowsocksSecuritys: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.ss_securitys)
|
|
||||||
}
|
|
||||||
private val flows: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.flows)
|
|
||||||
}
|
|
||||||
private val networks: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.networks)
|
|
||||||
}
|
|
||||||
private val tcpTypes: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.header_type_tcp)
|
|
||||||
}
|
|
||||||
private val kcpAndQuicTypes: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.header_type_kcp_and_quic)
|
|
||||||
}
|
|
||||||
private val grpcModes: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.mode_type_grpc)
|
|
||||||
}
|
|
||||||
private val streamSecuritys: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.streamsecurityxs)
|
|
||||||
}
|
|
||||||
private val allowinsecures: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.allowinsecures)
|
|
||||||
}
|
|
||||||
private val uTlsItems: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.streamsecurity_utls)
|
|
||||||
}
|
|
||||||
private val alpns: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.streamsecurity_alpn)
|
|
||||||
}
|
|
||||||
private val xhttpMode: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.xhttp_mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
|
|
||||||
// We don't use AndroidViewBinding because, it is better to share similar logics for different
|
|
||||||
// protocols. Use findViewById manually ensures the xml are de-coupled with the activity logic.
|
|
||||||
private val et_remarks: EditText by lazy { findViewById(R.id.et_remarks) }
|
|
||||||
private val et_address: EditText by lazy { findViewById(R.id.et_address) }
|
|
||||||
private val et_port: EditText by lazy { findViewById(R.id.et_port) }
|
|
||||||
private val et_id: EditText by lazy { findViewById(R.id.et_id) }
|
|
||||||
private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
|
|
||||||
private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
|
|
||||||
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
|
|
||||||
private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
|
|
||||||
private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
|
|
||||||
private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.lay_allow_insecure) }
|
|
||||||
private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
|
|
||||||
private val container_sni: LinearLayout? by lazy { findViewById(R.id.lay_sni) }
|
|
||||||
private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
|
|
||||||
private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.lay_stream_fingerprint) }
|
|
||||||
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
|
|
||||||
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
|
|
||||||
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
|
|
||||||
private val tv_request_host: TextView? by lazy { findViewById(R.id.tv_request_host) }
|
|
||||||
private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
|
|
||||||
private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) }
|
|
||||||
private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
|
|
||||||
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
|
||||||
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
|
|
||||||
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
|
|
||||||
private val et_preshared_key: EditText? by lazy { findViewById(R.id.et_preshared_key) }
|
|
||||||
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
|
|
||||||
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
|
|
||||||
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
|
|
||||||
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
|
|
||||||
private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.lay_spider_x) }
|
|
||||||
private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
|
|
||||||
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
|
|
||||||
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
|
|
||||||
private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
|
|
||||||
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
|
|
||||||
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
|
|
||||||
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
|
|
||||||
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
|
|
||||||
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
|
|
||||||
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
|
|
||||||
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
title = getString(R.string.title_server)
|
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid)
|
|
||||||
when (config?.configType ?: createConfigType) {
|
|
||||||
EConfigType.VMESS -> setContentView(R.layout.activity_server_vmess)
|
|
||||||
EConfigType.CUSTOM -> return
|
|
||||||
EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
|
|
||||||
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
|
|
||||||
EConfigType.HTTP -> setContentView(R.layout.activity_server_socks)
|
|
||||||
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
|
|
||||||
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
|
|
||||||
EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard)
|
|
||||||
EConfigType.HYSTERIA2 -> setContentView(R.layout.activity_server_hysteria2)
|
|
||||||
}
|
|
||||||
sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(
|
|
||||||
parent: AdapterView<*>?,
|
|
||||||
view: View?,
|
|
||||||
position: Int,
|
|
||||||
id: Long,
|
|
||||||
) {
|
|
||||||
val types = transportTypes(networks[position])
|
|
||||||
sp_header_type?.isEnabled = types.size > 1
|
|
||||||
val adapter =
|
|
||||||
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
||||||
sp_header_type?.adapter = adapter
|
|
||||||
sp_header_type_title?.text =
|
|
||||||
when (networks[position]) {
|
|
||||||
NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type)
|
|
||||||
NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode)
|
|
||||||
else -> getString(R.string.server_lab_head_type)
|
|
||||||
}.orEmpty()
|
|
||||||
sp_header_type?.setSelection(
|
|
||||||
Utils.arrayFind(
|
|
||||||
types,
|
|
||||||
when (networks[position]) {
|
|
||||||
NetworkType.GRPC.type -> config?.mode
|
|
||||||
NetworkType.XHTTP.type -> config?.xhttpMode
|
|
||||||
else -> config?.headerType
|
|
||||||
}.orEmpty()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
et_request_host?.text = Utils.getEditable(
|
|
||||||
when (networks[position]) {
|
|
||||||
//"quic" -> config?.quicSecurity
|
|
||||||
NetworkType.GRPC.type -> config?.authority
|
|
||||||
else -> config?.host
|
|
||||||
}.orEmpty()
|
|
||||||
)
|
|
||||||
et_path?.text = Utils.getEditable(
|
|
||||||
when (networks[position]) {
|
|
||||||
NetworkType.KCP.type -> config?.seed
|
|
||||||
//"quic" -> config?.quicKey
|
|
||||||
NetworkType.GRPC.type -> config?.serviceName
|
|
||||||
else -> config?.path
|
|
||||||
}.orEmpty()
|
|
||||||
)
|
|
||||||
|
|
||||||
tv_request_host?.text = Utils.getEditable(
|
|
||||||
getString(
|
|
||||||
when (networks[position]) {
|
|
||||||
NetworkType.TCP.type -> R.string.server_lab_request_host_http
|
|
||||||
NetworkType.WS.type -> R.string.server_lab_request_host_ws
|
|
||||||
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade
|
|
||||||
NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp
|
|
||||||
NetworkType.H2.type -> R.string.server_lab_request_host_h2
|
|
||||||
//"quic" -> R.string.server_lab_request_host_quic
|
|
||||||
NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc
|
|
||||||
else -> R.string.server_lab_request_host
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
tv_path?.text = Utils.getEditable(
|
|
||||||
getString(
|
|
||||||
when (networks[position]) {
|
|
||||||
NetworkType.KCP.type -> R.string.server_lab_path_kcp
|
|
||||||
NetworkType.WS.type -> R.string.server_lab_path_ws
|
|
||||||
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
|
|
||||||
NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
|
|
||||||
NetworkType.H2.type -> R.string.server_lab_path_h2
|
|
||||||
//"quic" -> R.string.server_lab_path_quic
|
|
||||||
NetworkType.GRPC.type -> R.string.server_lab_path_grpc
|
|
||||||
else -> R.string.server_lab_path
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
et_extra?.text = Utils.getEditable(
|
|
||||||
when (networks[position]) {
|
|
||||||
NetworkType.XHTTP.type -> config?.xhttpExtra
|
|
||||||
else -> null
|
|
||||||
}.orEmpty()
|
|
||||||
)
|
|
||||||
|
|
||||||
layout_extra?.visibility =
|
|
||||||
when (networks[position]) {
|
|
||||||
NetworkType.XHTTP.type -> View.VISIBLE
|
|
||||||
else -> View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sp_stream_security?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(
|
|
||||||
parent: AdapterView<*>?,
|
|
||||||
view: View?,
|
|
||||||
position: Int,
|
|
||||||
id: Long,
|
|
||||||
) {
|
|
||||||
val isBlank = streamSecuritys[position].isBlank()
|
|
||||||
val isTLS = streamSecuritys[position] == TLS
|
|
||||||
|
|
||||||
when {
|
|
||||||
// Case 1: Null or blank
|
|
||||||
isBlank -> {
|
|
||||||
listOf(
|
|
||||||
container_sni, container_fingerprint, container_alpn,
|
|
||||||
container_allow_insecure, container_public_key,
|
|
||||||
container_short_id, container_spider_x
|
|
||||||
).forEach { it?.visibility = View.GONE }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2: TLS value
|
|
||||||
isTLS -> {
|
|
||||||
listOf(
|
|
||||||
container_sni,
|
|
||||||
container_fingerprint,
|
|
||||||
container_alpn
|
|
||||||
).forEach { it?.visibility = View.VISIBLE }
|
|
||||||
container_allow_insecure?.visibility = View.VISIBLE
|
|
||||||
listOf(
|
|
||||||
container_public_key,
|
|
||||||
container_short_id,
|
|
||||||
container_spider_x
|
|
||||||
).forEach { it?.visibility = View.GONE }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 3: Other reality values
|
|
||||||
else -> {
|
|
||||||
listOf(container_sni, container_fingerprint).forEach {
|
|
||||||
it?.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
container_alpn?.visibility = View.GONE
|
|
||||||
container_allow_insecure?.visibility = View.GONE
|
|
||||||
listOf(
|
|
||||||
container_public_key,
|
|
||||||
container_short_id,
|
|
||||||
container_spider_x
|
|
||||||
).forEach { it?.visibility = View.VISIBLE }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(p0: AdapterView<*>?) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (config != null) {
|
|
||||||
bindingServer(config)
|
|
||||||
} else {
|
|
||||||
clearServer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* binding selected server config
|
|
||||||
*/
|
|
||||||
private fun bindingServer(config: ProfileItem): Boolean {
|
|
||||||
|
|
||||||
et_remarks.text = Utils.getEditable(config.remarks)
|
|
||||||
et_address.text = Utils.getEditable(config.server.orEmpty())
|
|
||||||
et_port.text = Utils.getEditable(config.serverPort ?: DEFAULT_PORT.toString())
|
|
||||||
et_id.text = Utils.getEditable(config.password.orEmpty())
|
|
||||||
|
|
||||||
if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
|
|
||||||
et_security?.text = Utils.getEditable(config.username.orEmpty())
|
|
||||||
} else if (config.configType == EConfigType.VLESS) {
|
|
||||||
et_security?.text = Utils.getEditable(config.method.orEmpty())
|
|
||||||
val flow = Utils.arrayFind(flows, config.flow.orEmpty())
|
|
||||||
if (flow >= 0) {
|
|
||||||
sp_flow?.setSelection(flow)
|
|
||||||
}
|
|
||||||
} else if (config.configType == EConfigType.WIREGUARD) {
|
|
||||||
et_id.text = Utils.getEditable(config.secretKey.orEmpty())
|
|
||||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
|
||||||
et_preshared_key?.visibility = View.VISIBLE
|
|
||||||
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
|
|
||||||
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
|
|
||||||
et_local_address?.text = Utils.getEditable(
|
|
||||||
config.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
)
|
|
||||||
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
|
|
||||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
|
||||||
et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
|
|
||||||
et_port_hop?.text = Utils.getEditable(config.portHopping)
|
|
||||||
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
|
|
||||||
et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
|
|
||||||
et_bandwidth_down?.text = Utils.getEditable(config.bandwidthDown)
|
|
||||||
et_bandwidth_up?.text = Utils.getEditable(config.bandwidthUp)
|
|
||||||
}
|
|
||||||
val securityEncryptions =
|
|
||||||
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
|
||||||
val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
|
|
||||||
if (security >= 0) {
|
|
||||||
sp_security?.setSelection(security)
|
|
||||||
}
|
|
||||||
|
|
||||||
val streamSecurity = Utils.arrayFind(streamSecuritys, config.security.orEmpty())
|
|
||||||
if (streamSecurity >= 0) {
|
|
||||||
sp_stream_security?.setSelection(streamSecurity)
|
|
||||||
container_sni?.visibility = View.VISIBLE
|
|
||||||
container_fingerprint?.visibility = View.VISIBLE
|
|
||||||
container_alpn?.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
et_sni?.text = Utils.getEditable(config.sni)
|
|
||||||
config.fingerPrint?.let { it ->
|
|
||||||
val utlsIndex = Utils.arrayFind(uTlsItems, it)
|
|
||||||
utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) }
|
|
||||||
}
|
|
||||||
config.alpn?.let { it ->
|
|
||||||
val alpnIndex = Utils.arrayFind(alpns, it)
|
|
||||||
alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) }
|
|
||||||
}
|
|
||||||
if (config.security == TLS) {
|
|
||||||
container_allow_insecure?.visibility = View.VISIBLE
|
|
||||||
val allowinsecure = Utils.arrayFind(allowinsecures, config.insecure.toString())
|
|
||||||
if (allowinsecure >= 0) {
|
|
||||||
sp_allow_insecure?.setSelection(allowinsecure)
|
|
||||||
}
|
|
||||||
container_public_key?.visibility = View.GONE
|
|
||||||
container_short_id?.visibility = View.GONE
|
|
||||||
container_spider_x?.visibility = View.GONE
|
|
||||||
} else if (config.security == REALITY) {
|
|
||||||
container_public_key?.visibility = View.VISIBLE
|
|
||||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
|
||||||
container_short_id?.visibility = View.VISIBLE
|
|
||||||
et_short_id?.text = Utils.getEditable(config.shortId.orEmpty())
|
|
||||||
container_spider_x?.visibility = View.VISIBLE
|
|
||||||
et_spider_x?.text = Utils.getEditable(config.spiderX.orEmpty())
|
|
||||||
container_allow_insecure?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.security.isNullOrEmpty()) {
|
|
||||||
container_sni?.visibility = View.GONE
|
|
||||||
container_fingerprint?.visibility = View.GONE
|
|
||||||
container_alpn?.visibility = View.GONE
|
|
||||||
container_allow_insecure?.visibility = View.GONE
|
|
||||||
container_public_key?.visibility = View.GONE
|
|
||||||
container_short_id?.visibility = View.GONE
|
|
||||||
container_spider_x?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
val network = Utils.arrayFind(networks, config.network.orEmpty())
|
|
||||||
if (network >= 0) {
|
|
||||||
sp_network?.setSelection(network)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear or init server config
|
|
||||||
*/
|
|
||||||
private fun clearServer(): Boolean {
|
|
||||||
et_remarks.text = null
|
|
||||||
et_address.text = null
|
|
||||||
et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
|
|
||||||
et_id.text = null
|
|
||||||
sp_security?.setSelection(0)
|
|
||||||
sp_network?.setSelection(0)
|
|
||||||
|
|
||||||
sp_header_type?.setSelection(0)
|
|
||||||
et_request_host?.text = null
|
|
||||||
et_path?.text = null
|
|
||||||
sp_stream_security?.setSelection(0)
|
|
||||||
sp_allow_insecure?.setSelection(0)
|
|
||||||
et_sni?.text = null
|
|
||||||
|
|
||||||
//et_security.text = null
|
|
||||||
sp_flow?.setSelection(0)
|
|
||||||
et_public_key?.text = null
|
|
||||||
et_reserved1?.text = Utils.getEditable("0,0,0")
|
|
||||||
et_local_address?.text =
|
|
||||||
Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4)
|
|
||||||
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save server config
|
|
||||||
*/
|
|
||||||
private fun saveServer(): Boolean {
|
|
||||||
if (TextUtils.isEmpty(et_remarks.text.toString())) {
|
|
||||||
toast(R.string.server_lab_remarks)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(et_address.text.toString())) {
|
|
||||||
toast(R.string.server_lab_address)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (createConfigType != EConfigType.HYSTERIA2) {
|
|
||||||
if (Utils.parseInt(et_port.text.toString()) <= 0) {
|
|
||||||
toast(R.string.server_lab_port)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val config =
|
|
||||||
MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
|
|
||||||
if (config.configType != EConfigType.SOCKS
|
|
||||||
&& config.configType != EConfigType.HTTP
|
|
||||||
&& TextUtils.isEmpty(et_id.text.toString())
|
|
||||||
) {
|
|
||||||
if (config.configType == EConfigType.TROJAN
|
|
||||||
|| config.configType == EConfigType.SHADOWSOCKS
|
|
||||||
|| config.configType == EConfigType.HYSTERIA2
|
|
||||||
) {
|
|
||||||
toast(R.string.server_lab_id3)
|
|
||||||
} else {
|
|
||||||
toast(R.string.server_lab_id)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
sp_stream_security?.let {
|
|
||||||
if (config.configType == EConfigType.TROJAN && TextUtils.isEmpty(streamSecuritys[it.selectedItemPosition])) {
|
|
||||||
toast(R.string.server_lab_stream_security)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (et_extra?.text?.toString().isNotNullEmpty()) {
|
|
||||||
if (JsonUtil.parseString(et_extra?.text?.toString()) == null) {
|
|
||||||
toast(R.string.server_lab_xhttp_extra)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveCommon(config)
|
|
||||||
saveStreamSettings(config)
|
|
||||||
saveTls(config)
|
|
||||||
|
|
||||||
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
|
||||||
config.subscriptionId = subscriptionId.orEmpty()
|
|
||||||
}
|
|
||||||
//Log.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveCommon(config: ProfileItem) {
|
|
||||||
config.remarks = et_remarks.text.toString().trim()
|
|
||||||
config.server = et_address.text.toString().trim()
|
|
||||||
config.serverPort = et_port.text.toString().trim()
|
|
||||||
config.password = et_id.text.toString().trim()
|
|
||||||
|
|
||||||
if (config.configType == EConfigType.VMESS) {
|
|
||||||
config.method = securitys[sp_security?.selectedItemPosition ?: 0]
|
|
||||||
} else if (config.configType == EConfigType.VLESS) {
|
|
||||||
config.method = et_security?.text.toString().trim()
|
|
||||||
config.flow = flows[sp_flow?.selectedItemPosition ?: 0]
|
|
||||||
} else if (config.configType == EConfigType.SHADOWSOCKS) {
|
|
||||||
config.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
|
|
||||||
} else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
|
|
||||||
if (!TextUtils.isEmpty(et_security?.text) || !TextUtils.isEmpty(et_id.text)) {
|
|
||||||
config.username = et_security?.text.toString().trim()
|
|
||||||
}
|
|
||||||
} else if (config.configType == EConfigType.TROJAN) {
|
|
||||||
} else if (config.configType == EConfigType.WIREGUARD) {
|
|
||||||
config.secretKey = et_id.text.toString().trim()
|
|
||||||
config.publicKey = et_public_key?.text.toString().trim()
|
|
||||||
config.preSharedKey = et_preshared_key?.text.toString().trim()
|
|
||||||
config.reserved = et_reserved1?.text.toString().trim()
|
|
||||||
config.localAddress = et_local_address?.text.toString().trim()
|
|
||||||
config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
|
||||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
|
||||||
config.obfsPassword = et_obfs_password?.text?.toString()
|
|
||||||
config.portHopping = et_port_hop?.text?.toString()
|
|
||||||
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
|
|
||||||
config.pinSHA256 = et_pinsha256?.text?.toString()
|
|
||||||
config.bandwidthDown = et_bandwidth_down?.text?.toString()
|
|
||||||
config.bandwidthUp = et_bandwidth_up?.text?.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun saveStreamSettings(profileItem: ProfileItem) {
|
|
||||||
val network = sp_network?.selectedItemPosition ?: return
|
|
||||||
val type = sp_header_type?.selectedItemPosition ?: return
|
|
||||||
val requestHost = et_request_host?.text?.toString()?.trim() ?: return
|
|
||||||
val path = et_path?.text?.toString()?.trim() ?: return
|
|
||||||
|
|
||||||
profileItem.network = networks[network]
|
|
||||||
profileItem.headerType = transportTypes(networks[network])[type]
|
|
||||||
profileItem.host = requestHost
|
|
||||||
profileItem.path = path
|
|
||||||
profileItem.seed = path
|
|
||||||
profileItem.quicSecurity = requestHost
|
|
||||||
profileItem.quicKey = path
|
|
||||||
profileItem.mode = transportTypes(networks[network])[type]
|
|
||||||
profileItem.serviceName = path
|
|
||||||
profileItem.authority = requestHost
|
|
||||||
profileItem.xhttpMode = transportTypes(networks[network])[type]
|
|
||||||
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveTls(config: ProfileItem) {
|
|
||||||
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
|
||||||
val sniField = et_sni?.text?.toString()?.trim()
|
|
||||||
val allowInsecureField = sp_allow_insecure?.selectedItemPosition
|
|
||||||
val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: 0
|
|
||||||
val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: 0
|
|
||||||
val publicKey = et_public_key?.text?.toString()
|
|
||||||
val shortId = et_short_id?.text?.toString()
|
|
||||||
val spiderX = et_spider_x?.text?.toString()
|
|
||||||
|
|
||||||
val allowInsecure =
|
|
||||||
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
|
|
||||||
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE)
|
|
||||||
} else {
|
|
||||||
allowinsecures[allowInsecureField].toBoolean()
|
|
||||||
}
|
|
||||||
|
|
||||||
config.security = streamSecuritys[streamSecurity]
|
|
||||||
config.insecure = allowInsecure
|
|
||||||
config.sni = sniField
|
|
||||||
config.fingerPrint = uTlsItems[utlsIndex]
|
|
||||||
config.alpn = alpns[alpnIndex]
|
|
||||||
config.publicKey = publicKey
|
|
||||||
config.shortId = shortId
|
|
||||||
config.spiderX = spiderX
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun transportTypes(network: String?): Array<out String> {
|
|
||||||
return when (network) {
|
|
||||||
NetworkType.TCP.type -> {
|
|
||||||
tcpTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP.type -> {
|
|
||||||
kcpAndQuicTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.GRPC.type -> {
|
|
||||||
grpcModes
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.XHTTP.type -> {
|
|
||||||
xhttpMode
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
arrayOf("---")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete server config
|
|
||||||
*/
|
|
||||||
private fun deleteServer(): Boolean {
|
|
||||||
if (editGuid.isNotEmpty()) {
|
|
||||||
if (editGuid != MmkvManager.getSelectServer()) {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
MmkvManager.removeServer(editGuid)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
MmkvManager.removeServer(editGuid)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
application.toast(R.string.toast_action_not_allowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.action_server, menu)
|
|
||||||
val delButton = menu.findItem(R.id.del_config)
|
|
||||||
val saveButton = menu.findItem(R.id.save_config)
|
|
||||||
|
|
||||||
if (editGuid.isNotEmpty()) {
|
|
||||||
if (isRunning) {
|
|
||||||
delButton?.isVisible = false
|
|
||||||
saveButton?.isVisible = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delButton?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.del_config -> {
|
|
||||||
deleteServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.save_config -> {
|
|
||||||
saveServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,372 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.preference.CheckBoxPreference
|
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
|
||||||
import androidx.work.PeriodicWorkRequest
|
|
||||||
import androidx.work.multiprocess.RemoteWorkManager
|
|
||||||
import com.v2ray.ang.AngApplication
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.extension.toLongEx
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.service.SubscriptionUpdater
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import com.v2ray.ang.viewmodel.SettingsViewModel
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
|
||||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_settings)
|
|
||||||
|
|
||||||
title = getString(R.string.title_settings)
|
|
||||||
|
|
||||||
settingsViewModel.startListenPreferenceChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
|
||||||
|
|
||||||
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
|
||||||
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
|
||||||
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
|
||||||
private val appendHttpProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_APPEND_HTTP_PROXY) }
|
|
||||||
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
|
||||||
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
|
||||||
private val vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
|
|
||||||
private val vpnInterfaceAddress by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
|
|
||||||
|
|
||||||
private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
|
|
||||||
private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) }
|
|
||||||
private val muxXudpConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_XUDP_CONCURRENCY) }
|
|
||||||
private val muxXudpQuic by lazy { findPreference<ListPreference>(AppConfig.PREF_MUX_XUDP_QUIC) }
|
|
||||||
|
|
||||||
private val fragment by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FRAGMENT_ENABLED) }
|
|
||||||
private val fragmentPackets by lazy { findPreference<ListPreference>(AppConfig.PREF_FRAGMENT_PACKETS) }
|
|
||||||
private val fragmentLength by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_LENGTH) }
|
|
||||||
private val fragmentInterval by lazy { findPreference<EditTextPreference>(AppConfig.PREF_FRAGMENT_INTERVAL) }
|
|
||||||
|
|
||||||
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
|
|
||||||
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
|
||||||
|
|
||||||
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
|
||||||
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
|
||||||
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
|
|
||||||
private val dnsHosts by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DNS_HOSTS) }
|
|
||||||
private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
|
|
||||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
|
||||||
|
|
||||||
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_settings)
|
|
||||||
|
|
||||||
perAppProxy?.setOnPreferenceClickListener {
|
|
||||||
startActivity(Intent(activity, PerAppProxyActivity::class.java))
|
|
||||||
perAppProxy?.isChecked = true
|
|
||||||
false
|
|
||||||
}
|
|
||||||
localDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
updateLocalDns(any as Boolean)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
localDnsPort?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
val nval = any as String
|
|
||||||
localDnsPort?.summary =
|
|
||||||
if (TextUtils.isEmpty(nval)) AppConfig.PORT_LOCAL_DNS else nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
vpnDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
vpnDns?.summary = any as String
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
mux?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateMux(newValue as Boolean)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
muxConcurrency?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateMuxConcurrency(newValue as String)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
muxXudpConcurrency?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateMuxXudpConcurrency(newValue as String)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateFragment(newValue as Boolean)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
fragmentPackets?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateFragmentPackets(newValue as String)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
fragmentLength?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateFragmentLength(newValue as String)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
fragmentInterval?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateFragmentInterval(newValue as String)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdateCheck?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val value = newValue as Boolean
|
|
||||||
autoUpdateCheck?.isChecked = value
|
|
||||||
autoUpdateInterval?.isEnabled = value
|
|
||||||
autoUpdateInterval?.text?.toLongEx()?.let {
|
|
||||||
if (newValue) configureUpdateTask(it) else cancelUpdateTask()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
autoUpdateInterval?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
var nval = any as String
|
|
||||||
|
|
||||||
// It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
|
|
||||||
nval =
|
|
||||||
if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
|
|
||||||
autoUpdateInterval?.summary = nval
|
|
||||||
configureUpdateTask(nval.toLongEx())
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
socksPort?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
val nval = any as String
|
|
||||||
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
val nval = any as String
|
|
||||||
remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
domesticDns?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
val nval = any as String
|
|
||||||
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
dnsHosts?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
val nval = any as String
|
|
||||||
dnsHosts?.summary = nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
val nval = any as String
|
|
||||||
delayTestUrl?.summary = if (nval == "") AppConfig.DELAY_TEST_URL else nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
mode?.setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
updateMode(newValue.toString())
|
|
||||||
true
|
|
||||||
}
|
|
||||||
mode?.dialogLayoutResource = R.layout.preference_with_help_link
|
|
||||||
//loglevel.summary = "LogLevel"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
|
||||||
localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
|
||||||
fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
|
||||||
appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false)
|
|
||||||
localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
|
||||||
vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
|
||||||
|
|
||||||
updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false))
|
|
||||||
mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
|
||||||
muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")
|
|
||||||
muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
|
||||||
|
|
||||||
updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false))
|
|
||||||
fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)
|
|
||||||
fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
|
|
||||||
fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
|
||||||
fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
|
||||||
|
|
||||||
autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
|
||||||
autoUpdateInterval?.summary =
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
|
||||||
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
|
||||||
|
|
||||||
socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
|
||||||
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
|
||||||
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
|
||||||
dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
|
||||||
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL)
|
|
||||||
|
|
||||||
initSharedPreference()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSharedPreference() {
|
|
||||||
listOf(
|
|
||||||
localDnsPort,
|
|
||||||
vpnDns,
|
|
||||||
muxConcurrency,
|
|
||||||
muxXudpConcurrency,
|
|
||||||
fragmentLength,
|
|
||||||
fragmentInterval,
|
|
||||||
autoUpdateInterval,
|
|
||||||
socksPort,
|
|
||||||
remoteDns,
|
|
||||||
domesticDns,
|
|
||||||
delayTestUrl
|
|
||||||
).forEach { key ->
|
|
||||||
key?.text = key?.summary.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
listOf(
|
|
||||||
AppConfig.PREF_SNIFFING_ENABLED,
|
|
||||||
).forEach { key ->
|
|
||||||
findPreference<CheckBoxPreference>(key)?.isChecked =
|
|
||||||
MmkvManager.decodeSettingsBool(key, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
listOf(
|
|
||||||
AppConfig.PREF_ROUTE_ONLY_ENABLED,
|
|
||||||
AppConfig.PREF_IS_BOOTED,
|
|
||||||
AppConfig.PREF_BYPASS_APPS,
|
|
||||||
AppConfig.PREF_SPEED_ENABLED,
|
|
||||||
AppConfig.PREF_CONFIRM_REMOVE,
|
|
||||||
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
|
||||||
AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
|
|
||||||
AppConfig.PREF_PREFER_IPV6,
|
|
||||||
AppConfig.PREF_PROXY_SHARING,
|
|
||||||
AppConfig.PREF_ALLOW_INSECURE
|
|
||||||
).forEach { key ->
|
|
||||||
findPreference<CheckBoxPreference>(key)?.isChecked =
|
|
||||||
MmkvManager.decodeSettingsBool(key, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
listOf(
|
|
||||||
AppConfig.PREF_VPN_BYPASS_LAN,
|
|
||||||
AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
|
|
||||||
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
|
||||||
AppConfig.PREF_MUX_XUDP_QUIC,
|
|
||||||
AppConfig.PREF_FRAGMENT_PACKETS,
|
|
||||||
AppConfig.PREF_LANGUAGE,
|
|
||||||
AppConfig.PREF_UI_MODE_NIGHT,
|
|
||||||
AppConfig.PREF_LOGLEVEL,
|
|
||||||
AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
|
|
||||||
AppConfig.PREF_MODE
|
|
||||||
).forEach { key ->
|
|
||||||
if (MmkvManager.decodeSettingsString(key) != null) {
|
|
||||||
findPreference<ListPreference>(key)?.value = MmkvManager.decodeSettingsString(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateMode(mode: String?) {
|
|
||||||
val vpn = mode == VPN
|
|
||||||
perAppProxy?.isEnabled = vpn
|
|
||||||
perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
|
|
||||||
localDns?.isEnabled = vpn
|
|
||||||
fakeDns?.isEnabled = vpn
|
|
||||||
appendHttpProxy?.isEnabled = vpn
|
|
||||||
localDnsPort?.isEnabled = vpn
|
|
||||||
vpnDns?.isEnabled = vpn
|
|
||||||
vpnBypassLan?.isEnabled = vpn
|
|
||||||
vpnInterfaceAddress?.isEnabled = vpn
|
|
||||||
if (vpn) {
|
|
||||||
updateLocalDns(
|
|
||||||
MmkvManager.decodeSettingsBool(
|
|
||||||
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateLocalDns(enabled: Boolean) {
|
|
||||||
fakeDns?.isEnabled = enabled
|
|
||||||
localDnsPort?.isEnabled = enabled
|
|
||||||
vpnDns?.isEnabled = !enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureUpdateTask(interval: Long) {
|
|
||||||
val rw = RemoteWorkManager.getInstance(AngApplication.application)
|
|
||||||
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
|
|
||||||
rw.enqueueUniquePeriodicWork(
|
|
||||||
AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME,
|
|
||||||
ExistingPeriodicWorkPolicy.UPDATE,
|
|
||||||
PeriodicWorkRequest.Builder(
|
|
||||||
SubscriptionUpdater.UpdateTask::class.java,
|
|
||||||
interval,
|
|
||||||
TimeUnit.MINUTES
|
|
||||||
)
|
|
||||||
.apply {
|
|
||||||
setInitialDelay(interval, TimeUnit.MINUTES)
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelUpdateTask() {
|
|
||||||
val rw = RemoteWorkManager.getInstance(AngApplication.application)
|
|
||||||
rw.cancelUniqueWork(AppConfig.SUBSCRIPTION_UPDATE_TASK_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateMux(enabled: Boolean) {
|
|
||||||
muxConcurrency?.isEnabled = enabled
|
|
||||||
muxXudpConcurrency?.isEnabled = enabled
|
|
||||||
muxXudpQuic?.isEnabled = enabled
|
|
||||||
if (enabled) {
|
|
||||||
updateMuxConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
|
|
||||||
updateMuxXudpConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateMuxConcurrency(value: String?) {
|
|
||||||
val concurrency = value?.toIntOrNull() ?: 8
|
|
||||||
muxConcurrency?.summary = concurrency.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun updateMuxXudpConcurrency(value: String?) {
|
|
||||||
if (value == null) {
|
|
||||||
muxXudpQuic?.isEnabled = true
|
|
||||||
} else {
|
|
||||||
val concurrency = value.toIntOrNull() ?: 8
|
|
||||||
muxXudpConcurrency?.summary = concurrency.toString()
|
|
||||||
muxXudpQuic?.isEnabled = concurrency >= 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateFragment(enabled: Boolean) {
|
|
||||||
fragmentPackets?.isEnabled = enabled
|
|
||||||
fragmentLength?.isEnabled = enabled
|
|
||||||
fragmentInterval?.isEnabled = enabled
|
|
||||||
if (enabled) {
|
|
||||||
updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
|
|
||||||
updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
|
|
||||||
updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateFragmentPackets(value: String?) {
|
|
||||||
fragmentPackets?.summary = value.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateFragmentLength(value: String?) {
|
|
||||||
fragmentLength?.summary = value.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateFragmentInterval(value: String?) {
|
|
||||||
fragmentInterval?.summary = value.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onModeHelpClicked(view: View) {
|
|
||||||
Utils.openUri(this, AppConfig.APP_WIKI_MODE)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class SubSettingActivity : BaseActivity() {
|
|
||||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
var subscriptions: List<Pair<String, SubscriptionItem>> = listOf()
|
|
||||||
private val adapter by lazy { SubSettingRecyclerAdapter(this) }
|
|
||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.title_sub_setting)
|
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
|
||||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.action_sub_setting, menu)
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.add_config -> {
|
|
||||||
startActivity(Intent(this, SubEditActivity::class.java))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.sub_update -> {
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val count = AngConfigManager.updateConfigViaSubAll()
|
|
||||||
delay(500L)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (count > 0) {
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
fun refreshData() {
|
|
||||||
subscriptions = MmkvManager.decodeSubscriptions()
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.net.URLDecoder
|
|
||||||
|
|
||||||
class UrlSchemeActivity : BaseActivity() {
|
|
||||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
try {
|
|
||||||
intent.apply {
|
|
||||||
if (action == Intent.ACTION_SEND) {
|
|
||||||
if ("text/plain" == type) {
|
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
|
||||||
parseUri(it, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (action == Intent.ACTION_VIEW) {
|
|
||||||
when (data?.host) {
|
|
||||||
"install-config" -> {
|
|
||||||
val uri: Uri? = intent.data
|
|
||||||
val shareUrl = uri?.getQueryParameter("url").orEmpty()
|
|
||||||
parseUri(shareUrl, uri?.fragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
"install-sub" -> {
|
|
||||||
val uri: Uri? = intent.data
|
|
||||||
val shareUrl = uri?.getQueryParameter("url").orEmpty()
|
|
||||||
parseUri(shareUrl, uri?.fragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
|
||||||
finish()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Error processing URL scheme", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseUri(uriString: String?, fragment: String?) {
|
|
||||||
if (uriString.isNullOrEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.i(AppConfig.TAG, uriString)
|
|
||||||
|
|
||||||
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
|
|
||||||
val uri = Uri.parse(decodedUrl)
|
|
||||||
if (uri != null) {
|
|
||||||
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
|
|
||||||
decodedUrl += "#${fragment}"
|
|
||||||
}
|
|
||||||
Log.i(AppConfig.TAG, decodedUrl)
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (count + countSub > 0) {
|
|
||||||
toast(R.string.import_subscription_success)
|
|
||||||
} else {
|
|
||||||
toast(R.string.import_subscription_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,376 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View.GONE
|
|
||||||
import android.view.View.VISIBLE
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
|
||||||
import com.v2ray.ang.extension.concatUrl
|
|
||||||
import com.v2ray.ang.extension.toTrafficString
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
|
||||||
import com.v2ray.ang.util.HttpUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class UserAssetActivity : BaseActivity() {
|
|
||||||
private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
|
||||||
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
|
||||||
|
|
||||||
private val requestStoragePermissionLauncher = registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { isGranted: Boolean ->
|
|
||||||
if (isGranted) {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
intent.type = "*/*"
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
|
|
||||||
try {
|
|
||||||
chooseFile.launch(
|
|
||||||
Intent.createChooser(
|
|
||||||
intent,
|
|
||||||
getString(R.string.title_file_chooser)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (ex: android.content.ActivityNotFoundException) {
|
|
||||||
toast(R.string.toast_require_file_manager)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val requestCameraPermissionLauncher = registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { isGranted: Boolean ->
|
|
||||||
if (isGranted) {
|
|
||||||
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
title = getString(R.string.title_user_asset_setting)
|
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
|
||||||
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
|
||||||
binding.recyclerView.adapter = UserAssetAdapter()
|
|
||||||
|
|
||||||
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
|
|
||||||
binding.layoutGeoFilesSources.setOnClickListener {
|
|
||||||
setGeoFilesSources()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.menu_asset, menu)
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use when to streamline the option selection
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
|
||||||
R.id.add_file -> showFileChooser().let { true }
|
|
||||||
R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
|
|
||||||
R.id.add_qrcode -> importAssetFromQRcode().let { true }
|
|
||||||
R.id.download_file -> downloadGeoFiles().let { true }
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGeoFilesSources(): String {
|
|
||||||
return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setGeoFilesSources() {
|
|
||||||
AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i ->
|
|
||||||
try {
|
|
||||||
val value = AppConfig.GEO_FILES_SOURCES[i]
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
|
|
||||||
binding.tvGeoFilesSourcesSummary.text = value
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to set geo files sources", e)
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showFileChooser() {
|
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
|
||||||
} else {
|
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
}
|
|
||||||
requestStoragePermissionLauncher.launch(permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
val uri = result.data?.data
|
|
||||||
if (result.resultCode == RESULT_OK && uri != null) {
|
|
||||||
val assetId = Utils.getUuid()
|
|
||||||
runCatching {
|
|
||||||
val assetItem = AssetUrlItem(
|
|
||||||
getCursorName(uri) ?: uri.toString(),
|
|
||||||
"file"
|
|
||||||
)
|
|
||||||
|
|
||||||
val assetList = MmkvManager.decodeAssetUrls()
|
|
||||||
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
|
|
||||||
toast(R.string.msg_remark_is_duplicate)
|
|
||||||
} else {
|
|
||||||
MmkvManager.encodeAsset(assetId, assetItem)
|
|
||||||
copyFile(uri)
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
toastError(R.string.toast_asset_copy_failed)
|
|
||||||
MmkvManager.removeAssetUrl(assetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyFile(uri: Uri): String {
|
|
||||||
val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
|
|
||||||
contentResolver.openInputStream(uri).use { inputStream ->
|
|
||||||
targetFile.outputStream().use { fileOut ->
|
|
||||||
inputStream?.copyTo(fileOut)
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return targetFile.path
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCursorName(uri: Uri): String? = try {
|
|
||||||
contentResolver.query(uri, null, null, null, null)?.let { cursor ->
|
|
||||||
cursor.run {
|
|
||||||
if (moveToFirst()) getString(getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
|
||||||
else null
|
|
||||||
}.also { cursor.close() }
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to get cursor name", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importAssetFromQRcode(): Boolean {
|
|
||||||
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scanQRCodeForAssetURL = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
importAsset(it.data?.getStringExtra("SCAN_RESULT"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importAsset(url: String?): Boolean {
|
|
||||||
try {
|
|
||||||
if (!Utils.isValidUrl(url)) {
|
|
||||||
toast(R.string.toast_invalid_url)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Send URL to UserAssetUrlActivity for Processing
|
|
||||||
startActivity(
|
|
||||||
Intent(this, UserAssetUrlActivity::class.java)
|
|
||||||
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to import asset from URL", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadGeoFiles() {
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
toast(R.string.msg_downloading_content)
|
|
||||||
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
|
||||||
assets = addBuiltInGeoItems(assets)
|
|
||||||
|
|
||||||
var resultCount = 0
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
assets.forEach {
|
|
||||||
try {
|
|
||||||
var result = downloadGeo(it.second, 15000, httpPort)
|
|
||||||
if (!result) {
|
|
||||||
result = downloadGeo(it.second, 15000, 0)
|
|
||||||
}
|
|
||||||
if (result)
|
|
||||||
resultCount++
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (resultCount > 0) {
|
|
||||||
toast(getString(R.string.title_update_config_count, resultCount))
|
|
||||||
refreshData()
|
|
||||||
} else {
|
|
||||||
toast(getString(R.string.toast_failure))
|
|
||||||
}
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
|
||||||
val targetTemp = File(extDir, item.remarks + "_temp")
|
|
||||||
val target = File(extDir, item.remarks)
|
|
||||||
Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
|
|
||||||
|
|
||||||
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
|
|
||||||
try {
|
|
||||||
val inputStream = conn.inputStream
|
|
||||||
val responseCode = conn.responseCode
|
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
|
||||||
FileOutputStream(targetTemp).use { output ->
|
|
||||||
inputStream.copyTo(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetTemp.renameTo(target)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
conn.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
|
|
||||||
val list = mutableListOf<Pair<String, AssetUrlItem>>()
|
|
||||||
builtInGeoFiles
|
|
||||||
.filter { geoFile -> assets.none { it.second.remarks == geoFile } }
|
|
||||||
.forEach {
|
|
||||||
list.add(
|
|
||||||
Utils.getUuid() to AssetUrlItem(
|
|
||||||
it,
|
|
||||||
String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
|
|
||||||
locked = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return list + assets
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initAssets() {
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
fun refreshData() {
|
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
|
||||||
return UserAssetViewHolder(
|
|
||||||
ItemRecyclerUserAssetBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
|
||||||
assets = addBuiltInGeoItems(assets)
|
|
||||||
val item = assets.getOrNull(position) ?: return
|
|
||||||
// file with name == item.second.remarks
|
|
||||||
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
|
||||||
|
|
||||||
holder.itemUserAssetBinding.assetName.text = item.second.remarks
|
|
||||||
|
|
||||||
if (file != null) {
|
|
||||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
|
||||||
holder.itemUserAssetBinding.assetProperties.text =
|
|
||||||
"${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
|
|
||||||
} else {
|
|
||||||
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.second.locked == true) {
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
|
||||||
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
|
||||||
} else {
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
|
|
||||||
//holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
|
|
||||||
val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java)
|
|
||||||
intent.putExtra("assetId", item.first)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
|
||||||
AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
file?.delete()
|
|
||||||
MmkvManager.removeAssetUrl(item.first)
|
|
||||||
initAssets()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
|
||||||
assets = addBuiltInGeoItems(assets)
|
|
||||||
return assets.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
|
||||||
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
|
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class UserAssetUrlActivity : BaseActivity() {
|
|
||||||
// Receive QRcode URL from UserAssetActivity
|
|
||||||
companion object {
|
|
||||||
const val ASSET_URL_QRCODE = "ASSET_URL_QRCODE"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
private var del_config: MenuItem? = null
|
|
||||||
private var save_config: MenuItem? = null
|
|
||||||
|
|
||||||
private val extDir by lazy { File(Utils.userAssetPath(this)) }
|
|
||||||
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
title = getString(R.string.title_user_asset_add_url)
|
|
||||||
|
|
||||||
val assetItem = MmkvManager.decodeAsset(editAssetId)
|
|
||||||
val assetUrlQrcode = intent.getStringExtra(ASSET_URL_QRCODE)
|
|
||||||
val assetNameQrcode = File(assetUrlQrcode.toString()).name
|
|
||||||
when {
|
|
||||||
assetItem != null -> bindingAsset(assetItem)
|
|
||||||
assetUrlQrcode != null -> {
|
|
||||||
binding.etRemarks.setText(assetNameQrcode)
|
|
||||||
binding.etUrl.setText(assetUrlQrcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> clearAsset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* bingding seleced asset config
|
|
||||||
*/
|
|
||||||
private fun bindingAsset(assetItem: AssetUrlItem): Boolean {
|
|
||||||
binding.etRemarks.text = Utils.getEditable(assetItem.remarks)
|
|
||||||
binding.etUrl.text = Utils.getEditable(assetItem.url)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear or init asset config
|
|
||||||
*/
|
|
||||||
private fun clearAsset(): Boolean {
|
|
||||||
binding.etRemarks.text = null
|
|
||||||
binding.etUrl.text = null
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save asset config
|
|
||||||
*/
|
|
||||||
private fun saveServer(): Boolean {
|
|
||||||
var assetItem = MmkvManager.decodeAsset(editAssetId)
|
|
||||||
var assetId = editAssetId
|
|
||||||
if (assetItem != null) {
|
|
||||||
// remove file associated with the asset
|
|
||||||
val file = extDir.resolve(assetItem.remarks)
|
|
||||||
if (file.exists()) {
|
|
||||||
try {
|
|
||||||
file.delete()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assetId = Utils.getUuid()
|
|
||||||
assetItem = AssetUrlItem()
|
|
||||||
}
|
|
||||||
|
|
||||||
assetItem.remarks = binding.etRemarks.text.toString()
|
|
||||||
assetItem.url = binding.etUrl.text.toString()
|
|
||||||
|
|
||||||
// check remarks unique
|
|
||||||
val assetList = MmkvManager.decodeAssetUrls()
|
|
||||||
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
|
|
||||||
toast(R.string.msg_remark_is_duplicate)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(assetItem.remarks)) {
|
|
||||||
toast(R.string.sub_setting_remarks)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (TextUtils.isEmpty(assetItem.url)) {
|
|
||||||
toast(R.string.title_url)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
MmkvManager.encodeAsset(assetId, assetItem)
|
|
||||||
toastSuccess(R.string.toast_success)
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save server config
|
|
||||||
*/
|
|
||||||
private fun deleteServer(): Boolean {
|
|
||||||
if (editAssetId.isNotEmpty()) {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
MmkvManager.removeAssetUrl(editAssetId)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.action_server, menu)
|
|
||||||
del_config = menu.findItem(R.id.del_config)
|
|
||||||
save_config = menu.findItem(R.id.save_config)
|
|
||||||
|
|
||||||
if (editAssetId.isEmpty()) {
|
|
||||||
del_config?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.del_config -> {
|
|
||||||
deleteServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.save_config -> {
|
|
||||||
saveServer()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue