Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf5d49a3d | ||
|
|
4fffb17283 | ||
|
|
83b8bdfdf4 | ||
|
|
cc1538a24d | ||
|
|
eb19199d18 | ||
|
|
441e5ef8d5 | ||
|
|
d768774aad | ||
|
|
c3d83907a5 | ||
|
|
b52a98ae5e | ||
|
|
a70e4089e3 | ||
|
|
397989769c | ||
|
|
796676abdc | ||
|
|
3c93ccb86c | ||
|
|
fc8c74184a | ||
|
|
1939e6b5cf | ||
|
|
1c2ac9385d | ||
|
|
7513f1fe07 | ||
|
|
63e2c02daa | ||
|
|
fec76385e1 | ||
|
|
bf21428746 | ||
|
|
01ab8dad17 | ||
|
|
8974f73841 | ||
|
|
d3a2a2413c | ||
|
|
9b52863270 | ||
|
|
733914a7a7 | ||
|
|
7b0ab1ea4d | ||
|
|
51ff95f071 | ||
|
|
fc7aca46ce | ||
|
|
78e34caff3 | ||
|
|
d2aecd4dee | ||
|
|
9be1083495 | ||
|
|
38eb9ee13f | ||
|
|
15b36dfc57 | ||
|
|
40a83f7cac | ||
|
|
fd9f912c18 | ||
|
|
eef6e60dbb | ||
|
|
f0de5275b9 | ||
|
|
4eb5c0263c | ||
|
|
7a0d997a81 | ||
|
|
17b7c6d357 | ||
|
|
206f2cb306 | ||
|
|
1bc433097b | ||
|
|
9bee5fbe99 | ||
|
|
e819798d80 | ||
|
|
14ff9eb527 | ||
|
|
a60f45ce31 | ||
|
|
0b9a96209f | ||
|
|
3747e58e4e | ||
|
|
28b1788dc1 | ||
|
|
12ab2954b0 | ||
|
|
d0e8937f03 | ||
|
|
1a7ab97a3a | ||
|
|
ef4145787b | ||
|
|
899e4c1b14 | ||
|
|
faa4385087 | ||
|
|
172d9fd093 | ||
|
|
093716baaa | ||
|
|
0165ad54b3 | ||
|
|
b52dd33102 | ||
|
|
00aed90f2f | ||
|
|
5f3d7c0213 | ||
|
|
401f051774 | ||
|
|
e14b48f3eb | ||
|
|
1972f83b86 | ||
|
|
55a11bbeee | ||
|
|
ca32ee3a0e | ||
|
|
159a370286 | ||
|
|
d1904d52d9 | ||
|
|
5e993fd91b | ||
|
|
bada7c93d7 | ||
|
|
38850597f3 | ||
|
|
da347492d3 | ||
|
|
2ec5d8db3c | ||
|
|
fd9c5040bf | ||
|
|
aa328f0add | ||
|
|
9743d7b87b | ||
|
|
98fb0c433e | ||
|
|
7c9fcd9f43 | ||
|
|
54c76d9968 | ||
|
|
40b3f0fedc | ||
|
|
dcfcf83430 | ||
|
|
e46b354643 | ||
|
|
f497e4e301 | ||
|
|
b65e4b3819 | ||
|
|
d166b036fc | ||
|
|
ddf5f22037 | ||
|
|
7d8a9f2b6d | ||
|
|
0a1695e3d7 | ||
|
|
4a653d4935 | ||
|
|
2bc31a10c5 | ||
|
|
e8d2c6214b | ||
|
|
3a0f2687e9 | ||
|
|
04c98326b2 | ||
|
|
eb22c7f303 | ||
|
|
d51a4d7a7e | ||
|
|
0fb705e1e2 | ||
|
|
10b849ef09 | ||
|
|
d7d3b23cea | ||
|
|
c3786d434e | ||
|
|
9e3b92014a | ||
|
|
f4e088131b | ||
|
|
e55e069fe3 | ||
|
|
d8d3767798 | ||
|
|
7e99b1ac78 | ||
|
|
6ff3a73bf2 | ||
|
|
2a43b52344 | ||
|
|
abff80ec23 | ||
|
|
a4edf86195 | ||
|
|
0d0da6bfec | ||
|
|
e0c8ece9b5 | ||
|
|
4d875bc3d4 | ||
|
|
3a6e23bcef | ||
|
|
efd0716707 | ||
|
|
c94a5fb743 | ||
|
|
047011f60b | ||
|
|
a54ed3a51a | ||
|
|
c37f09bfcd | ||
|
|
1c7042463d | ||
|
|
dcb003f9ab | ||
|
|
7dbda3cee7 | ||
|
|
26bee229a1 | ||
|
|
5bf2beb179 | ||
|
|
4a5c551678 | ||
|
|
277894215d | ||
|
|
684e08a3a1 | ||
|
|
19dbc2f9b9 | ||
|
|
833a1e06f0 | ||
|
|
daca0831a4 | ||
|
|
337889c5f1 | ||
|
|
244d2d3866 | ||
|
|
c0fed0ba4f | ||
|
|
affb107b9d | ||
|
|
f96073af99 | ||
|
|
496a0483d2 | ||
|
|
e11dca00bb | ||
|
|
fde39bf34e | ||
|
|
4f11bae238 | ||
|
|
f6282ba71f | ||
|
|
3edf1f4e1b | ||
|
|
41bc064083 | ||
|
|
eb8562e6b0 | ||
|
|
68fbdd92c3 | ||
|
|
02038a5d93 | ||
|
|
4fb8c2f4b2 | ||
|
|
7afffa60c3 | ||
|
|
0e6c860360 | ||
|
|
ebfbbfa08b | ||
|
|
b5f182dfec | ||
|
|
4cf28d0ad0 | ||
|
|
149bb049a5 | ||
|
|
124702f0a2 | ||
|
|
c1cebe578b | ||
|
|
e46c1ee849 | ||
|
|
70f1743114 | ||
|
|
54d520727e | ||
|
|
1f1e4db486 | ||
|
|
e536236634 | ||
|
|
140c236da5 | ||
|
|
69ede34274 | ||
|
|
fcf6e22132 |
172
.github/workflows/build.yml
vendored
172
.github/workflows/build.yml
vendored
@@ -1,13 +1,14 @@
|
|||||||
name: Build APK
|
name: Build APK
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
XRAY_CORE_VERSION:
|
release_tag:
|
||||||
description: 'Xray core version or commit hash'
|
|
||||||
required: false
|
required: false
|
||||||
|
type: string
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -15,79 +16,144 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4.2.2
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
fetch-depth: '0'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3.2.0
|
||||||
|
with:
|
||||||
|
log-accepted-android-sdk-licenses: false
|
||||||
|
cmdline-tools-version: '12266719'
|
||||||
|
packages: 'platforms;android-35 build-tools;35.0.0 platform-tools'
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: |
|
||||||
|
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
|
||||||
|
--channel=3 \
|
||||||
|
--install "ndk;29.0.13113456"
|
||||||
|
echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.13113456" >> $GITHUB_ENV
|
||||||
|
sed -i '10i\
|
||||||
|
\
|
||||||
|
ndkVersion = "29.0.13113456"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
||||||
|
|
||||||
|
- name: Restore cached libtun2socks
|
||||||
|
id: cache-libtun2socks-restore
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/libs
|
||||||
|
key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
|
||||||
|
|
||||||
|
- name: Build libtun2socks
|
||||||
|
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
bash compile-tun2socks.sh
|
||||||
|
|
||||||
|
- name: Save libtun2socks
|
||||||
|
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/libs
|
||||||
|
key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
|
||||||
|
|
||||||
|
- name: Copy libtun2socks
|
||||||
|
run: |
|
||||||
|
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
|
||||||
|
|
||||||
|
- name: Fetch AndroidLibXrayLite tag
|
||||||
|
run: |
|
||||||
|
pushd AndroidLibXrayLite
|
||||||
|
CURRENT_TAG=$(git describe --tags --abbrev=0)
|
||||||
|
echo "Current tag in this repo: $CURRENT_TAG"
|
||||||
|
echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Download libv2ray
|
||||||
|
uses: robinraju/release-downloader@v1.12
|
||||||
|
with:
|
||||||
|
repository: '2dust/AndroidLibXrayLite'
|
||||||
|
tag: ${{ env.CURRENT_TAG }}
|
||||||
|
fileName: 'libv2ray.aar'
|
||||||
|
out-file-path: V2rayNG/app/libs/
|
||||||
|
|
||||||
|
- name: Restore cached libhysteria2
|
||||||
|
id: cache-libhysteria2-restore
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/hysteria/libs
|
||||||
|
key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
||||||
|
|
||||||
|
- name: Setup Golang
|
||||||
|
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/setup-go@v5.4.0
|
||||||
|
with:
|
||||||
|
go-version-file: 'AndroidLibXrayLite/go.mod'
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Build libhysteria2
|
||||||
|
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
bash libhysteria2.sh
|
||||||
|
|
||||||
|
- name: Save libhysteria2
|
||||||
|
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/hysteria/libs
|
||||||
|
key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
||||||
|
|
||||||
|
- name: Copy libhysteria2
|
||||||
|
run: |
|
||||||
|
cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Setup Golang
|
- name: Decode Keystore
|
||||||
uses: actions/setup-go@v5
|
uses: timheuer/base64-to-file@v1.2.4
|
||||||
|
id: android_keystore
|
||||||
with:
|
with:
|
||||||
go-version: '1.23.2'
|
fileName: "android_keystore.jks"
|
||||||
cache: false
|
encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }}
|
||||||
|
|
||||||
- name: Patch Go use 600296
|
|
||||||
#https://go-review.googlesource.com/c/go/+/600296
|
|
||||||
run: |
|
|
||||||
cd "$(go env GOROOT)"
|
|
||||||
curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1
|
|
||||||
|
|
||||||
- name: Install gomobile
|
|
||||||
run: |
|
|
||||||
go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240806205939-81131f6468ab
|
|
||||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Setup Android environment
|
|
||||||
uses: android-actions/setup-android@v3
|
|
||||||
|
|
||||||
- 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 21 -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 755 gradlew
|
chmod 755 gradlew
|
||||||
./gradlew assembleDebug
|
./gradlew licenseFdroidReleaseReport
|
||||||
|
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Upload arm64-v8a APK
|
- name: Upload arm64-v8a APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4.6.2
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: arm64-v8a
|
name: arm64-v8a
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
|
||||||
|
|
||||||
- name: Upload armeabi-v7a APK
|
- name: Upload armeabi-v7a APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4.6.2
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: armeabi-v7a
|
name: armeabi-v7a
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
|
||||||
|
|
||||||
- name: Upload x86 APK
|
- name: Upload x86 APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4.6.2
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: x86-apk
|
name: x86-apk
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*x86*.apk
|
||||||
|
|
||||||
- name: Upload Other APKs
|
- name: Upload to release
|
||||||
uses: actions/upload-artifact@v4
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
if: github.event.inputs.release_tag != ''
|
||||||
with:
|
with:
|
||||||
name: others-apk
|
file: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*playstore*/release/*.apk
|
||||||
path: |
|
tag: ${{ github.event.inputs.release_tag }}
|
||||||
${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug
|
file_glob: true
|
||||||
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
|
prerelease: true
|
||||||
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
|
|
||||||
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
|
|
||||||
|
|||||||
16
.github/workflows/fastlane.yml
vendored
Normal file
16
.github/workflows/fastlane.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: Validate Fastlane metadata
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Validate Fastlane Supply Metadata
|
||||||
|
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0
|
||||||
61
.gitignore
vendored
61
.gitignore
vendored
@@ -1,5 +1,66 @@
|
|||||||
|
# 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
Normal file
12
.gitmodules
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[submodule "hysteria"]
|
||||||
|
path = hysteria
|
||||||
|
url = https://github.com/apernet/hysteria
|
||||||
|
[submodule "AndroidLibXrayLite"]
|
||||||
|
path = AndroidLibXrayLite
|
||||||
|
url = https://github.com/2dust/AndroidLibXrayLite
|
||||||
|
[submodule "badvpn"]
|
||||||
|
path = badvpn
|
||||||
|
url = https://github.com/XTLS/badvpn
|
||||||
|
[submodule "libancillary"]
|
||||||
|
path = libancillary
|
||||||
|
url = https://github.com/shadowsocks/libancillary
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 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
AndroidLibXrayLite
Submodule
1
AndroidLibXrayLite
Submodule
Submodule AndroidLibXrayLite added at c8a6ca7c5e
@@ -3,7 +3,7 @@
|
|||||||
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)
|
||||||
|
|||||||
10
V2rayNG/.gitignore
vendored
10
V2rayNG/.gitignore
vendored
@@ -1,10 +0,0 @@
|
|||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
*.apk
|
|
||||||
signing.properties
|
|
||||||
*.aar
|
|
||||||
2
V2rayNG/app/.gitignore
vendored
2
V2rayNG/app/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
/build
|
|
||||||
/google-services.json
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
id("com.google.android.gms.oss-licenses-plugin")
|
id("com.jaredsburrows.license")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -12,20 +12,26 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 621
|
versionCode = 643
|
||||||
versionName = "1.9.25"
|
versionName = "1.9.43"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
isEnable = true
|
isEnable = true
|
||||||
include(
|
reset()
|
||||||
"arm64-v8a",
|
if (abiFilterList != null && abiFilterList.isNotEmpty()) {
|
||||||
"armeabi-v7a",
|
include(*abiFilterList.toTypedArray())
|
||||||
"x86_64",
|
} else {
|
||||||
"x86"
|
include(
|
||||||
)
|
"arm64-v8a",
|
||||||
isUniversalApk = true
|
"armeabi-v7a",
|
||||||
|
"x86_64",
|
||||||
|
"x86"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isUniversalApk = abiFilterList.isNullOrEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +48,19 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
sourceSets {
|
||||||
getByName("main") {
|
getByName("main") {
|
||||||
jniLibs.srcDirs("libs")
|
jniLibs.srcDirs("libs")
|
||||||
@@ -50,34 +69,56 @@ android {
|
|||||||
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
isCoreLibraryDesugaringEnabled = true
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all {
|
applicationVariants.all {
|
||||||
val variant = this
|
val variant = this
|
||||||
val versionCodes =
|
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
|
||||||
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
|
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
|
variant.outputs
|
||||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||||
.forEach { output ->
|
.forEach { output ->
|
||||||
val abi = if (output.getFilter("ABI") != null)
|
val abi = output.getFilter("ABI") ?: "universal"
|
||||||
output.getFilter("ABI")
|
output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
|
||||||
else
|
if (versionCodes.containsKey(abi)) {
|
||||||
"universal"
|
output.versionCodeOverride =
|
||||||
|
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
} else {
|
||||||
if (versionCodes.containsKey(abi)) {
|
return@forEach
|
||||||
output.versionCodeOverride =
|
}
|
||||||
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
|
||||||
} 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 {
|
buildFeatures {
|
||||||
@@ -104,10 +145,11 @@ dependencies {
|
|||||||
implementation(libs.androidx.constraintlayout)
|
implementation(libs.androidx.constraintlayout)
|
||||||
implementation(libs.preference.ktx)
|
implementation(libs.preference.ktx)
|
||||||
implementation(libs.recyclerview)
|
implementation(libs.recyclerview)
|
||||||
|
implementation(libs.androidx.swiperefreshlayout)
|
||||||
|
|
||||||
// UI Libraries
|
// UI Libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.toastcompat)
|
implementation(libs.toasty)
|
||||||
implementation(libs.editorkit)
|
implementation(libs.editorkit)
|
||||||
implementation(libs.flexbox)
|
implementation(libs.flexbox)
|
||||||
|
|
||||||
@@ -116,16 +158,15 @@ dependencies {
|
|||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
|
|
||||||
// Reactive and Utility Libraries
|
// Reactive and Utility Libraries
|
||||||
implementation(libs.rxjava)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.rxandroid)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.rxpermissions)
|
|
||||||
|
|
||||||
// Language and Processing Libraries
|
// Language and Processing Libraries
|
||||||
implementation(libs.language.base)
|
implementation(libs.language.base)
|
||||||
implementation(libs.language.json)
|
implementation(libs.language.json)
|
||||||
|
|
||||||
// Intent and Utility Libraries
|
// Intent and Utility Libraries
|
||||||
implementation(libs.quickie.bundled)
|
implementation(libs.quickie.foss)
|
||||||
implementation(libs.core)
|
implementation(libs.core)
|
||||||
|
|
||||||
// AndroidX Lifecycle and Architecture Components
|
// AndroidX Lifecycle and Architecture Components
|
||||||
@@ -146,6 +187,5 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
testImplementation(libs.org.mockito.mockito.inline)
|
testImplementation(libs.org.mockito.mockito.inline)
|
||||||
testImplementation(libs.mockito.kotlin)
|
testImplementation(libs.mockito.kotlin)
|
||||||
// Oss Licenses
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
implementation(libs.play.services.oss.licenses)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -35,7 +35,6 @@
|
|||||||
<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
|
||||||
@@ -52,6 +51,7 @@
|
|||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppThemeDayNight"
|
android:theme="@style/AppThemeDayNight"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
@@ -212,7 +212,8 @@
|
|||||||
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: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>
|
||||||
|
|||||||
@@ -99,10 +99,5 @@
|
|||||||
"domain": [
|
"domain": [
|
||||||
"geosite:cn"
|
"geosite:cn"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "最终代理",
|
|
||||||
"port": "0-65535",
|
|
||||||
"outboundTag": "proxy"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -40,10 +40,5 @@
|
|||||||
"ip": [
|
"ip": [
|
||||||
"geoip:ir"
|
"geoip:ir"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"remarks": "Final Agent",
|
|
||||||
"port": "0-65535",
|
|
||||||
"outboundTag": "proxy"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
1285
V2rayNG/app/src/main/assets/open_source_licenses.html
Normal file
1285
V2rayNG/app/src/main/assets/open_source_licenses.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ au.com.shiftyjelly.pocketcasts
|
|||||||
bbc.mobile.news.ww
|
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
|
||||||
@@ -34,7 +35,9 @@ 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
|
||||||
@@ -108,6 +111,7 @@ 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
|
||||||
@@ -115,6 +119,7 @@ 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
|
||||||
@@ -159,6 +164,7 @@ 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
|
||||||
@@ -173,10 +179,13 @@ 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
|
||||||
@@ -186,10 +195,12 @@ 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
|
||||||
@@ -210,6 +221,7 @@ 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
|
||||||
@@ -226,6 +238,7 @@ 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
|
||||||
@@ -239,3 +252,162 @@ 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
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
package com.v2ray.ang
|
package com.v2ray.ang
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication() {
|
class AngApplication : MultiDexApplication() {
|
||||||
companion object {
|
companion object {
|
||||||
//const val PREF_LAST_VERSION = "pref_last_version"
|
|
||||||
lateinit var application: AngApplication
|
lateinit var application: AngApplication
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the base context to the application.
|
||||||
|
* @param base The base context.
|
||||||
|
*/
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
application = this
|
application = this
|
||||||
@@ -26,23 +26,18 @@ class AngApplication : MultiDexApplication() {
|
|||||||
.setDefaultProcessName("${ANG_PACKAGE}:bg")
|
.setDefaultProcessName("${ANG_PACKAGE}:bg")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the application.
|
||||||
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// LeakCanary.install(this)
|
|
||||||
|
|
||||||
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
|
||||||
// if (firstRun)
|
|
||||||
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
|
||||||
|
|
||||||
MMKV.initialize(this)
|
MMKV.initialize(this)
|
||||||
|
|
||||||
Utils.setNightMode()
|
SettingsManager.setNightMode()
|
||||||
// Initialize WorkManager with the custom configuration
|
// Initialize WorkManager with the custom configuration
|
||||||
WorkManager.initialize(this, workManagerConfiguration)
|
WorkManager.initialize(this, workManagerConfiguration)
|
||||||
|
|
||||||
SettingsManager.initRoutingRulesets(this)
|
SettingsManager.initRoutingRulesets(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ object AppConfig {
|
|||||||
|
|
||||||
/** Legacy configuration keys. */
|
/** Legacy configuration keys. */
|
||||||
const val ANG_CONFIG = "ang_config"
|
const val ANG_CONFIG = "ang_config"
|
||||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
|
||||||
|
|
||||||
/** Preferences mapped to MMKV storage. */
|
/** Preferences mapped to MMKV storage. */
|
||||||
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
||||||
@@ -25,6 +24,7 @@ object AppConfig {
|
|||||||
const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy"
|
const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy"
|
||||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
||||||
const val PREF_VPN_DNS = "pref_vpn_dns"
|
const val PREF_VPN_DNS = "pref_vpn_dns"
|
||||||
|
const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
|
||||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||||
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
|
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
|
||||||
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
||||||
@@ -42,6 +42,7 @@ object AppConfig {
|
|||||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||||
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
||||||
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
|
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_LANGUAGE = "pref_language"
|
||||||
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
||||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||||
@@ -50,6 +51,7 @@ object AppConfig {
|
|||||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||||
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
||||||
const val PREF_DOMESTIC_DNS = "pref_domestic_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_DELAY_TEST_URL = "pref_delay_test_url"
|
||||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||||
const val PREF_MODE = "pref_mode"
|
const val PREF_MODE = "pref_mode"
|
||||||
@@ -167,7 +169,7 @@ object AppConfig {
|
|||||||
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
||||||
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
||||||
|
|
||||||
|
|
||||||
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||||
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||||
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
|
|||||||
VLESS(5, AppConfig.VLESS),
|
VLESS(5, AppConfig.VLESS),
|
||||||
TROJAN(6, AppConfig.TROJAN),
|
TROJAN(6, AppConfig.TROJAN),
|
||||||
WIREGUARD(7, AppConfig.WIREGUARD),
|
WIREGUARD(7, AppConfig.WIREGUARD),
|
||||||
// TUIC(8, AppConfig.TUIC),
|
|
||||||
|
// TUIC(8, AppConfig.TUIC),
|
||||||
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
||||||
HTTP(10, AppConfig.HTTP);
|
HTTP(10, AppConfig.HTTP);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data class Hysteria2Bean(
|
|||||||
val http: Socks5Bean? = null,
|
val http: Socks5Bean? = null,
|
||||||
val tls: TlsBean? = null,
|
val tls: TlsBean? = null,
|
||||||
val transport: TransportBean? = null,
|
val transport: TransportBean? = null,
|
||||||
|
val bandwidth: BandwidthBean? = null,
|
||||||
) {
|
) {
|
||||||
data class ObfsBean(
|
data class ObfsBean(
|
||||||
val type: String?,
|
val type: String?,
|
||||||
@@ -37,4 +38,9 @@ data class Hysteria2Bean(
|
|||||||
val hopInterval: String?,
|
val hopInterval: String?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
data class BandwidthBean(
|
||||||
|
val down: String?,
|
||||||
|
val up: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ enum class NetworkType(val type: String) {
|
|||||||
XHTTP("xhttp"),
|
XHTTP("xhttp"),
|
||||||
HTTP("http"),
|
HTTP("http"),
|
||||||
H2("h2"),
|
H2("h2"),
|
||||||
|
|
||||||
//QUIC("quic"),
|
//QUIC("quic"),
|
||||||
GRPC("grpc");
|
GRPC("grpc");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.v2ray.ang.dto
|
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_BLOCKED
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
@@ -53,6 +55,8 @@ data class ProfileItem(
|
|||||||
var portHopping: String? = null,
|
var portHopping: String? = null,
|
||||||
var portHoppingInterval: String? = null,
|
var portHoppingInterval: String? = null,
|
||||||
var pinSHA256: String? = null,
|
var pinSHA256: String? = null,
|
||||||
|
var bandwidthDown: String? = null,
|
||||||
|
var bandwidthUp: String? = null,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -66,6 +70,9 @@ data class ProfileItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getServerAddressAndPort(): String {
|
fun getServerAddressAndPort(): String {
|
||||||
|
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
|
||||||
|
return "$LOOPBACK:$PORT_SOCKS"
|
||||||
|
}
|
||||||
return Utils.getIpv6Address(server) + ":" + serverPort
|
return Utils.getIpv6Address(server) + ":" + serverPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import com.google.gson.JsonSerializer
|
|||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.*
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.ServersBean
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.*
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
@@ -257,7 +259,10 @@ data class V2rayConfig(
|
|||||||
var header: HeaderBean = HeaderBean(),
|
var header: HeaderBean = HeaderBean(),
|
||||||
var seed: String? = null
|
var seed: String? = null
|
||||||
) {
|
) {
|
||||||
data class HeaderBean(var type: String = "none")
|
data class HeaderBean(
|
||||||
|
var type: String = "none",
|
||||||
|
var domain: String? = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class WsSettingsBean(
|
data class WsSettingsBean(
|
||||||
@@ -385,6 +390,11 @@ data class V2rayConfig(
|
|||||||
} else {
|
} else {
|
||||||
kcpsetting.seed = seed
|
kcpsetting.seed = seed
|
||||||
}
|
}
|
||||||
|
if (host.isNullOrEmpty()) {
|
||||||
|
kcpsetting.header.domain = null
|
||||||
|
} else {
|
||||||
|
kcpsetting.header.domain = host
|
||||||
|
}
|
||||||
kcpSettings = kcpsetting
|
kcpSettings = kcpsetting
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,9 +486,9 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class MuxBean(
|
data class MuxBean(
|
||||||
var enabled: Boolean,
|
var enabled: Boolean,
|
||||||
var concurrency: Int = 8,
|
var concurrency: Int? = null,
|
||||||
var xudpConcurrency: Int = 8,
|
var xudpConcurrency: Int? = null,
|
||||||
var xudpProxyUDP443: String = "",
|
var xudpProxyUDP443: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getServerAddress(): String? {
|
fun getServerAddress(): String? {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
import me.drakeet.support.toast.ToastCompat
|
import es.dmoral.toasty.Toasty
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -17,18 +17,75 @@ import java.net.URLConnection
|
|||||||
val Context.v2RayApplication: AngApplication?
|
val Context.v2RayApplication: AngApplication?
|
||||||
get() = applicationContext as? 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) {
|
fun Context.toast(message: Int) {
|
||||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
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) {
|
fun Context.toast(message: CharSequence) {
|
||||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
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?>) {
|
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
|
||||||
put(pair.first, pair.second)
|
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?>) {
|
fun JSONObject.putOpt(pairs: Map<String, Any?>) {
|
||||||
pairs.forEach { put(it.key, it.value) }
|
pairs.forEach { put(it.key, it.value) }
|
||||||
}
|
}
|
||||||
@@ -36,8 +93,18 @@ fun JSONObject.putOpt(pairs: Map<String, Any?>) {
|
|||||||
const val THRESHOLD = 1000L
|
const val THRESHOLD = 1000L
|
||||||
const val DIVISOR = 1024.0
|
const val DIVISOR = 1024.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Long value to a speed string.
|
||||||
|
*
|
||||||
|
* @return The speed string.
|
||||||
|
*/
|
||||||
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Long value to a traffic string.
|
||||||
|
*
|
||||||
|
* @return The traffic string.
|
||||||
|
*/
|
||||||
fun Long.toTrafficString(): String {
|
fun Long.toTrafficString(): String {
|
||||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
||||||
var size = this.toDouble()
|
var size = this.toDouble()
|
||||||
@@ -59,10 +126,27 @@ val URLConnection.responseLength: Long
|
|||||||
val URI.idnHost: String
|
val URI.idnHost: String
|
||||||
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
||||||
|
|
||||||
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
|
/**
|
||||||
|
* 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
|
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) =
|
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
@@ -85,14 +169,31 @@ fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Uni
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
|
||||||
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
|
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 {
|
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
|
||||||
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
|
/**
|
||||||
|
* 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())
|
||||||
@@ -6,6 +6,12 @@ import com.v2ray.ang.dto.V2rayConfig
|
|||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
|
||||||
object CustomFmt : FmtBase() {
|
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? {
|
fun parse(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
val config = ProfileItem.create(EConfigType.CUSTOM)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ import com.v2ray.ang.util.Utils
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
open class FmtBase {
|
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 {
|
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
||||||
val query = if (dicQuery != null)
|
val query = if (dicQuery != null)
|
||||||
("?" + dicQuery.toList().joinToString(
|
("?" + dicQuery.toList().joinToString(
|
||||||
@@ -25,11 +33,24 @@ open class FmtBase {
|
|||||||
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
|
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> {
|
fun getQueryParam(uri: URI): Map<String, String> {
|
||||||
return uri.rawQuery.split("&")
|
return uri.rawQuery.split("&")
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
.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) {
|
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
|
||||||
config.network = queryParam["type"] ?: NetworkType.TCP.type
|
config.network = queryParam["type"] ?: NetworkType.TCP.type
|
||||||
config.headerType = queryParam["headerType"]
|
config.headerType = queryParam["headerType"]
|
||||||
@@ -63,6 +84,12 @@ open class FmtBase {
|
|||||||
config.flow = queryParam["flow"]
|
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> {
|
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
|
||||||
val dicQuery = HashMap<String, String>()
|
val dicQuery = HashMap<String, String>()
|
||||||
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
|
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
|
||||||
@@ -93,7 +120,7 @@ open class FmtBase {
|
|||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkType.XHTTP -> {
|
NetworkType.XHTTP -> {
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||||
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
||||||
@@ -121,5 +148,4 @@ open class FmtBase {
|
|||||||
|
|
||||||
return dicQuery
|
return dicQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,14 @@ import com.v2ray.ang.dto.EConfigType
|
|||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object HttpFmt : FmtBase() {
|
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? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
||||||
|
|
||||||
@@ -23,6 +28,4 @@ object HttpFmt : FmtBase() {
|
|||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,12 @@ import com.v2ray.ang.util.Utils
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object Hysteria2Fmt : FmtBase() {
|
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? {
|
fun parse(str: String): ProfileItem? {
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
@@ -45,6 +51,12 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
return config
|
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 {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = HashMap<String, String>()
|
val dicQuery = HashMap<String, String>()
|
||||||
|
|
||||||
@@ -67,6 +79,13 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
return toUri(config, config.password, dicQuery)
|
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? {
|
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
|
||||||
|
|
||||||
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
|
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
|
||||||
@@ -85,6 +104,12 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val bandwidth = if (config.bandwidthDown.isNullOrEmpty() || config.bandwidthUp.isNullOrEmpty()) null else
|
||||||
|
Hysteria2Bean.BandwidthBean(
|
||||||
|
down = config.bandwidthDown,
|
||||||
|
up = config.bandwidthUp,
|
||||||
|
)
|
||||||
|
|
||||||
val server =
|
val server =
|
||||||
if (config.portHopping.isNullOrEmpty())
|
if (config.portHopping.isNullOrEmpty())
|
||||||
config.getServerAddressAndPort()
|
config.getServerAddressAndPort()
|
||||||
@@ -96,6 +121,7 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
auth = config.password,
|
auth = config.password,
|
||||||
obfs = obfs,
|
obfs = obfs,
|
||||||
transport = transport,
|
transport = transport,
|
||||||
|
bandwidth = bandwidth,
|
||||||
socks5 = Hysteria2Bean.Socks5Bean(
|
socks5 = Hysteria2Bean.Socks5Bean(
|
||||||
listen = "$LOOPBACK:${socksPort}",
|
listen = "$LOOPBACK:${socksPort}",
|
||||||
),
|
),
|
||||||
@@ -111,10 +137,14 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
return bean
|
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? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
|
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,22 @@ import com.v2ray.ang.util.Utils
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object ShadowsocksFmt : FmtBase() {
|
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? {
|
fun parse(str: String): ProfileItem? {
|
||||||
return parseSip002(str) ?: parseLegacy(str)
|
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? {
|
fun parseSip002(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
@@ -55,6 +67,12 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
return config
|
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? {
|
fun parseLegacy(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||||
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||||
@@ -92,12 +110,24 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
return config
|
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 {
|
fun toUri(config: ProfileItem): String {
|
||||||
val pw = "${config.method}:${config.password}"
|
val pw = "${config.method}:${config.password}"
|
||||||
|
|
||||||
return toUri(config, Utils.encode(pw), null)
|
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? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
@@ -134,6 +164,4 @@ object ShadowsocksFmt : FmtBase() {
|
|||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,14 @@ import com.v2ray.ang.extension.idnHost
|
|||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object SocksFmt : FmtBase() {
|
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? {
|
fun parse(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.SOCKS)
|
val config = ProfileItem.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
@@ -32,6 +37,12 @@ object SocksFmt : FmtBase() {
|
|||||||
return config
|
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 {
|
fun toUri(config: ProfileItem): String {
|
||||||
val pw =
|
val pw =
|
||||||
if (config.username.isNotNullEmpty())
|
if (config.username.isNotNullEmpty())
|
||||||
@@ -42,6 +53,12 @@ object SocksFmt : FmtBase() {
|
|||||||
return toUri(config, Utils.encode(pw), null)
|
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? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
@@ -58,5 +75,4 @@ object SocksFmt : FmtBase() {
|
|||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,14 @@ import com.v2ray.ang.extension.idnHost
|
|||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object TrojanFmt : FmtBase() {
|
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? {
|
fun parse(str: String): ProfileItem? {
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||||
@@ -36,12 +41,24 @@ object TrojanFmt : FmtBase() {
|
|||||||
return config
|
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 {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = getQueryDic(config)
|
val dicQuery = getQueryDic(config)
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
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? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ import java.net.URI
|
|||||||
|
|
||||||
object VlessFmt : FmtBase() {
|
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? {
|
fun parse(str: String): ProfileItem? {
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.VLESS)
|
val config = ProfileItem.create(EConfigType.VLESS)
|
||||||
@@ -31,6 +37,12 @@ object VlessFmt : FmtBase() {
|
|||||||
return config
|
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 {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = getQueryDic(config)
|
val dicQuery = getQueryDic(config)
|
||||||
dicQuery["encryption"] = config.method ?: "none"
|
dicQuery["encryption"] = config.method ?: "none"
|
||||||
@@ -38,7 +50,12 @@ object VlessFmt : FmtBase() {
|
|||||||
return toUri(config, config.password, dicQuery)
|
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? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
||||||
|
|
||||||
@@ -78,6 +95,4 @@ object VlessFmt : FmtBase() {
|
|||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -14,9 +14,14 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object VmessFmt : FmtBase() {
|
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? {
|
fun parse(str: String): ProfileItem? {
|
||||||
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
||||||
return parseVmessStd(str)
|
return parseVmessStd(str)
|
||||||
@@ -68,6 +73,7 @@ object VmessFmt : FmtBase() {
|
|||||||
config.serviceName = vmessQRCode.path
|
config.serviceName = vmessQRCode.path
|
||||||
config.authority = vmessQRCode.host
|
config.authority = vmessQRCode.host
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +86,12 @@ object VmessFmt : FmtBase() {
|
|||||||
return config
|
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 {
|
fun toUri(config: ProfileItem): String {
|
||||||
val vmessQRCode = VmessQRCode()
|
val vmessQRCode = VmessQRCode()
|
||||||
|
|
||||||
@@ -108,6 +120,7 @@ object VmessFmt : FmtBase() {
|
|||||||
vmessQRCode.path = config.serviceName.orEmpty()
|
vmessQRCode.path = config.serviceName.orEmpty()
|
||||||
vmessQRCode.host = config.authority.orEmpty()
|
vmessQRCode.host = config.authority.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +136,12 @@ object VmessFmt : FmtBase() {
|
|||||||
return Utils.encode(json)
|
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? {
|
fun parseVmessStd(str: String): ProfileItem? {
|
||||||
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.VMESS)
|
val config = ProfileItem.create(EConfigType.VMESS)
|
||||||
@@ -142,7 +161,12 @@ object VmessFmt : FmtBase() {
|
|||||||
return config
|
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? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,17 @@ import com.v2ray.ang.dto.EConfigType
|
|||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.extension.removeWhiteSpace
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object WireguardFmt : FmtBase() {
|
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? {
|
fun parse(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
@@ -32,6 +38,12 @@ object WireguardFmt : FmtBase() {
|
|||||||
return config
|
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? {
|
fun parseWireguardConfFile(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
@@ -86,6 +98,12 @@ object WireguardFmt : FmtBase() {
|
|||||||
return config
|
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? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
@@ -104,19 +122,25 @@ object WireguardFmt : FmtBase() {
|
|||||||
return outboundBean
|
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 {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = HashMap<String, String>()
|
val dicQuery = HashMap<String, String>()
|
||||||
|
|
||||||
dicQuery["publickey"] = config.publicKey.orEmpty()
|
dicQuery["publickey"] = config.publicKey.orEmpty()
|
||||||
if (config.reserved != null) {
|
if (config.reserved != null) {
|
||||||
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty()
|
dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty()
|
||||||
}
|
}
|
||||||
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty()
|
dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty()
|
||||||
if (config.mtu != null) {
|
if (config.mtu != null) {
|
||||||
dicQuery["mtu"] = config.mtu.toString()
|
dicQuery["mtu"] = config.mtu.toString()
|
||||||
}
|
}
|
||||||
if (config.preSharedKey != null) {
|
if (config.preSharedKey != null) {
|
||||||
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty()
|
dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
return toUri(config, config.secretKey, dicQuery)
|
return toUri(config, config.secretKey, dicQuery)
|
||||||
|
|||||||
@@ -16,14 +16,314 @@ import com.v2ray.ang.fmt.TrojanFmt
|
|||||||
import com.v2ray.ang.fmt.VlessFmt
|
import com.v2ray.ang.fmt.VlessFmt
|
||||||
import com.v2ray.ang.fmt.VmessFmt
|
import com.v2ray.ang.fmt.VmessFmt
|
||||||
import com.v2ray.ang.fmt.WireguardFmt
|
import com.v2ray.ang.fmt.WireguardFmt
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object AngConfigManager {
|
object AngConfigManager {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* parse config form qrcode or...
|
* 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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
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(
|
private fun parseConfig(
|
||||||
str: String?,
|
str: String?,
|
||||||
@@ -79,249 +379,10 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* share config
|
* Updates the configuration via all subscriptions.
|
||||||
|
*
|
||||||
|
* @return The number of configurations updated.
|
||||||
*/
|
*/
|
||||||
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) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2Clipboard
|
|
||||||
*/
|
|
||||||
fun share2Clipboard(context: Context, guid: String): Int {
|
|
||||||
try {
|
|
||||||
val conf = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(conf)) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.setClipboard(context, conf)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2Clipboard
|
|
||||||
*/
|
|
||||||
fun shareNonCustomConfigsToClipboard(context: Context, serverList: List<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) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2QRCode
|
|
||||||
*/
|
|
||||||
fun share2QRCode(guid: String): Bitmap? {
|
|
||||||
try {
|
|
||||||
val conf = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(conf)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return QRCodeDecoder.createQRCode(conf)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shareFullContent2Clipboard
|
|
||||||
*/
|
|
||||||
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
|
|
||||||
try {
|
|
||||||
if (guid == null) return -1
|
|
||||||
val result = 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) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConfigViaSubAll(): Int {
|
fun updateConfigViaSubAll(): Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
try {
|
try {
|
||||||
@@ -335,6 +396,12 @@ object AngConfigManager {
|
|||||||
return count
|
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 {
|
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
|
||||||
try {
|
try {
|
||||||
if (TextUtils.isEmpty(it.first)
|
if (TextUtils.isEmpty(it.first)
|
||||||
@@ -346,7 +413,7 @@ object AngConfigManager {
|
|||||||
if (!it.second.enabled) {
|
if (!it.second.enabled) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
val url = Utils.idnToASCII(it.second.url)
|
val url = HttpUtil.idnToASCII(it.second.url)
|
||||||
if (!Utils.isValidUrl(url)) {
|
if (!Utils.isValidUrl(url)) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -354,7 +421,7 @@ object AngConfigManager {
|
|||||||
|
|
||||||
var configText = try {
|
var configText = try {
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
|
HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
|
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
|
||||||
//e.printStackTrace()
|
//e.printStackTrace()
|
||||||
@@ -362,7 +429,7 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
if (configText.isEmpty()) {
|
if (configText.isEmpty()) {
|
||||||
configText = try {
|
configText = try {
|
||||||
Utils.getUrlContentWithCustomUserAgent(url)
|
HttpUtil.getUrlContentWithUserAgent(url)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
""
|
""
|
||||||
@@ -378,6 +445,14 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
|
||||||
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
@@ -389,6 +464,12 @@ object AngConfigManager {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports a URL as a subscription.
|
||||||
|
*
|
||||||
|
* @param url The URL.
|
||||||
|
* @return The number of subscriptions imported.
|
||||||
|
*/
|
||||||
private fun importUrlAsSubscription(url: String): Int {
|
private fun importUrlAsSubscription(url: String): Int {
|
||||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
subscriptions.forEach {
|
subscriptions.forEach {
|
||||||
|
|||||||
@@ -8,14 +8,19 @@ import com.v2ray.ang.dto.EConfigType
|
|||||||
import com.v2ray.ang.dto.NetworkType
|
import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.extension.removeWhiteSpace
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
object MigrateManager {
|
object MigrateManager {
|
||||||
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
||||||
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
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 {
|
fun migrateServerConfig2Profile(): Boolean {
|
||||||
if (serverStorage.count().toInt() == 0) {
|
if (serverStorage.count().toInt() == 0) {
|
||||||
return false
|
return false
|
||||||
@@ -44,6 +49,12 @@ object MigrateManager {
|
|||||||
return true
|
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? {
|
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
|
||||||
return when (configOld.getProxyOutbound()?.protocol) {
|
return when (configOld.getProxyOutbound()?.protocol) {
|
||||||
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
||||||
@@ -62,6 +73,12 @@ object MigrateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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? {
|
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(configOld.configType)
|
val config = ProfileItem.create(configOld.configType)
|
||||||
|
|
||||||
@@ -92,7 +109,7 @@ object MigrateManager {
|
|||||||
config.insecure = tlsSettings?.allowInsecure
|
config.insecure = tlsSettings?.allowInsecure
|
||||||
config.sni = tlsSettings?.serverName
|
config.sni = tlsSettings?.serverName
|
||||||
config.fingerPrint = tlsSettings?.fingerprint
|
config.fingerPrint = tlsSettings?.fingerprint
|
||||||
config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString()
|
config.alpn = tlsSettings?.alpn?.joinToString(",").removeWhiteSpace().toString()
|
||||||
|
|
||||||
config.publicKey = tlsSettings?.publicKey
|
config.publicKey = tlsSettings?.publicKey
|
||||||
config.shortId = tlsSettings?.shortId
|
config.shortId = tlsSettings?.shortId
|
||||||
@@ -101,6 +118,12 @@ object MigrateManager {
|
|||||||
return config
|
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? {
|
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.SOCKS)
|
val config = ProfileItem.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
@@ -114,6 +137,12 @@ object MigrateManager {
|
|||||||
return config
|
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? {
|
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.HTTP)
|
val config = ProfileItem.create(EConfigType.HTTP)
|
||||||
|
|
||||||
@@ -127,6 +156,12 @@ object MigrateManager {
|
|||||||
return config
|
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? {
|
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
@@ -137,14 +172,20 @@ object MigrateManager {
|
|||||||
|
|
||||||
outbound.settings?.let { wireguard ->
|
outbound.settings?.let { wireguard ->
|
||||||
config.secretKey = wireguard.secretKey
|
config.secretKey = wireguard.secretKey
|
||||||
config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString()
|
config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString()
|
||||||
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
|
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
|
||||||
config.mtu = wireguard.mtu
|
config.mtu = wireguard.mtu
|
||||||
config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString()
|
config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString()
|
||||||
}
|
}
|
||||||
return config
|
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? {
|
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
@@ -158,7 +199,7 @@ object MigrateManager {
|
|||||||
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
|
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
|
||||||
config.insecure = tlsSetting.allowInsecure
|
config.insecure = tlsSetting.allowInsecure
|
||||||
config.sni = tlsSetting.serverName
|
config.sni = tlsSetting.serverName
|
||||||
config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty()
|
config.alpn = tlsSetting.alpn?.joinToString(",").removeWhiteSpace().orEmpty()
|
||||||
|
|
||||||
}
|
}
|
||||||
config.obfsPassword = outbound.settings?.obfsPassword
|
config.obfsPassword = outbound.settings?.obfsPassword
|
||||||
@@ -166,6 +207,12 @@ object MigrateManager {
|
|||||||
return config
|
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? {
|
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
val config = ProfileItem.create(EConfigType.CUSTOM)
|
||||||
|
|
||||||
@@ -177,7 +224,12 @@ object MigrateManager {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the old server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The old server configuration.
|
||||||
|
*/
|
||||||
private fun decodeServerConfigOld(guid: String): ServerConfig? {
|
private fun decodeServerConfigOld(guid: String): ServerConfig? {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.handler
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
|
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
|
||||||
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
|
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
|
||||||
@@ -41,18 +40,38 @@ object MmkvManager {
|
|||||||
|
|
||||||
//region Server
|
//region Server
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the selected server GUID.
|
||||||
|
*
|
||||||
|
* @return The selected server GUID.
|
||||||
|
*/
|
||||||
fun getSelectServer(): String? {
|
fun getSelectServer(): String? {
|
||||||
return mainStorage.decodeString(KEY_SELECTED_SERVER)
|
return mainStorage.decodeString(KEY_SELECTED_SERVER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected server GUID.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
*/
|
||||||
fun setSelectServer(guid: String) {
|
fun setSelectServer(guid: String) {
|
||||||
mainStorage.encode(KEY_SELECTED_SERVER, guid)
|
mainStorage.encode(KEY_SELECTED_SERVER, guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the server list.
|
||||||
|
*
|
||||||
|
* @param serverList The list of server GUIDs.
|
||||||
|
*/
|
||||||
fun encodeServerList(serverList: MutableList<String>) {
|
fun encodeServerList(serverList: MutableList<String>) {
|
||||||
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
|
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the server list.
|
||||||
|
*
|
||||||
|
* @return The list of server GUIDs.
|
||||||
|
*/
|
||||||
fun decodeServerList(): MutableList<String> {
|
fun decodeServerList(): MutableList<String> {
|
||||||
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
||||||
return if (json.isNullOrBlank()) {
|
return if (json.isNullOrBlank()) {
|
||||||
@@ -62,7 +81,12 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The server configuration.
|
||||||
|
*/
|
||||||
fun decodeServerConfig(guid: String): ProfileItem? {
|
fun decodeServerConfig(guid: String): ProfileItem? {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
@@ -85,6 +109,13 @@ object MmkvManager {
|
|||||||
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
|
// 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 {
|
fun encodeServerConfig(guid: String, config: ProfileItem): String {
|
||||||
val key = guid.ifBlank { Utils.getUuid() }
|
val key = guid.ifBlank { Utils.getUuid() }
|
||||||
profileFullStorage.encode(key, JsonUtil.toJson(config))
|
profileFullStorage.encode(key, JsonUtil.toJson(config))
|
||||||
@@ -107,6 +138,11 @@ object MmkvManager {
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
*/
|
||||||
fun removeServer(guid: String) {
|
fun removeServer(guid: String) {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return
|
return
|
||||||
@@ -122,6 +158,11 @@ object MmkvManager {
|
|||||||
serverAffStorage.remove(guid)
|
serverAffStorage.remove(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the server configurations via subscription ID.
|
||||||
|
*
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
*/
|
||||||
fun removeServerViaSubid(subid: String) {
|
fun removeServerViaSubid(subid: String) {
|
||||||
if (subid.isBlank()) {
|
if (subid.isBlank()) {
|
||||||
return
|
return
|
||||||
@@ -135,6 +176,12 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the server affiliation information.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The server affiliation information.
|
||||||
|
*/
|
||||||
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
|
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
@@ -146,6 +193,12 @@ object MmkvManager {
|
|||||||
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
|
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) {
|
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return
|
return
|
||||||
@@ -155,6 +208,11 @@ object MmkvManager {
|
|||||||
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
|
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all test delay results.
|
||||||
|
*
|
||||||
|
* @param keys The list of server GUIDs.
|
||||||
|
*/
|
||||||
fun clearAllTestDelayResults(keys: List<String>?) {
|
fun clearAllTestDelayResults(keys: List<String>?) {
|
||||||
keys?.forEach { key ->
|
keys?.forEach { key ->
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
@@ -164,6 +222,11 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all server configurations.
|
||||||
|
*
|
||||||
|
* @return The number of server configurations removed.
|
||||||
|
*/
|
||||||
fun removeAllServer(): Int {
|
fun removeAllServer(): Int {
|
||||||
val count = profileFullStorage.allKeys()?.count() ?: 0
|
val count = profileFullStorage.allKeys()?.count() ?: 0
|
||||||
mainStorage.clearAll()
|
mainStorage.clearAll()
|
||||||
@@ -173,6 +236,12 @@ object MmkvManager {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes invalid server configurations.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The number of server configurations removed.
|
||||||
|
*/
|
||||||
fun removeInvalidServer(guid: String): Int {
|
fun removeInvalidServer(guid: String): Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
if (guid.isNotEmpty()) {
|
if (guid.isNotEmpty()) {
|
||||||
@@ -195,10 +264,22 @@ object MmkvManager {
|
|||||||
return 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) {
|
fun encodeServerRaw(guid: String, config: String) {
|
||||||
serverRawStorage.encode(guid, config)
|
serverRawStorage.encode(guid, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the raw server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The raw server configuration.
|
||||||
|
*/
|
||||||
fun decodeServerRaw(guid: String): String? {
|
fun decodeServerRaw(guid: String): String? {
|
||||||
return serverRawStorage.decodeString(guid)
|
return serverRawStorage.decodeString(guid)
|
||||||
}
|
}
|
||||||
@@ -207,6 +288,9 @@ object MmkvManager {
|
|||||||
|
|
||||||
//region Subscriptions
|
//region Subscriptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the subscription list.
|
||||||
|
*/
|
||||||
private fun initSubsList() {
|
private fun initSubsList() {
|
||||||
val subsList = decodeSubsList()
|
val subsList = decodeSubsList()
|
||||||
if (subsList.isNotEmpty()) {
|
if (subsList.isNotEmpty()) {
|
||||||
@@ -218,6 +302,11 @@ object MmkvManager {
|
|||||||
encodeSubsList(subsList)
|
encodeSubsList(subsList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the subscriptions.
|
||||||
|
*
|
||||||
|
* @return The list of subscriptions.
|
||||||
|
*/
|
||||||
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
|
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
|
||||||
initSubsList()
|
initSubsList()
|
||||||
|
|
||||||
@@ -231,6 +320,11 @@ object MmkvManager {
|
|||||||
return subscriptions
|
return subscriptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the subscription.
|
||||||
|
*
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
*/
|
||||||
fun removeSubscription(subid: String) {
|
fun removeSubscription(subid: String) {
|
||||||
subStorage.remove(subid)
|
subStorage.remove(subid)
|
||||||
val subsList = decodeSubsList()
|
val subsList = decodeSubsList()
|
||||||
@@ -240,6 +334,12 @@ object MmkvManager {
|
|||||||
removeServerViaSubid(subid)
|
removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the subscription.
|
||||||
|
*
|
||||||
|
* @param guid The subscription GUID.
|
||||||
|
* @param subItem The subscription item.
|
||||||
|
*/
|
||||||
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
|
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
|
||||||
val key = guid.ifBlank { Utils.getUuid() }
|
val key = guid.ifBlank { Utils.getUuid() }
|
||||||
subStorage.encode(key, JsonUtil.toJson(subItem))
|
subStorage.encode(key, JsonUtil.toJson(subItem))
|
||||||
@@ -251,15 +351,31 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the subscription.
|
||||||
|
*
|
||||||
|
* @param subscriptionId The subscription ID.
|
||||||
|
* @return The subscription item.
|
||||||
|
*/
|
||||||
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
|
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
|
||||||
val json = subStorage.decodeString(subscriptionId) ?: return null
|
val json = subStorage.decodeString(subscriptionId) ?: return null
|
||||||
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
|
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the subscription list.
|
||||||
|
*
|
||||||
|
* @param subsList The list of subscription IDs.
|
||||||
|
*/
|
||||||
fun encodeSubsList(subsList: MutableList<String>) {
|
fun encodeSubsList(subsList: MutableList<String>) {
|
||||||
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
|
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the subscription list.
|
||||||
|
*
|
||||||
|
* @return The list of subscription IDs.
|
||||||
|
*/
|
||||||
fun decodeSubsList(): MutableList<String> {
|
fun decodeSubsList(): MutableList<String> {
|
||||||
val json = mainStorage.decodeString(KEY_SUB_IDS)
|
val json = mainStorage.decodeString(KEY_SUB_IDS)
|
||||||
return if (json.isNullOrBlank()) {
|
return if (json.isNullOrBlank()) {
|
||||||
@@ -273,6 +389,11 @@ object MmkvManager {
|
|||||||
|
|
||||||
//region Asset
|
//region Asset
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the asset URLs.
|
||||||
|
*
|
||||||
|
* @return The list of asset URLs.
|
||||||
|
*/
|
||||||
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
||||||
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||||
assetStorage.allKeys()?.forEach { key ->
|
assetStorage.allKeys()?.forEach { key ->
|
||||||
@@ -284,15 +405,32 @@ object MmkvManager {
|
|||||||
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the asset URL.
|
||||||
|
*
|
||||||
|
* @param assetid The asset ID.
|
||||||
|
*/
|
||||||
fun removeAssetUrl(assetid: String) {
|
fun removeAssetUrl(assetid: String) {
|
||||||
assetStorage.remove(assetid)
|
assetStorage.remove(assetid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the asset.
|
||||||
|
*
|
||||||
|
* @param assetid The asset ID.
|
||||||
|
* @param assetItem The asset item.
|
||||||
|
*/
|
||||||
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
|
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
|
||||||
val key = assetid.ifBlank { Utils.getUuid() }
|
val key = assetid.ifBlank { Utils.getUuid() }
|
||||||
assetStorage.encode(key, JsonUtil.toJson(assetItem))
|
assetStorage.encode(key, JsonUtil.toJson(assetItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the asset.
|
||||||
|
*
|
||||||
|
* @param assetid The asset ID.
|
||||||
|
* @return The asset item.
|
||||||
|
*/
|
||||||
fun decodeAsset(assetid: String): AssetUrlItem? {
|
fun decodeAsset(assetid: String): AssetUrlItem? {
|
||||||
val json = assetStorage.decodeString(assetid) ?: return null
|
val json = assetStorage.decodeString(assetid) ?: return null
|
||||||
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
|
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
|
||||||
@@ -302,12 +440,22 @@ object MmkvManager {
|
|||||||
|
|
||||||
//region Routing
|
//region Routing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the routing rulesets.
|
||||||
|
*
|
||||||
|
* @return The list of routing rulesets.
|
||||||
|
*/
|
||||||
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
||||||
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
||||||
if (ruleset.isNullOrEmpty()) return null
|
if (ruleset.isNullOrEmpty()) return null
|
||||||
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
|
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>?) {
|
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
|
||||||
if (rulesetList.isNullOrEmpty())
|
if (rulesetList.isNullOrEmpty())
|
||||||
encodeSettings(PREF_ROUTING_RULESET, "")
|
encodeSettings(PREF_ROUTING_RULESET, "")
|
||||||
@@ -316,43 +464,99 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//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 {
|
fun encodeSettings(key: String, value: String?): Boolean {
|
||||||
return settingsStorage.encode(key, value)
|
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 {
|
fun encodeSettings(key: String, value: Int): Boolean {
|
||||||
return settingsStorage.encode(key, value)
|
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 {
|
fun encodeSettings(key: String, value: Boolean): Boolean {
|
||||||
return settingsStorage.encode(key, value)
|
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 {
|
fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
|
||||||
return settingsStorage.encode(key, value)
|
return settingsStorage.encode(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the settings string.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @return The settings value.
|
||||||
|
*/
|
||||||
fun decodeSettingsString(key: String): String? {
|
fun decodeSettingsString(key: String): String? {
|
||||||
return settingsStorage.decodeString(key)
|
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? {
|
fun decodeSettingsString(key: String, defaultValue: String?): String? {
|
||||||
return settingsStorage.decodeString(key, defaultValue)
|
return settingsStorage.decodeString(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the settings boolean.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @return The settings value.
|
||||||
|
*/
|
||||||
fun decodeSettingsBool(key: String): Boolean {
|
fun decodeSettingsBool(key: String): Boolean {
|
||||||
return settingsStorage.decodeBool(key, false)
|
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 {
|
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
|
||||||
return settingsStorage.decodeBool(key, defaultValue)
|
return settingsStorage.decodeBool(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decodeSettingsInt(key: String, defaultValue: Int): Int {
|
/**
|
||||||
return settingsStorage.decodeInt(key, defaultValue)
|
* Decodes the settings string set.
|
||||||
}
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @return The settings value.
|
||||||
|
*/
|
||||||
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
|
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
|
||||||
return settingsStorage.decodeStringSet(key)
|
return settingsStorage.decodeStringSet(key)
|
||||||
}
|
}
|
||||||
@@ -361,10 +565,20 @@ object MmkvManager {
|
|||||||
|
|
||||||
//region Others
|
//region Others
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the start on boot setting.
|
||||||
|
*
|
||||||
|
* @param startOnBoot Whether to start on boot.
|
||||||
|
*/
|
||||||
fun encodeStartOnBoot(startOnBoot: Boolean) {
|
fun encodeStartOnBoot(startOnBoot: Boolean) {
|
||||||
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
|
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the start on boot setting.
|
||||||
|
*
|
||||||
|
* @return Whether to start on boot.
|
||||||
|
*/
|
||||||
fun decodeStartOnBoot(): Boolean {
|
fun decodeStartOnBoot(): Boolean {
|
||||||
return decodeSettingsBool(PREF_IS_BOOTED, false)
|
return decodeSettingsBool(PREF_IS_BOOTED, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,33 @@ import android.content.Context
|
|||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
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.ProfileItem
|
||||||
import com.v2ray.ang.dto.RoutingType
|
import com.v2ray.ang.dto.RoutingType
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerList
|
import com.v2ray.ang.handler.MmkvManager.decodeServerList
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.Utils.parseInt
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import kotlin.Int
|
import java.util.Locale
|
||||||
|
|
||||||
object SettingsManager {
|
object SettingsManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize routing rulesets.
|
||||||
|
* @param context The application context.
|
||||||
|
*/
|
||||||
fun initRoutingRulesets(context: Context) {
|
fun initRoutingRulesets(context: Context) {
|
||||||
val exist = MmkvManager.decodeRoutingRulesets()
|
val exist = MmkvManager.decodeRoutingRulesets()
|
||||||
if (exist.isNullOrEmpty()) {
|
if (exist.isNullOrEmpty()) {
|
||||||
@@ -32,6 +39,12 @@ object SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>? {
|
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
|
||||||
val fileName = RoutingType.fromIndex(index).fileName
|
val fileName = RoutingType.fromIndex(index).fileName
|
||||||
val assets = Utils.readTextFromAssets(context, fileName)
|
val assets = Utils.readTextFromAssets(context, fileName)
|
||||||
@@ -42,12 +55,21 @@ object SettingsManager {
|
|||||||
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
|
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) {
|
fun resetRoutingRulesetsFromPresets(context: Context, index: Int) {
|
||||||
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
|
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
|
||||||
resetRoutingRulesetsCommon(rulesetList)
|
resetRoutingRulesetsCommon(rulesetList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset routing rulesets.
|
||||||
|
* @param content The content of the rulesets.
|
||||||
|
* @return True if successful, false otherwise.
|
||||||
|
*/
|
||||||
fun resetRoutingRulesets(content: String?): Boolean {
|
fun resetRoutingRulesets(content: String?): Boolean {
|
||||||
if (content.isNullOrEmpty()) {
|
if (content.isNullOrEmpty()) {
|
||||||
return false
|
return false
|
||||||
@@ -67,6 +89,10 @@ object SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common method to reset routing rulesets.
|
||||||
|
* @param rulesetList The list of rulesets.
|
||||||
|
*/
|
||||||
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
|
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
|
||||||
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
|
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
|
||||||
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
|
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
|
||||||
@@ -79,6 +105,11 @@ object SettingsManager {
|
|||||||
MmkvManager.encodeRoutingRulesets(rulesetNew)
|
MmkvManager.encodeRoutingRulesets(rulesetNew)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a routing ruleset by index.
|
||||||
|
* @param index The index of the ruleset.
|
||||||
|
* @return The RulesetItem.
|
||||||
|
*/
|
||||||
fun getRoutingRuleset(index: Int): RulesetItem? {
|
fun getRoutingRuleset(index: Int): RulesetItem? {
|
||||||
if (index < 0) return null
|
if (index < 0) return null
|
||||||
|
|
||||||
@@ -88,11 +119,18 @@ object SettingsManager {
|
|||||||
return rulesetList[index]
|
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?) {
|
fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
|
||||||
if (ruleset == null) return
|
if (ruleset == null) return
|
||||||
|
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
var rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
if (rulesetList.isNullOrEmpty()) return
|
if (rulesetList.isNullOrEmpty()) {
|
||||||
|
rulesetList = mutableListOf()
|
||||||
|
}
|
||||||
|
|
||||||
if (index < 0 || index >= rulesetList.count()) {
|
if (index < 0 || index >= rulesetList.count()) {
|
||||||
rulesetList.add(0, ruleset)
|
rulesetList.add(0, ruleset)
|
||||||
@@ -102,6 +140,10 @@ object SettingsManager {
|
|||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a routing ruleset by index.
|
||||||
|
* @param index The index of the ruleset.
|
||||||
|
*/
|
||||||
fun removeRoutingRuleset(index: Int) {
|
fun removeRoutingRuleset(index: Int) {
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
|
|
||||||
@@ -112,7 +154,29 @@ object SettingsManager {
|
|||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if routing rulesets bypass LAN.
|
||||||
|
* @return True if bypassing LAN, false otherwise.
|
||||||
|
*/
|
||||||
fun routingRulesetsBypassLan(): Boolean {
|
fun routingRulesetsBypassLan(): Boolean {
|
||||||
|
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "0"
|
||||||
|
if (vpnBypassLan == "1") {
|
||||||
|
return true
|
||||||
|
} else if (vpnBypassLan == "2") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val guid = MmkvManager.getSelectServer() ?: return false
|
||||||
|
val config = MmkvManager.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 rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any {
|
val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any {
|
||||||
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
|
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
|
||||||
@@ -120,6 +184,11 @@ object SettingsManager {
|
|||||||
return exist == 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) {
|
fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
if (rulesetList.isNullOrEmpty()) return
|
if (rulesetList.isNullOrEmpty()) return
|
||||||
@@ -128,6 +197,11 @@ object SettingsManager {
|
|||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
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) {
|
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
|
||||||
val subsList = MmkvManager.decodeSubsList()
|
val subsList = MmkvManager.decodeSubsList()
|
||||||
if (subsList.isNullOrEmpty()) return
|
if (subsList.isNullOrEmpty()) return
|
||||||
@@ -136,6 +210,11 @@ object SettingsManager {
|
|||||||
MmkvManager.encodeSubsList(subsList)
|
MmkvManager.encodeSubsList(subsList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server via remarks.
|
||||||
|
* @param remarks The remarks of the server.
|
||||||
|
* @return The ProfileItem.
|
||||||
|
*/
|
||||||
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
||||||
if (remarks == null) {
|
if (remarks == null) {
|
||||||
return null
|
return null
|
||||||
@@ -150,14 +229,27 @@ object SettingsManager {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the SOCKS port.
|
||||||
|
* @return The SOCKS port.
|
||||||
|
*/
|
||||||
fun getSocksPort(): Int {
|
fun getSocksPort(): Int {
|
||||||
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the HTTP port.
|
||||||
|
* @return The HTTP port.
|
||||||
|
*/
|
||||||
fun getHttpPort(): Int {
|
fun getHttpPort(): Int {
|
||||||
return getSocksPort() + (if (Utils.isXray()) 0 else 1)
|
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) {
|
fun initAssets(context: Context, assets: AssetManager) {
|
||||||
val extFolder = Utils.userAssetPath(context)
|
val extFolder = Utils.userAssetPath(context)
|
||||||
|
|
||||||
@@ -181,6 +273,89 @@ object SettingsManager {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
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.DelayTestUrl2
|
||||||
|
} else {
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
||||||
|
?: AppConfig.DelayTestUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.responseLength
|
import com.v2ray.ang.extension.responseLength
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.net.URL
|
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
object SpeedtestUtil {
|
object SpeedtestManager {
|
||||||
|
|
||||||
private val tcpTestingSockets = ArrayList<Socket?>()
|
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 {
|
suspend fun tcping(url: String, port: Int): Long {
|
||||||
var time = -1L
|
var time = -1L
|
||||||
for (k in 0 until 2) {
|
for (k in 0 until 2) {
|
||||||
@@ -37,15 +41,27 @@ object SpeedtestUtil {
|
|||||||
return time
|
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 {
|
fun realPing(config: String): Long {
|
||||||
return try {
|
return try {
|
||||||
Libv2ray.measureOutboundDelay(config, Utils.getDelayTestUrl())
|
Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
|
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
|
||||||
-1L
|
-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 {
|
fun ping(url: String): String {
|
||||||
try {
|
try {
|
||||||
val command = "/system/bin/ping -c 3 $url"
|
val command = "/system/bin/ping -c 3 $url"
|
||||||
@@ -65,6 +81,13 @@ object SpeedtestUtil {
|
|||||||
return "-1ms"
|
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 {
|
fun socketConnectTime(url: String, port: Int): Long {
|
||||||
try {
|
try {
|
||||||
val socket = Socket()
|
val socket = Socket()
|
||||||
@@ -89,6 +112,9 @@ object SpeedtestUtil {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all TCP sockets that are currently being tested.
|
||||||
|
*/
|
||||||
fun closeAllTcpSockets() {
|
fun closeAllTcpSockets() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
tcpTestingSockets.forEach {
|
tcpTestingSockets.forEach {
|
||||||
@@ -98,26 +124,19 @@ object SpeedtestUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
fun testConnection(context: Context, port: Int): Pair<Long, String> {
|
||||||
var result: String
|
var result: String
|
||||||
var elapsed = -1L
|
var elapsed = -1L
|
||||||
var conn: HttpURLConnection? = null
|
|
||||||
|
|
||||||
|
val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
|
||||||
try {
|
try {
|
||||||
val url = URL(Utils.getDelayTestUrl())
|
|
||||||
|
|
||||||
conn = url.openConnection(
|
|
||||||
Proxy(
|
|
||||||
Proxy.Type.HTTP,
|
|
||||||
InetSocketAddress(LOOPBACK, port)
|
|
||||||
)
|
|
||||||
) as HttpURLConnection
|
|
||||||
conn.connectTimeout = 30000
|
|
||||||
conn.readTimeout = 30000
|
|
||||||
conn.setRequestProperty("Connection", "close")
|
|
||||||
conn.instanceFollowRedirects = false
|
|
||||||
conn.useCaches = false
|
|
||||||
|
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
val code = conn.responseCode
|
val code = conn.responseCode
|
||||||
elapsed = SystemClock.elapsedRealtime() - start
|
elapsed = SystemClock.elapsedRealtime() - start
|
||||||
@@ -133,11 +152,9 @@ object SpeedtestUtil {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// network exception
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
|
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
result = context.getString(R.string.connection_test_error, e.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// library exception, eg sumsung
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e))
|
Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e))
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
result = context.getString(R.string.connection_test_error, e.message)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -147,6 +164,11 @@ object SpeedtestUtil {
|
|||||||
return Pair(elapsed, result)
|
return Pair(elapsed, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the version of the V2Ray library.
|
||||||
|
*
|
||||||
|
* @return The version of the V2Ray library.
|
||||||
|
*/
|
||||||
fun getLibVersion(): String {
|
fun getLibVersion(): String {
|
||||||
return Libv2ray.checkVersionX()
|
return Libv2ray.checkVersionX()
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,7 @@ import com.v2ray.ang.dto.ProfileItem
|
|||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.fmt.HttpFmt
|
import com.v2ray.ang.fmt.HttpFmt
|
||||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
import com.v2ray.ang.fmt.ShadowsocksFmt
|
import com.v2ray.ang.fmt.ShadowsocksFmt
|
||||||
@@ -52,6 +53,13 @@ import com.v2ray.ang.util.Utils
|
|||||||
|
|
||||||
object V2rayConfigManager {
|
object V2rayConfigManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the V2ray configuration for the given GUID.
|
||||||
|
*
|
||||||
|
* @param context The context of the caller.
|
||||||
|
* @param guid The unique identifier for the V2ray configuration.
|
||||||
|
* @return A ConfigResult object containing the configuration details or indicating failure.
|
||||||
|
*/
|
||||||
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
|
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
|
||||||
try {
|
try {
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
||||||
@@ -71,6 +79,13 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the non-custom V2ray configuration.
|
||||||
|
*
|
||||||
|
* @param context The context in which the function is called.
|
||||||
|
* @param config The profile item containing the configuration details.
|
||||||
|
* @return A ConfigResult object containing the result of the configuration retrieval.
|
||||||
|
*/
|
||||||
private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult {
|
private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult {
|
||||||
val result = ConfigResult(false)
|
val result = ConfigResult(false)
|
||||||
|
|
||||||
@@ -82,7 +97,6 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//取得默认配置
|
|
||||||
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||||
if (TextUtils.isEmpty(assets)) {
|
if (TextUtils.isEmpty(assets)) {
|
||||||
return result
|
return result
|
||||||
@@ -239,7 +253,7 @@ object V2rayConfigManager {
|
|||||||
|
|
||||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
rulesetItems?.forEach { key ->
|
rulesetItems?.forEach { key ->
|
||||||
if (key != null && key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
|
if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
|
||||||
key.domain?.forEach {
|
key.domain?.forEach {
|
||||||
if (it != GEOSITE_PRIVATE
|
if (it != GEOSITE_PRIVATE
|
||||||
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
|
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
|
||||||
@@ -269,8 +283,8 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS inbound对象
|
// DNS inbound
|
||||||
val remoteDns = Utils.getRemoteDnsServers()
|
val remoteDns = SettingsManager.getRemoteDnsServers()
|
||||||
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
|
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
|
||||||
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
|
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
|
||||||
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
|
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
|
||||||
@@ -294,7 +308,7 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS outbound对象
|
// DNS outbound
|
||||||
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
|
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
|
||||||
v2rayConfig.outbounds.add(
|
v2rayConfig.outbounds.add(
|
||||||
V2rayConfig.OutboundBean(
|
V2rayConfig.OutboundBean(
|
||||||
@@ -328,12 +342,12 @@ object V2rayConfigManager {
|
|||||||
val servers = ArrayList<Any>()
|
val servers = ArrayList<Any>()
|
||||||
|
|
||||||
//remote Dns
|
//remote Dns
|
||||||
val remoteDns = Utils.getRemoteDnsServers()
|
val remoteDns = SettingsManager.getRemoteDnsServers()
|
||||||
val proxyDomain = userRule2Domain(TAG_PROXY)
|
val proxyDomain = userRule2Domain(TAG_PROXY)
|
||||||
remoteDns.forEach {
|
remoteDns.forEach {
|
||||||
servers.add(it)
|
servers.add(it)
|
||||||
}
|
}
|
||||||
if (proxyDomain.size > 0) {
|
if (proxyDomain.isNotEmpty()) {
|
||||||
servers.add(
|
servers.add(
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
address = remoteDns.first(),
|
address = remoteDns.first(),
|
||||||
@@ -343,11 +357,11 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// domestic DNS
|
// domestic DNS
|
||||||
val domesticDns = Utils.getDomesticDnsServers()
|
val domesticDns = SettingsManager.getDomesticDnsServers()
|
||||||
val directDomain = userRule2Domain(TAG_DIRECT)
|
val directDomain = userRule2Domain(TAG_DIRECT)
|
||||||
val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
|
val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
|
||||||
val geoipCn = arrayListOf(GEOIP_CN)
|
val geoipCn = arrayListOf(GEOIP_CN)
|
||||||
if (directDomain.size > 0) {
|
if (directDomain.isNotEmpty()) {
|
||||||
servers.add(
|
servers.add(
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
address = domesticDns.first(),
|
address = domesticDns.first(),
|
||||||
@@ -369,9 +383,23 @@ object V2rayConfigManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//User DNS hosts
|
||||||
|
try {
|
||||||
|
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
||||||
|
if (userHosts.isNotNullEmpty()) {
|
||||||
|
var userHostsMap = userHosts?.split(",")
|
||||||
|
?.filter { it.isNotEmpty() }
|
||||||
|
?.filter { it.contains(":") }
|
||||||
|
?.associate { it.split(":").let { (k, v) -> k to v } }
|
||||||
|
if (userHostsMap != null) hosts.putAll(userHostsMap)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
//block dns
|
//block dns
|
||||||
val blkDomain = userRule2Domain(TAG_BLOCKED)
|
val blkDomain = userRule2Domain(TAG_BLOCKED)
|
||||||
if (blkDomain.size > 0) {
|
if (blkDomain.isNotEmpty()) {
|
||||||
hosts.putAll(blkDomain.map { it to LOOPBACK })
|
hosts.putAll(blkDomain.map { it to LOOPBACK })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +415,7 @@ object V2rayConfigManager {
|
|||||||
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
|
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
|
||||||
|
|
||||||
|
|
||||||
// DNS dns对象
|
// DNS dns
|
||||||
v2rayConfig.dns = V2rayConfig.DnsBean(
|
v2rayConfig.dns = V2rayConfig.DnsBean(
|
||||||
servers = servers,
|
servers = servers,
|
||||||
hosts = hosts
|
hosts = hosts
|
||||||
@@ -423,21 +451,18 @@ object V2rayConfigManager {
|
|||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
) {
|
) {
|
||||||
muxEnabled = false
|
muxEnabled = false
|
||||||
} else if (protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
&& outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true
|
|
||||||
) {
|
|
||||||
muxEnabled = false
|
|
||||||
} else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
|
} else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
|
||||||
muxEnabled = false
|
muxEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (muxEnabled == true) {
|
if (muxEnabled == true) {
|
||||||
outbound.mux?.enabled = true
|
outbound.mux?.enabled = true
|
||||||
outbound.mux?.concurrency =
|
outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt()
|
||||||
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_CONCURRENCY, 8)
|
outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt()
|
||||||
outbound.mux?.xudpConcurrency =
|
outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC, "reject")
|
||||||
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY, 16)
|
if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true) {
|
||||||
outbound.mux?.xudpProxyUDP443 =
|
outbound.mux?.concurrency = -1
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
|
}
|
||||||
} else {
|
} else {
|
||||||
outbound.mux?.enabled = false
|
outbound.mux?.enabled = false
|
||||||
outbound.mux?.concurrency = -1
|
outbound.mux?.concurrency = -1
|
||||||
@@ -618,6 +643,12 @@ object V2rayConfigManager {
|
|||||||
return returnPair
|
return returnPair
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the proxy outbound configuration for the given profile item.
|
||||||
|
*
|
||||||
|
* @param profileItem The profile item for which to get the proxy outbound configuration.
|
||||||
|
* @return The proxy outbound configuration as a V2rayConfig.OutboundBean, or null if not found.
|
||||||
|
*/
|
||||||
fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
|
fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
|
||||||
return when (profileItem.configType) {
|
return when (profileItem.configType) {
|
||||||
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
|
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.v2ray.ang.helper
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class CustomDividerItemDecoration(
|
||||||
|
private val divider: Drawable,
|
||||||
|
private val orientation: Int
|
||||||
|
) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
if (orientation == RecyclerView.VERTICAL) {
|
||||||
|
drawVerticalDividers(canvas, parent)
|
||||||
|
} else {
|
||||||
|
drawHorizontalDividers(canvas, parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawVerticalDividers(canvas: Canvas, parent: RecyclerView) {
|
||||||
|
val left = parent.paddingLeft
|
||||||
|
val right = parent.width - parent.paddingRight
|
||||||
|
|
||||||
|
val childCount = parent.childCount
|
||||||
|
for (i in 0 until childCount - 1) {
|
||||||
|
val child = parent.getChildAt(i)
|
||||||
|
val params = child.layoutParams as RecyclerView.LayoutParams
|
||||||
|
|
||||||
|
val top = child.bottom + params.bottomMargin
|
||||||
|
val bottom = top + divider.intrinsicHeight
|
||||||
|
|
||||||
|
divider.setBounds(left, top, right, bottom)
|
||||||
|
divider.draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawHorizontalDividers(canvas: Canvas, parent: RecyclerView) {
|
||||||
|
val top = parent.paddingTop
|
||||||
|
val bottom = parent.height - parent.paddingBottom
|
||||||
|
|
||||||
|
val childCount = parent.childCount
|
||||||
|
for (i in 0 until childCount - 1) {
|
||||||
|
val child = parent.getChildAt(i)
|
||||||
|
val params = child.layoutParams as RecyclerView.LayoutParams
|
||||||
|
|
||||||
|
val left = child.right + params.rightMargin
|
||||||
|
val right = left + divider.intrinsicWidth
|
||||||
|
|
||||||
|
divider.setBounds(left, top, right, bottom)
|
||||||
|
divider.draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
if (orientation == RecyclerView.VERTICAL) {
|
||||||
|
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
||||||
|
} else {
|
||||||
|
outRect.set(0, 0, divider.intrinsicWidth, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
package com.v2ray.ang.helper
|
package com.v2ray.ang.helper
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.animation.ValueAnimator.AnimatorUpdateListener
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ import android.database.Cursor
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
import com.v2ray.ang.extension.listenForPackageChanges
|
import com.v2ray.ang.extension.listenForPackageChanges
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
|
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@@ -126,7 +126,7 @@ object PluginManager {
|
|||||||
if (providers.size > 1) {
|
if (providers.size > 1) {
|
||||||
val message =
|
val message =
|
||||||
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
|
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
|
||||||
Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show()
|
AngApplication.application.toast(message)
|
||||||
throw IllegalStateException(message)
|
throw IllegalStateException(message)
|
||||||
}
|
}
|
||||||
val provider = providers.single().providerInfo
|
val provider = providers.single().providerInfo
|
||||||
@@ -222,10 +222,10 @@ object PluginManager {
|
|||||||
return File(pluginDir, pluginId).absolutePath
|
return File(pluginDir, pluginId).absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {
|
fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
|
||||||
is String -> value
|
is String -> value
|
||||||
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
|
// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
|
||||||
.getString(value)
|
// .getString(value)
|
||||||
|
|
||||||
null -> null
|
null -> null
|
||||||
else -> error("meta-data $key has invalid type ${value.javaClass}")
|
else -> error("meta-data $key has invalid type ${value.javaClass}")
|
||||||
|
|||||||
@@ -7,12 +7,17 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
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?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
//Check if context is not null and action is the one we want
|
|
||||||
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||||
//Check if flag is true and a server is selected
|
|
||||||
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
||||||
//Start v2ray
|
V2RayServiceManager.startVService(context)
|
||||||
V2RayServiceManager.startV2Ray(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class TaskerReceiver : BroadcastReceiver() {
|
class TaskerReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
||||||
|
* It retrieves the bundle from the intent and checks the switch and guid values.
|
||||||
|
* Depending on the switch value, it starts or stops the V2Ray service.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param intent The Intent being received.
|
||||||
|
*/
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
|
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
|
||||||
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
|
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
|
||||||
@@ -22,13 +27,12 @@ class TaskerReceiver : BroadcastReceiver() {
|
|||||||
return
|
return
|
||||||
} else if (switch) {
|
} else if (switch) {
|
||||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||||
Utils.startVServiceFromToggle(context)
|
V2RayServiceManager.startVServiceFromToggle(context)
|
||||||
} else {
|
} else {
|
||||||
MmkvManager.setSelectServer(guid)
|
V2RayServiceManager.startVService(context, guid)
|
||||||
V2RayServiceManager.startV2Ray(context)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Utils.stopVService(context)
|
V2RayServiceManager.stopVService(context)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -11,18 +11,29 @@ import android.widget.RemoteViews
|
|||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class WidgetProvider : AppWidgetProvider() {
|
class WidgetProvider : AppWidgetProvider() {
|
||||||
/**
|
/**
|
||||||
* 每次窗口小部件被更新都调用一次该方法
|
* This method is called every time the widget is updated.
|
||||||
|
* It updates the widget background based on the V2Ray service running state.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param appWidgetManager The AppWidgetManager instance.
|
||||||
|
* @param appWidgetIds The appWidgetIds for which an update is needed.
|
||||||
*/
|
*/
|
||||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning)
|
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.isRunning())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the widget background based on whether the V2Ray service is running.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param appWidgetManager The AppWidgetManager instance.
|
||||||
|
* @param appWidgetIds The appWidgetIds for which an update is needed.
|
||||||
|
* @param isRunning Boolean indicating if the V2Ray service is running.
|
||||||
|
*/
|
||||||
private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
|
private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
|
||||||
val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
|
val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
|
||||||
val intent = Intent(context, WidgetProvider::class.java)
|
val intent = Intent(context, WidgetProvider::class.java)
|
||||||
@@ -52,15 +63,19 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接收窗口小部件发送的广播
|
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
||||||
|
* It handles widget click actions and updates the widget background based on the V2Ray service state.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param intent The Intent being received.
|
||||||
*/
|
*/
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
super.onReceive(context, intent)
|
super.onReceive(context, intent)
|
||||||
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
|
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
|
||||||
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
if (V2RayServiceManager.isRunning()) {
|
||||||
Utils.stopVService(context)
|
V2RayServiceManager.stopVService(context)
|
||||||
} else {
|
} else {
|
||||||
Utils.startVServiceFromToggle(context)
|
V2RayServiceManager.startVServiceFromToggle(context)
|
||||||
}
|
}
|
||||||
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
|
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
|
||||||
AppWidgetManager.getInstance(context)?.let { manager ->
|
AppWidgetManager.getInstance(context)?.let { manager ->
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,11 @@ import kotlinx.coroutines.launch
|
|||||||
class ProcessService {
|
class ProcessService {
|
||||||
private var process: Process? = null
|
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>) {
|
fun runProcess(context: Context, cmd: MutableList<String>) {
|
||||||
Log.d(ANG_PACKAGE, cmd.toString())
|
Log.d(ANG_PACKAGE, cmd.toString())
|
||||||
|
|
||||||
@@ -33,6 +38,9 @@ class ProcessService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the running process.
|
||||||
|
*/
|
||||||
fun stopProcess() {
|
fun stopProcess() {
|
||||||
try {
|
try {
|
||||||
Log.d(ANG_PACKAGE, "runProcess destroy")
|
Log.d(ANG_PACKAGE, "runProcess destroy")
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.v2ray.ang.service
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -9,6 +8,7 @@ import android.graphics.drawable.Icon
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -16,9 +16,13 @@ import com.v2ray.ang.util.MessageUtil
|
|||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
class QSTileService : TileService() {
|
class QSTileService : TileService() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the state of the tile.
|
||||||
|
* @param state The state to set.
|
||||||
|
*/
|
||||||
fun setState(state: Int) {
|
fun setState(state: Int) {
|
||||||
if (state == Tile.STATE_INACTIVE) {
|
if (state == Tile.STATE_INACTIVE) {
|
||||||
qsTile?.state = Tile.STATE_INACTIVE
|
qsTile?.state = Tile.STATE_INACTIVE
|
||||||
@@ -26,7 +30,7 @@ class QSTileService : TileService() {
|
|||||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||||
} else if (state == Tile.STATE_ACTIVE) {
|
} else if (state == Tile.STATE_ACTIVE) {
|
||||||
qsTile?.state = Tile.STATE_ACTIVE
|
qsTile?.state = Tile.STATE_ACTIVE
|
||||||
qsTile?.label = V2RayServiceManager.currentConfig?.remarks
|
qsTile?.label = V2RayServiceManager.getRunningServerName()
|
||||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +41,6 @@ class QSTileService : TileService() {
|
|||||||
* 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):
|
* 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)`.
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
|
|
||||||
@@ -48,6 +51,9 @@ class QSTileService : TileService() {
|
|||||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the tile stops listening.
|
||||||
|
*/
|
||||||
override fun onStopListening() {
|
override fun onStopListening() {
|
||||||
super.onStopListening()
|
super.onStopListening()
|
||||||
|
|
||||||
@@ -60,15 +66,18 @@ class QSTileService : TileService() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the tile is clicked.
|
||||||
|
*/
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
when (qsTile.state) {
|
when (qsTile.state) {
|
||||||
Tile.STATE_INACTIVE -> {
|
Tile.STATE_INACTIVE -> {
|
||||||
Utils.startVServiceFromToggle(this)
|
V2RayServiceManager.startVServiceFromToggle(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
Tile.STATE_ACTIVE -> {
|
Tile.STATE_ACTIVE -> {
|
||||||
Utils.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,26 @@ package com.v2ray.ang.service
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
|
||||||
interface ServiceControl {
|
interface ServiceControl {
|
||||||
|
/**
|
||||||
|
* Gets the service instance.
|
||||||
|
* @return The service instance.
|
||||||
|
*/
|
||||||
fun getService(): Service
|
fun getService(): Service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the service.
|
||||||
|
*/
|
||||||
fun startService()
|
fun startService()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*/
|
||||||
fun stopService()
|
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
|
fun vpnProtect(socket: Int): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
|
|
||||||
object SubscriptionUpdater {
|
object SubscriptionUpdater {
|
||||||
|
|
||||||
|
|
||||||
class UpdateTask(context: Context, params: WorkerParameters) :
|
class UpdateTask(context: Context, params: WorkerParameters) :
|
||||||
CoroutineWorker(context, params) {
|
CoroutineWorker(context, params) {
|
||||||
|
|
||||||
@@ -33,6 +32,10 @@ object SubscriptionUpdater {
|
|||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the subscription update work.
|
||||||
|
* @return The result of the work.
|
||||||
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "subscription automatic update starting")
|
Log.d(AppConfig.ANG_PACKAGE, "subscription automatic update starting")
|
||||||
|
|||||||
@@ -6,50 +6,87 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
|
|
||||||
class V2RayProxyOnlyService : Service(), ServiceControl {
|
class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||||
|
/**
|
||||||
|
* Initializes the service.
|
||||||
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the start command for the service.
|
||||||
|
* @param intent The intent.
|
||||||
|
* @param flags The flags.
|
||||||
|
* @param startId The start ID.
|
||||||
|
* @return The start mode.
|
||||||
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
V2RayServiceManager.startV2rayPoint()
|
V2RayServiceManager.startV2rayPoint()
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the service.
|
||||||
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
V2RayServiceManager.stopV2rayPoint()
|
V2RayServiceManager.stopV2rayPoint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the service instance.
|
||||||
|
* @return The service instance.
|
||||||
|
*/
|
||||||
override fun getService(): Service {
|
override fun getService(): Service {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the service.
|
||||||
|
*/
|
||||||
override fun startService() {
|
override fun startService() {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*/
|
||||||
override fun stopService() {
|
override fun stopService() {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protects the VPN socket.
|
||||||
|
* @param socket The socket to protect.
|
||||||
|
* @return True if the socket is protected, false otherwise.
|
||||||
|
*/
|
||||||
override fun vpnProtect(socket: Int): Boolean {
|
override fun vpnProtect(socket: Int): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the service.
|
||||||
|
* @param intent The intent.
|
||||||
|
* @return The binder.
|
||||||
|
*/
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the base context to the service.
|
||||||
|
* @param newBase The new base context.
|
||||||
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val context = newBase?.let {
|
val context = newBase?.let {
|
||||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
|
||||||
}
|
}
|
||||||
super.attachBaseContext(context)
|
super.attachBaseContext(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,26 @@
|
|||||||
package com.v2ray.ang.service
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Service
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.toSpeedString
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.ui.MainActivity
|
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.PluginUtil
|
import com.v2ray.ang.util.PluginUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -38,17 +28,12 @@ import libv2ray.Libv2ray
|
|||||||
import libv2ray.V2RayPoint
|
import libv2ray.V2RayPoint
|
||||||
import libv2ray.V2RayVPNServiceSupportsSet
|
import libv2ray.V2RayVPNServiceSupportsSet
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
object V2RayServiceManager {
|
object V2RayServiceManager {
|
||||||
private const val NOTIFICATION_ID = 1
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
|
|
||||||
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
|
||||||
|
|
||||||
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
private val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
private val mMsgReceive = ReceiveMessageHandler()
|
||||||
|
private var currentConfig: ProfileItem? = null
|
||||||
|
|
||||||
var serviceControl: SoftReference<ServiceControl>? = null
|
var serviceControl: SoftReference<ServiceControl>? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -56,18 +41,66 @@ object V2RayServiceManager {
|
|||||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
||||||
}
|
}
|
||||||
var currentConfig: ProfileItem? = null
|
|
||||||
|
|
||||||
private var lastQueryTime = 0L
|
/**
|
||||||
private var mBuilder: NotificationCompat.Builder? = null
|
* Starts the V2Ray service from a toggle action.
|
||||||
private var mDisposable: Disposable? = null
|
* @param context The context from which the service is started.
|
||||||
private var mNotificationManager: NotificationManager? = null
|
* @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
|
||||||
|
}
|
||||||
|
|
||||||
fun startV2Ray(context: Context) {
|
/**
|
||||||
|
* 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() = v2rayPoint.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.
|
||||||
|
* @param context The context from which the service is started.
|
||||||
|
*/
|
||||||
|
private fun startContextService(context: Context) {
|
||||||
if (v2rayPoint.isRunning) return
|
if (v2rayPoint.isRunning) return
|
||||||
val guid = MmkvManager.getSelectServer() ?: return
|
val guid = MmkvManager.getSelectServer() ?: return
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||||
if (!Utils.isValidUrl(config.server) && !Utils.isIpAddress(config.server)) return
|
if (config.configType != EConfigType.CUSTOM
|
||||||
|
&& !Utils.isValidUrl(config.server)
|
||||||
|
&& !Utils.isIpAddress(config.server)
|
||||||
|
) return
|
||||||
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||||
// if (!result.status) return
|
// if (!result.status) return
|
||||||
|
|
||||||
@@ -76,7 +109,7 @@ object V2RayServiceManager {
|
|||||||
} else {
|
} else {
|
||||||
context.toast(R.string.toast_services_start)
|
context.toast(R.string.toast_services_start)
|
||||||
}
|
}
|
||||||
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) {
|
||||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||||
} else {
|
} else {
|
||||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||||
@@ -88,53 +121,13 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class V2RayCallback : V2RayVPNServiceSupportsSet {
|
|
||||||
override fun shutdown(): Long {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
|
||||||
// called by go
|
|
||||||
return try {
|
|
||||||
serviceControl.stopService()
|
|
||||||
0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun prepare(): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun protect(l: Long): Boolean {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return true
|
|
||||||
return serviceControl.vpnProtect(l.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setup(s: String): Long {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
|
||||||
return try {
|
|
||||||
serviceControl.startService()
|
|
||||||
lastQueryTime = System.currentTimeMillis()
|
|
||||||
startSpeedNotification()
|
|
||||||
0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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):
|
* 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)`.
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
|
* Starts the V2Ray point.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startV2rayPoint() {
|
fun startV2rayPoint() {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
val service = getService() ?: return
|
||||||
val guid = MmkvManager.getSelectServer() ?: return
|
val guid = MmkvManager.getSelectServer() ?: return
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||||
if (v2rayPoint.isRunning) {
|
if (v2rayPoint.isRunning) {
|
||||||
@@ -166,17 +159,20 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
if (v2rayPoint.isRunning) {
|
if (v2rayPoint.isRunning) {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||||
showNotification()
|
NotificationService.showNotification(currentConfig)
|
||||||
|
|
||||||
PluginUtil.runPlugin(service, config, result.domainPort)
|
PluginUtil.runPlugin(service, config, result.domainPort)
|
||||||
} else {
|
} else {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||||
cancelNotification()
|
NotificationService.cancelNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the V2Ray point.
|
||||||
|
*/
|
||||||
fun stopV2rayPoint() {
|
fun stopV2rayPoint() {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
val service = getService() ?: return
|
||||||
|
|
||||||
if (v2rayPoint.isRunning) {
|
if (v2rayPoint.isRunning) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
@@ -189,7 +185,7 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
cancelNotification()
|
NotificationService.cancelNotification()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
service.unregisterReceiver(mMsgReceive)
|
service.unregisterReceiver(mMsgReceive)
|
||||||
@@ -199,7 +195,114 @@ object V2RayServiceManager {
|
|||||||
PluginUtil.stopPlugin()
|
PluginUtil.stopPlugin()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 v2rayPoint.queryStats(tag, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the delay for V2Ray.
|
||||||
|
*/
|
||||||
|
private fun measureV2rayDelay() {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val service = getService() ?: return@launch
|
||||||
|
var time = -1L
|
||||||
|
var errstr = ""
|
||||||
|
if (v2rayPoint.isRunning) {
|
||||||
|
try {
|
||||||
|
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
||||||
|
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
|
}
|
||||||
|
if (time == -1L) {
|
||||||
|
try {
|
||||||
|
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
||||||
|
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val result = if (time == -1L) {
|
||||||
|
service.getString(R.string.connection_test_error, errstr)
|
||||||
|
} else {
|
||||||
|
service.getString(R.string.connection_test_available, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current service instance.
|
||||||
|
* @return The current service instance, or null if not available.
|
||||||
|
*/
|
||||||
|
private fun getService(): Service? {
|
||||||
|
return serviceControl?.get()?.getService()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class V2RayCallback : V2RayVPNServiceSupportsSet {
|
||||||
|
override fun shutdown(): Long {
|
||||||
|
val serviceControl = serviceControl?.get() ?: return -1
|
||||||
|
// called by go
|
||||||
|
return try {
|
||||||
|
serviceControl.stopService()
|
||||||
|
0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun prepare(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun protect(l: Long): Boolean {
|
||||||
|
val serviceControl = serviceControl?.get() ?: return true
|
||||||
|
return serviceControl.vpnProtect(l.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by Go to emit status.
|
||||||
|
* @param l The status code.
|
||||||
|
* @param s The status message.
|
||||||
|
* @return The status code.
|
||||||
|
*/
|
||||||
|
override fun onEmitStatus(l: Long, s: String?): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by Go to set up the service.
|
||||||
|
* @param s The setup string.
|
||||||
|
* @return The status code.
|
||||||
|
*/
|
||||||
|
override fun setup(s: String): Long {
|
||||||
|
val serviceControl = serviceControl?.get() ?: return -1
|
||||||
|
return try {
|
||||||
|
serviceControl.startService()
|
||||||
|
NotificationService.startSpeedNotification(currentConfig)
|
||||||
|
0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class ReceiveMessageHandler : BroadcastReceiver() {
|
private class ReceiveMessageHandler : BroadcastReceiver() {
|
||||||
|
/**
|
||||||
|
* Handles received broadcast messages.
|
||||||
|
* @param ctx The context in which the receiver is running.
|
||||||
|
* @param intent The intent being received.
|
||||||
|
*/
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||||
val serviceControl = serviceControl?.get() ?: return
|
val serviceControl = serviceControl?.get() ?: return
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
@@ -228,7 +331,7 @@ object V2RayServiceManager {
|
|||||||
Log.d(ANG_PACKAGE, "Restart Service")
|
Log.d(ANG_PACKAGE, "Restart Service")
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
Thread.sleep(500L)
|
Thread.sleep(500L)
|
||||||
startV2Ray(serviceControl.getService())
|
startVService(serviceControl.getService())
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY -> {
|
AppConfig.MSG_MEASURE_DELAY -> {
|
||||||
@@ -239,207 +342,14 @@ object V2RayServiceManager {
|
|||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
Intent.ACTION_SCREEN_OFF -> {
|
Intent.ACTION_SCREEN_OFF -> {
|
||||||
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
||||||
stopSpeedNotification()
|
NotificationService.stopSpeedNotification(currentConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent.ACTION_SCREEN_ON -> {
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
||||||
startSpeedNotification()
|
NotificationService.startSpeedNotification(currentConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun measureV2rayDelay() {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val service = serviceControl?.get()?.getService() ?: return@launch
|
|
||||||
var time = -1L
|
|
||||||
var errstr = ""
|
|
||||||
if (v2rayPoint.isRunning) {
|
|
||||||
try {
|
|
||||||
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
|
||||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
|
||||||
}
|
|
||||||
if (time == -1L) {
|
|
||||||
try {
|
|
||||||
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(true))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
|
||||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val result = if (time == -1L) {
|
|
||||||
service.getString(R.string.connection_test_error, errstr)
|
|
||||||
} else {
|
|
||||||
service.getString(R.string.connection_test_available, time)
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNotification() {
|
|
||||||
val service = serviceControl?.get()?.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
|
|
||||||
)
|
|
||||||
//.build()
|
|
||||||
|
|
||||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
|
||||||
|
|
||||||
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private fun createNotificationChannel(): String {
|
|
||||||
val channelId = 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
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelNotification() {
|
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
|
||||||
service.stopForeground(true)
|
|
||||||
mBuilder = null
|
|
||||||
mDisposable?.dispose()
|
|
||||||
mDisposable = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
|
||||||
if (mBuilder != null) {
|
|
||||||
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_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) // Emui4.1 need content text even if style is set as BigTextStyle
|
|
||||||
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNotificationManager(): NotificationManager? {
|
|
||||||
if (mNotificationManager == null) {
|
|
||||||
val service = serviceControl?.get()?.getService() ?: return null
|
|
||||||
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
}
|
|
||||||
return mNotificationManager
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startSpeedNotification() {
|
|
||||||
if (mDisposable == null &&
|
|
||||||
v2rayPoint.isRunning &&
|
|
||||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
|
|
||||||
) {
|
|
||||||
var lastZeroSpeed = false
|
|
||||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
|
||||||
outboundTags?.remove(TAG_DIRECT)
|
|
||||||
|
|
||||||
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.subscribe {
|
|
||||||
val queryTime = System.currentTimeMillis()
|
|
||||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
|
||||||
var proxyTotal = 0L
|
|
||||||
val text = StringBuilder()
|
|
||||||
outboundTags?.forEach {
|
|
||||||
val up = v2rayPoint.queryStats(it, AppConfig.UPLINK)
|
|
||||||
val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK)
|
|
||||||
if (up + down > 0) {
|
|
||||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
|
||||||
proxyTotal += up + down
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
|
||||||
val directDownlink = v2rayPoint.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopSpeedNotification() {
|
|
||||||
mDisposable?.let {
|
|
||||||
it.dispose() //stop queryStats
|
|
||||||
mDisposable = null
|
|
||||||
updateNotification(currentConfig?.remarks, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
|
|||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.extension.serializable
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.PluginUtil
|
import com.v2ray.ang.util.PluginUtil
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -26,12 +26,22 @@ import java.util.concurrent.Executors
|
|||||||
class V2RayTestService : Service() {
|
class V2RayTestService : Service() {
|
||||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
|
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the V2Ray environment.
|
||||||
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Seq.setContext(this)
|
Seq.setContext(this)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
Libv2ray.initV2Env(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 {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
MSG_MEASURE_CONFIG -> {
|
MSG_MEASURE_CONFIG -> {
|
||||||
@@ -49,10 +59,20 @@ class V2RayTestService : Service() {
|
|||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the service.
|
||||||
|
* @param intent The intent.
|
||||||
|
* @return The binder.
|
||||||
|
*/
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the real ping test.
|
||||||
|
* @param guid The GUID of the configuration.
|
||||||
|
* @return The ping result.
|
||||||
|
*/
|
||||||
private fun startRealPing(guid: String): Long {
|
private fun startRealPing(guid: String): Long {
|
||||||
val retFailure = -1L
|
val retFailure = -1L
|
||||||
|
|
||||||
@@ -65,7 +85,7 @@ class V2RayTestService : Service() {
|
|||||||
if (!config.status) {
|
if (!config.status) {
|
||||||
return retFailure
|
return retFailure
|
||||||
}
|
}
|
||||||
return SpeedtestUtil.realPing(config.content)
|
return SpeedtestManager.realPing(config.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,18 +35,15 @@ import java.lang.ref.SoftReference
|
|||||||
class V2RayVpnService : VpnService(), ServiceControl {
|
class V2RayVpnService : VpnService(), ServiceControl {
|
||||||
companion object {
|
companion object {
|
||||||
private const val VPN_MTU = 1500
|
private const val VPN_MTU = 1500
|
||||||
private const val PRIVATE_VLAN4_CLIENT = "10.10.10.1"
|
private const val PRIVATE_VLAN4_CLIENT = "10.10.14.1"
|
||||||
private const val PRIVATE_VLAN4_ROUTER = "10.10.10.2"
|
private const val PRIVATE_VLAN4_ROUTER = "10.10.14.2"
|
||||||
private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:10:1"
|
private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:14:1"
|
||||||
private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:10:2"
|
private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:14:2"
|
||||||
private const val TUN2SOCKS = "libtun2socks.so"
|
private const val TUN2SOCKS = "libtun2socks.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private lateinit var mInterface: ParcelFileDescriptor
|
private lateinit var mInterface: ParcelFileDescriptor
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
|
||||||
//val fd: Int get() = mInterface.fd
|
|
||||||
private lateinit var process: Process
|
private lateinit var process: Process
|
||||||
|
|
||||||
/**destroy
|
/**destroy
|
||||||
@@ -88,7 +85,6 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||||
StrictMode.setThreadPolicy(policy)
|
StrictMode.setThreadPolicy(policy)
|
||||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
@@ -105,15 +101,61 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
V2RayServiceManager.cancelNotification()
|
NotificationService.cancelNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
V2RayServiceManager.startV2rayPoint()
|
||||||
|
return START_STICKY
|
||||||
|
//return super.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getService(): Service {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startService() {
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopService() {
|
||||||
|
stopV2Ray(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun vpnProtect(socket: Int): Boolean {
|
||||||
|
return protect(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
val context = newBase?.let {
|
||||||
|
MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
|
||||||
|
}
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the VPN service.
|
||||||
|
* Prepares the VPN and configures it if preparation is successful.
|
||||||
|
*/
|
||||||
private fun setup() {
|
private fun setup() {
|
||||||
val prepare = prepare(this)
|
val prepare = prepare(this)
|
||||||
if (prepare != null) {
|
if (prepare != null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setupVpnService() != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runTun2socks()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the VPN service.
|
||||||
|
* @return True if the VPN service was configured successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun setupVpnService(): Boolean {
|
||||||
// If the old interface has exactly the same parameters, use it!
|
// If the old interface has exactly the same parameters, use it!
|
||||||
// Configure a builder while parsing the parameters.
|
// Configure a builder while parsing the parameters.
|
||||||
val builder = Builder()
|
val builder = Builder()
|
||||||
@@ -144,7 +186,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||||
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
// } else {
|
// } else {
|
||||||
Utils.getVpnDnsServers()
|
SettingsManager.getVpnDnsServers()
|
||||||
.forEach {
|
.forEach {
|
||||||
if (Utils.isPureIpAddress(it)) {
|
if (Utils.isPureIpAddress(it)) {
|
||||||
builder.addDnsServer(it)
|
builder.addDnsServer(it)
|
||||||
@@ -152,7 +194,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
|
|
||||||
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
builder.setSession(V2RayServiceManager.getRunningServerName())
|
||||||
|
|
||||||
val selfPackageName = BuildConfig.APPLICATION_ID
|
val selfPackageName = BuildConfig.APPLICATION_ID
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
|
||||||
@@ -200,14 +242,19 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
try {
|
try {
|
||||||
mInterface = builder.establish()!!
|
mInterface = builder.establish()!!
|
||||||
isRunning = true
|
isRunning = true
|
||||||
runTun2socks()
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// non-nullable lateinit var
|
// non-nullable lateinit var
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
stopV2Ray()
|
stopV2Ray()
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the tun2socks process.
|
||||||
|
* Starts the tun2socks process with the appropriate parameters.
|
||||||
|
*/
|
||||||
private fun runTun2socks() {
|
private fun runTun2socks() {
|
||||||
val socksPort = SettingsManager.getSocksPort()
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
val cmd = arrayListOf(
|
val cmd = arrayListOf(
|
||||||
@@ -255,6 +302,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the file descriptor to the tun2socks process.
|
||||||
|
* Attempts to send the file descriptor multiple times if necessary.
|
||||||
|
*/
|
||||||
private fun sendFd() {
|
private fun sendFd() {
|
||||||
val fd = mInterface.fileDescriptor
|
val fd = mInterface.fileDescriptor
|
||||||
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||||
@@ -279,12 +330,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
/**
|
||||||
V2RayServiceManager.startV2rayPoint()
|
* Stops the V2Ray service.
|
||||||
return START_STICKY
|
* @param isForced Whether to force stop the service.
|
||||||
//return super.onStartCommand(intent, flags, startId)
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopV2Ray(isForced: Boolean = true) {
|
private fun stopV2Ray(isForced: Boolean = true) {
|
||||||
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
|
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
|
||||||
// val emptyInfo = VpnNetworkInfo()
|
// val emptyInfo = VpnNetworkInfo()
|
||||||
@@ -323,28 +372,4 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getService(): Service {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun startService() {
|
|
||||||
setup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopService() {
|
|
||||||
stopV2Ray(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun vpnProtect(socket: Int): Boolean {
|
|
||||||
return protect(socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
|
||||||
val context = newBase?.let {
|
|
||||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
|
||||||
}
|
|
||||||
super.attachBaseContext(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,27 +6,41 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityAboutBinding
|
import com.v2ray.ang.databinding.ActivityAboutBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.ZipUtil
|
import com.v2ray.ang.util.ZipUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
|
||||||
|
|
||||||
|
|
||||||
class AboutActivity : BaseActivity() {
|
class AboutActivity : BaseActivity() {
|
||||||
|
|
||||||
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
|
||||||
private val extDir by lazy { File(Utils.backupPath(this)) }
|
private val extDir by lazy { File(Utils.backupPath(this)) }
|
||||||
|
|
||||||
|
private val requestPermissionLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||||
|
if (isGranted) {
|
||||||
|
try {
|
||||||
|
showFileChooser()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -34,12 +48,13 @@ class AboutActivity : BaseActivity() {
|
|||||||
title = getString(R.string.title_about)
|
title = getString(R.string.title_about)
|
||||||
|
|
||||||
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
|
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
|
||||||
|
|
||||||
binding.layoutBackup.setOnClickListener {
|
binding.layoutBackup.setOnClickListener {
|
||||||
val ret = backupConfiguration(extDir.absolutePath)
|
val ret = backupConfiguration(extDir.absolutePath)
|
||||||
if (ret.first) {
|
if (ret.first) {
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,35 +66,35 @@ class AboutActivity : BaseActivity() {
|
|||||||
Intent(Intent.ACTION_SEND).setType("application/zip")
|
Intent(Intent.ACTION_SEND).setType("application/zip")
|
||||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.putExtra(
|
.putExtra(
|
||||||
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
|
Intent.EXTRA_STREAM,
|
||||||
|
FileProvider.getUriForFile(
|
||||||
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
|
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
|
||||||
)
|
)
|
||||||
), getString(R.string.title_configuration_share)
|
), getString(R.string.title_configuration_share)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutRestore.setOnClickListener {
|
binding.layoutRestore.setOnClickListener {
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val permission =
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
} else {
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
} else {
|
||||||
}
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
RxPermissions(this)
|
|
||||||
.request(permission)
|
|
||||||
.subscribe {
|
|
||||||
if (it) {
|
|
||||||
try {
|
|
||||||
showFileChooser()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||||
|
try {
|
||||||
|
showFileChooser()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestPermissionLauncher.launch(permission)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutSoureCcode.setOnClickListener {
|
binding.layoutSoureCcode.setOnClickListener {
|
||||||
@@ -89,8 +104,15 @@ class AboutActivity : BaseActivity() {
|
|||||||
binding.layoutFeedback.setOnClickListener {
|
binding.layoutFeedback.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
||||||
}
|
}
|
||||||
binding.layoutOssLicenses.setOnClickListener{
|
|
||||||
startActivity(Intent(this, OssLicensesMenuActivity::class.java))
|
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 {
|
binding.layoutTgChannel.setOnClickListener {
|
||||||
@@ -101,12 +123,12 @@ class AboutActivity : BaseActivity() {
|
|||||||
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
|
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
|
||||||
}
|
}
|
||||||
|
|
||||||
"v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})".also {
|
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
||||||
binding.tvVersion.text = it
|
binding.tvVersion.text = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
|
private fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
|
||||||
val dateFormated = SimpleDateFormat(
|
val dateFormated = SimpleDateFormat(
|
||||||
"yyyy-MM-dd-HH-mm-ss",
|
"yyyy-MM-dd-HH-mm-ss",
|
||||||
Locale.getDefault()
|
Locale.getDefault()
|
||||||
@@ -127,7 +149,7 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreConfiguration(zipFile: File): Boolean {
|
private fun restoreConfiguration(zipFile: File): Boolean {
|
||||||
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
|
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
|
||||||
|
|
||||||
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
|
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
|
||||||
@@ -153,9 +175,9 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val chooseFile =
|
private val chooseFile =
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val uri = it.data?.data
|
val uri = result.data?.data
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
if (result.resultCode == RESULT_OK && uri != null) {
|
||||||
try {
|
try {
|
||||||
val targetFile =
|
val targetFile =
|
||||||
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
||||||
@@ -165,15 +187,14 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (restoreConfiguration(targetFile)) {
|
if (restoreConfiguration(targetFile)) {
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
|
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,16 @@ import android.os.Bundle
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
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.MyContextWrapper
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -34,6 +40,26 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, Utils.getLocale()))
|
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,27 +1,30 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.text.method.ScrollingMovementMethod
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class LogcatActivity : BaseActivity() {
|
|
||||||
private val binding by lazy {
|
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
ActivityLogcatBinding.inflate(layoutInflater)
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -29,63 +32,123 @@ class LogcatActivity : BaseActivity() {
|
|||||||
|
|
||||||
title = getString(R.string.title_logcat)
|
title = getString(R.string.title_logcat)
|
||||||
|
|
||||||
logcat(false)
|
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 logcat(shouldFlushLog: Boolean) {
|
private fun getLogcat() {
|
||||||
binding.pbWaiting.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
try {
|
||||||
try {
|
binding.refreshLayout.isRefreshing = true
|
||||||
if (shouldFlushLog) {
|
|
||||||
val lst = linkedSetOf("logcat", "-c")
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
withContext(Dispatchers.IO) {
|
val lst = LinkedHashSet<String>()
|
||||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
lst.add("logcat")
|
||||||
process.waitFor()
|
lst.add("-d")
|
||||||
}
|
lst.add("-v")
|
||||||
}
|
lst.add("time")
|
||||||
val lst = linkedSetOf(
|
lst.add("-s")
|
||||||
"logcat", "-d", "-v", "time", "-s",
|
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||||
"GoLog,tun2socks,$ANG_PACKAGE,AndroidRuntime,System.err"
|
|
||||||
)
|
|
||||||
val process = withContext(Dispatchers.IO) {
|
val process = withContext(Dispatchers.IO) {
|
||||||
Runtime.getRuntime().exec(lst.toTypedArray())
|
Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
}
|
}
|
||||||
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
|
||||||
withContext(Dispatchers.Main) {
|
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
|
||||||
binding.tvLogcat.text = allText
|
launch(Dispatchers.Main) {
|
||||||
binding.tvLogcat.movementMethod = ScrollingMovementMethod()
|
logsetsAll = allText.toMutableList()
|
||||||
binding.pbWaiting.visibility = View.GONE
|
logsets = allText.toMutableList()
|
||||||
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
|
refreshData()
|
||||||
|
binding.refreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
binding.pbWaiting.visibility = View.GONE
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
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)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.copy_all -> {
|
R.id.copy_all -> {
|
||||||
Utils.setClipboard(this, binding.tvLogcat.text.toString())
|
Utils.setClipboard(this, logsets.joinToString("\n"))
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.clear_all -> {
|
R.id.clear_all -> {
|
||||||
logcat(true)
|
clearLogcat()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,18 +1,17 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ActivityNotFoundException
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@@ -23,17 +22,17 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
import com.v2ray.ang.AppConfig.VPN
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MigrateManager
|
import com.v2ray.ang.handler.MigrateManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
@@ -41,14 +40,10 @@ import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
|||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.drakeet.support.toast.ToastCompat
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||||
private val binding by lazy {
|
private val binding by lazy {
|
||||||
@@ -81,6 +76,64 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
private var mItemTouchHelper: ItemTouchHelper? = null
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
val mainViewModel: MainViewModel by viewModels()
|
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.IMPORT_QR_CODE_URL ->
|
||||||
|
// scanQRCodeForUrlToCustomConfig.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,
|
||||||
|
|
||||||
|
//IMPORT_QR_CODE_URL,
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
// if (it.resultCode == RESULT_OK) {
|
||||||
|
// importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -89,7 +142,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
|
|
||||||
binding.fab.setOnClickListener {
|
binding.fab.setOnClickListener {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
Utils.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
||||||
val intent = VpnService.prepare(this)
|
val intent = VpnService.prepare(this)
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
@@ -111,7 +164,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
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
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
@@ -129,12 +187,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
migrateLegacy()
|
migrateLegacy()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
RxPermissions(this)
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
pendingAction = Action.POST_NOTIFICATIONS
|
||||||
.subscribe {
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
if (!it)
|
}
|
||||||
toast(R.string.toast_permission_denied_notification)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
@@ -142,13 +198,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
} else {
|
} else {
|
||||||
//super.onBackPressed()
|
isEnabled = false
|
||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
isEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun setupViewModel() {
|
private fun setupViewModel() {
|
||||||
mainViewModel.updateListAction.observe(this) { index ->
|
mainViewModel.updateListAction.observe(this) { index ->
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -214,23 +272,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
binding.tabGroup.isVisible = true
|
binding.tabGroup.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startV2Ray() {
|
private fun startV2Ray() {
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
toast(R.string.title_file_chooser)
|
toast(R.string.title_file_chooser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
V2RayServiceManager.startV2Ray(this)
|
V2RayServiceManager.startVService(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartV2Ray() {
|
private fun restartV2Ray() {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
Utils.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
delay(500)
|
||||||
|
startV2Ray()
|
||||||
}
|
}
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
startV2Ray()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onResume() {
|
public override fun onResume() {
|
||||||
@@ -276,6 +333,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.import_local -> {
|
||||||
|
importConfigLocal()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.import_manually_vmess -> {
|
R.id.import_manually_vmess -> {
|
||||||
importManually(EConfigType.VMESS.value)
|
importManually(EConfigType.VMESS.value)
|
||||||
true
|
true
|
||||||
@@ -316,44 +378,28 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.import_config_custom_clipboard -> {
|
// R.id.import_config_custom_clipboard -> {
|
||||||
importConfigCustomClipboard()
|
// importConfigCustomClipboard()
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
R.id.import_config_custom_local -> {
|
// R.id.import_config_custom_local -> {
|
||||||
importConfigCustomLocal()
|
// importConfigCustomLocal()
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
R.id.import_config_custom_url -> {
|
// R.id.import_config_custom_url -> {
|
||||||
importConfigCustomUrlClipboard()
|
// importConfigCustomUrlClipboard()
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
R.id.import_config_custom_url_scan -> {
|
// R.id.import_config_custom_url_scan -> {
|
||||||
importQRcode(false)
|
// importQRcode(false)
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
|
|
||||||
R.id.sub_update -> {
|
|
||||||
importConfigViaSub()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.export_all -> {
|
R.id.export_all -> {
|
||||||
binding.pbWaiting.show()
|
exportAll()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.exportAllServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (ret > 0)
|
|
||||||
toast(getString(R.string.title_export_config_count, ret))
|
|
||||||
else
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,77 +421,31 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.del_all_config -> {
|
R.id.del_all_config -> {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
delAllConfig()
|
||||||
.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.no) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.del_duplicate_config -> {
|
R.id.del_duplicate_config -> {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
delDuplicateConfig()
|
||||||
.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.no) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.del_invalid_config -> {
|
R.id.del_invalid_config -> {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
|
delInvalidConfig()
|
||||||
.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.no) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.sort_by_test_results -> {
|
R.id.sort_by_test_results -> {
|
||||||
binding.pbWaiting.show()
|
sortByTestResults()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
mainViewModel.sortByTestResults()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.sub_update -> {
|
||||||
|
importConfigViaSub()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,38 +462,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
* import config from qrcode
|
* import config from qrcode
|
||||||
*/
|
*/
|
||||||
private fun importQRcode(forConfig: Boolean): Boolean {
|
private fun importQRcode(forConfig: Boolean): Boolean {
|
||||||
// try {
|
val permission = Manifest.permission.CAMERA
|
||||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
if (forConfig) {
|
||||||
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
|
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
// } catch (e: Exception) {
|
} else {
|
||||||
RxPermissions(this)
|
//scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe {
|
|
||||||
if (it)
|
|
||||||
if (forConfig)
|
|
||||||
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
else
|
|
||||||
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
}
|
||||||
// }
|
} else {
|
||||||
|
pendingAction = Action.IMPORT_QR_CODE_CONFIG//if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL
|
||||||
|
requestPermissionLauncher.launch(permission)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* import config from clipboard
|
* import config from clipboard
|
||||||
*/
|
*/
|
||||||
@@ -524,13 +506,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
countSub > 0 -> initGroupTab()
|
countSub > 0 -> initGroupTab()
|
||||||
else -> toast(R.string.toast_failure)
|
else -> toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -538,27 +520,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun importConfigLocal(): Boolean {
|
||||||
private fun importConfigCustomClipboard()
|
|
||||||
: Boolean {
|
|
||||||
try {
|
|
||||||
val configText = Utils.getClipboard(this)
|
|
||||||
if (TextUtils.isEmpty(configText)) {
|
|
||||||
toast(R.string.toast_none_data_clipboard)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
importCustomizeConfig(configText)
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* import config from local config file
|
|
||||||
*/
|
|
||||||
private fun importConfigCustomLocal(): Boolean {
|
|
||||||
try {
|
try {
|
||||||
showFileChooser()
|
showFileChooser()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -568,56 +530,82 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun importConfigCustomUrlClipboard()
|
|
||||||
: Boolean {
|
// private fun importConfigCustomClipboard()
|
||||||
try {
|
// : Boolean {
|
||||||
val url = Utils.getClipboard(this)
|
// try {
|
||||||
if (TextUtils.isEmpty(url)) {
|
// val configText = Utils.getClipboard(this)
|
||||||
toast(R.string.toast_none_data_clipboard)
|
// if (TextUtils.isEmpty(configText)) {
|
||||||
return false
|
// toast(R.string.toast_none_data_clipboard)
|
||||||
}
|
// return false
|
||||||
return importConfigCustomUrl(url)
|
// }
|
||||||
} catch (e: Exception) {
|
// importCustomizeConfig(configText)
|
||||||
e.printStackTrace()
|
// return true
|
||||||
return false
|
// } catch (e: Exception) {
|
||||||
}
|
// e.printStackTrace()
|
||||||
}
|
// return false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* import config from local config file
|
||||||
|
*/
|
||||||
|
// private fun importConfigCustomLocal(): Boolean {
|
||||||
|
// try {
|
||||||
|
// showFileChooser()
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// e.printStackTrace()
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private fun importConfigCustomUrlClipboard()
|
||||||
|
// : Boolean {
|
||||||
|
// try {
|
||||||
|
// val url = Utils.getClipboard(this)
|
||||||
|
// if (TextUtils.isEmpty(url)) {
|
||||||
|
// toast(R.string.toast_none_data_clipboard)
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// return importConfigCustomUrl(url)
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// e.printStackTrace()
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* import config from url
|
* import config from url
|
||||||
*/
|
*/
|
||||||
private fun importConfigCustomUrl(url: String?): Boolean {
|
// private fun importConfigCustomUrl(url: String?): Boolean {
|
||||||
try {
|
// try {
|
||||||
if (!Utils.isValidUrl(url)) {
|
// if (!Utils.isValidUrl(url)) {
|
||||||
toast(R.string.toast_invalid_url)
|
// toast(R.string.toast_invalid_url)
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
// lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val configText = try {
|
// val configText = try {
|
||||||
Utils.getUrlContentWithCustomUserAgent(url)
|
// HttpUtil.getUrlContentWithUserAgent(url)
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
e.printStackTrace()
|
// e.printStackTrace()
|
||||||
""
|
// ""
|
||||||
}
|
// }
|
||||||
launch(Dispatchers.Main) {
|
// launch(Dispatchers.Main) {
|
||||||
importCustomizeConfig(configText)
|
// importCustomizeConfig(configText)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
e.printStackTrace()
|
// e.printStackTrace()
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
return true
|
// return true
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* import config from sub
|
* import config from sub
|
||||||
*/
|
*/
|
||||||
private fun importConfigViaSub(): Boolean {
|
private fun importConfigViaSub(): Boolean {
|
||||||
// val dialog = AlertDialog.Builder(this)
|
|
||||||
// .setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
|
||||||
// .setCancelable(false)
|
|
||||||
// .show()
|
|
||||||
binding.pbWaiting.show()
|
binding.pbWaiting.show()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@@ -628,15 +616,96 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
toast(getString(R.string.title_update_config_count, count))
|
toast(getString(R.string.title_update_config_count, count))
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
//dialog.dismiss()
|
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
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
|
* show file chooser
|
||||||
*/
|
*/
|
||||||
@@ -645,17 +714,17 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
intent.type = "*/*"
|
intent.type = "*/*"
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
try {
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
} catch (ex: ActivityNotFoundException) {
|
} else {
|
||||||
toast(R.string.toast_require_file_manager)
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
val uri = it.data?.data
|
pendingAction = Action.READ_CONTENT_FROM_URI
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||||
readContentFromUri(uri)
|
} else {
|
||||||
|
requestPermissionLauncher.launch(permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,45 +737,43 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
|
||||||
.request(permission)
|
|
||||||
.subscribe {
|
|
||||||
if (it) {
|
|
||||||
try {
|
|
||||||
contentResolver.openInputStream(uri).use { input ->
|
|
||||||
importCustomizeConfig(input?.bufferedReader()?.readText())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
* import customize config
|
try {
|
||||||
*/
|
contentResolver.openInputStream(uri).use { input ->
|
||||||
private fun importCustomizeConfig(server: String?) {
|
importBatchConfig(input?.bufferedReader()?.readText())
|
||||||
try {
|
}
|
||||||
if (server == null || TextUtils.isEmpty(server)) {
|
} catch (e: Exception) {
|
||||||
toast(R.string.toast_none_data)
|
e.printStackTrace()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (mainViewModel.appendCustomConfigServer(server)) {
|
} else {
|
||||||
mainViewModel.reloadServerList()
|
requestPermissionLauncher.launch(permission)
|
||||||
toast(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
|
||||||
e.printStackTrace()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * import customize config
|
||||||
|
// */
|
||||||
|
// private fun importCustomizeConfig(server: String?) {
|
||||||
|
// try {
|
||||||
|
// if (server == null || TextUtils.isEmpty(server)) {
|
||||||
|
// toast(R.string.toast_none_data)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if (mainViewModel.appendCustomConfigServer(server)) {
|
||||||
|
// mainViewModel.reloadServerList()
|
||||||
|
// toastSuccess(R.string.toast_success)
|
||||||
|
// } else {
|
||||||
|
// toastError(R.string.toast_failure)
|
||||||
|
// }
|
||||||
|
// //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
||||||
|
// e.printStackTrace()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
private fun setTestState(content: String?) {
|
private fun setTestState(content: String?) {
|
||||||
binding.tvTestState.text = content
|
binding.tvTestState.text = content
|
||||||
}
|
}
|
||||||
@@ -732,35 +799,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||||
// Handle navigation view item clicks here.
|
// Handle navigation view item clicks here.
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.sub_setting -> {
|
R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
|
||||||
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 -> {
|
R.id.settings -> startActivity(
|
||||||
startActivity(
|
Intent(this, SettingsActivity::class.java)
|
||||||
Intent(this, SettingsActivity::class.java)
|
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
||||||
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
)
|
||||||
)
|
R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||||
}
|
R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
|
||||||
|
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
|
||||||
R.id.routing_setting -> {
|
|
||||||
requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
R.id.promotion -> {
|
|
||||||
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.logcat -> {
|
|
||||||
startActivity(Intent(this, LogcatActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.about -> {
|
|
||||||
startActivity(Intent(this, AboutActivity::class.java))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AngApplication.Companion.application
|
import com.v2ray.ang.AngApplication.Companion.application
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
@@ -16,16 +17,17 @@ import com.v2ray.ang.databinding.ItemQrcodeBinding
|
|||||||
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.toast
|
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.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import kotlinx.coroutines.delay
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.launch
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -37,169 +39,273 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
private val share_method: Array<out String> by lazy {
|
private val share_method: Array<out String> by lazy {
|
||||||
mActivity.resources.getStringArray(R.array.share_method)
|
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
|
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 getItemCount() = mActivity.mainViewModel.serversCache.size + 1
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||||
if (holder is MainViewHolder) {
|
if (holder is MainViewHolder) {
|
||||||
val guid = mActivity.mainViewModel.serversCache[position].guid
|
val guid = mActivity.mainViewModel.serversCache[position].guid
|
||||||
val profile = mActivity.mainViewModel.serversCache[position].profile
|
val profile = mActivity.mainViewModel.serversCache[position].profile
|
||||||
// //filter
|
val isCustom = profile.configType == EConfigType.CUSTOM
|
||||||
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
|
|
||||||
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
|
|
||||||
// ) {
|
|
||||||
// holder.itemMainBinding.cardView.visibility = View.GONE
|
|
||||||
// } else {
|
|
||||||
// holder.itemMainBinding.cardView.visibility = View.VISIBLE
|
|
||||||
// }
|
|
||||||
|
|
||||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
|
||||||
|
|
||||||
holder.itemMainBinding.tvName.text = profile.remarks
|
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
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()
|
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
||||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//layoutIndicator
|
||||||
if (guid == MmkvManager.getSelectServer()) {
|
if (guid == MmkvManager.getSelectServer()) {
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
|
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||||
}
|
}
|
||||||
holder.itemMainBinding.tvSubscription.text = MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks ?: ""
|
|
||||||
|
|
||||||
var shareOptions = share_method.asList()
|
//subscription remarks
|
||||||
when (profile.configType) {
|
val subRemarks = getSubscriptionRemarks(profile)
|
||||||
EConfigType.CUSTOM -> {
|
holder.itemMainBinding.tvSubscription.text = subRemarks
|
||||||
holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
|
holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE
|
||||||
shareOptions = shareOptions.takeLast(1)
|
|
||||||
|
//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)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
||||||
holder.itemMainBinding.tvType.text = profile.configType.name
|
editServer(guid, profile)
|
||||||
}
|
}
|
||||||
}
|
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
||||||
|
removeServer(guid, position)
|
||||||
// 隐藏主页服务器地址为xxx:xxx:***/xxx.xxx.xxx.***
|
|
||||||
val strState = "${
|
|
||||||
profile.server?.let {
|
|
||||||
if (it.contains(":"))
|
|
||||||
it.split(":").take(2).joinToString(":", postfix = ":***")
|
|
||||||
else
|
|
||||||
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
|
|
||||||
}
|
|
||||||
} : ${profile.serverPort}"
|
|
||||||
|
|
||||||
holder.itemMainBinding.tvStatistics.text = strState
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
|
||||||
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
|
||||||
try {
|
|
||||||
when (i) {
|
|
||||||
0 -> {
|
|
||||||
if (profile.configType == EConfigType.CUSTOM) {
|
|
||||||
shareFullContent(guid)
|
|
||||||
} else {
|
|
||||||
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
|
||||||
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
|
||||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
|
|
||||||
mActivity.toast(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
mActivity.toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
2 -> shareFullContent(guid)
|
|
||||||
else -> mActivity.toast("else")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
|
||||||
val intent = Intent().putExtra("guid", guid)
|
|
||||||
.putExtra("isRunning", isRunning)
|
|
||||||
.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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
|
||||||
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) { _, _ ->
|
|
||||||
removeServer(guid, position)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
removeServer(guid, position)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
application.toast(R.string.toast_action_not_allowed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||||
val selected = MmkvManager.getSelectServer()
|
setSelectServer(guid)
|
||||||
if (guid != selected) {
|
|
||||||
MmkvManager.setSelectServer(guid)
|
|
||||||
if (!TextUtils.isEmpty(selected)) {
|
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
|
|
||||||
}
|
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
|
||||||
if (isRunning) {
|
|
||||||
Utils.stopVService(mActivity)
|
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
V2RayServiceManager.startV2Ray(mActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (holder is FooterViewHolder) {
|
// if (holder is FooterViewHolder) {
|
||||||
//if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) {
|
// if (true) {
|
||||||
if (true) {
|
// holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
||||||
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
// } else {
|
||||||
} else {
|
// holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
||||||
holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
// Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||||
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}.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) {
|
private fun shareFullContent(guid: String) {
|
||||||
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
|
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
|
||||||
mActivity.toast(R.string.toast_success)
|
mActivity.toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
mActivity.toast(R.string.toast_failure)
|
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) {
|
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)
|
mActivity.mainViewModel.removeServer(guid)
|
||||||
notifyItemRemoved(position)
|
notifyItemRemoved(position)
|
||||||
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
|
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
VIEW_TYPE_ITEM ->
|
VIEW_TYPE_ITEM ->
|
||||||
@@ -246,4 +352,4 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityBypassListBinding
|
import com.v2ray.ang.databinding.ActivityBypassListBinding
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.extension.v2RayApplication
|
import com.v2ray.ang.extension.v2RayApplication
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
|
||||||
class PerAppProxyActivity : BaseActivity() {
|
class PerAppProxyActivity : BaseActivity() {
|
||||||
@@ -38,98 +38,43 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
title = getString(R.string.per_app_proxy_settings)
|
||||||
binding.recyclerView.addItemDecoration(dividerItemDecoration)
|
|
||||||
|
|
||||||
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
|
|
||||||
AppManagerUtil.rxLoadNetworkAppList(this)
|
lifecycleScope.launch {
|
||||||
.subscribeOn(Schedulers.io())
|
try {
|
||||||
.map {
|
binding.pbWaiting.show()
|
||||||
if (blacklist != null) {
|
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
it.forEach { one ->
|
val apps = withContext(Dispatchers.IO) {
|
||||||
if (blacklist.contains(one.packageName)) {
|
val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
|
||||||
one.isSelected = 1
|
|
||||||
} else {
|
if (blacklist != null) {
|
||||||
one.isSelected = 0
|
appsList.forEach { app ->
|
||||||
|
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
|
||||||
}
|
}
|
||||||
}
|
appsList.sortedWith { p1, p2 ->
|
||||||
val comparator = Comparator<AppInfo> { p1, p2 ->
|
when {
|
||||||
when {
|
p1.isSelected > p2.isSelected -> -1
|
||||||
p1.isSelected > p2.isSelected -> -1
|
p1.isSelected == p2.isSelected -> 0
|
||||||
p1.isSelected == p2.isSelected -> 0
|
else -> 1
|
||||||
else -> 1
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
it.sortedWith(comparator)
|
|
||||||
} else {
|
|
||||||
val comparator = object : Comparator<AppInfo> {
|
|
||||||
val collator = Collator.getInstance()
|
val collator = Collator.getInstance()
|
||||||
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
|
appsList.sortedWith(compareBy(collator) { it.appName })
|
||||||
}
|
}
|
||||||
it.sortedWith(comparator)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// .map {
|
appsAll = apps
|
||||||
// val comparator = object : Comparator<AppInfo> {
|
adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
|
||||||
// val collator = Collator.getInstance()
|
|
||||||
// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
|
|
||||||
// }
|
|
||||||
// it.sortedWith(comparator)
|
|
||||||
// }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
appsAll = it
|
|
||||||
adapter = PerAppProxyAdapter(this, it, blacklist)
|
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.pbWaiting.visibility = View.GONE
|
binding.pbWaiting.hide()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
Log.e(ANG_PACKAGE, "Error loading apps", e)
|
||||||
}
|
}
|
||||||
/***
|
|
||||||
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
||||||
var dst = 0
|
|
||||||
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
dst += dy
|
|
||||||
if (dst > threshold) {
|
|
||||||
header_view.hide()
|
|
||||||
dst = 0
|
|
||||||
} else if (dst < -20) {
|
|
||||||
header_view.show()
|
|
||||||
dst = 0
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var hiding = false
|
|
||||||
fun View.hide() {
|
|
||||||
val target = -height.toFloat()
|
|
||||||
if (hiding || translationY == target) return
|
|
||||||
animate()
|
|
||||||
.translationY(target)
|
|
||||||
.setInterpolator(AccelerateInterpolator(2F))
|
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
|
||||||
hiding = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
hiding = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var showing = false
|
|
||||||
fun View.show() {
|
|
||||||
val target = 0f
|
|
||||||
if (showing || translationY == target) return
|
|
||||||
animate()
|
|
||||||
.translationY(target)
|
|
||||||
.setInterpolator(DecelerateInterpolator(2F))
|
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
|
||||||
showing = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
showing = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
***/
|
|
||||||
|
|
||||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
||||||
@@ -140,36 +85,6 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
||||||
}
|
}
|
||||||
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
||||||
|
|
||||||
/***
|
|
||||||
et_search.setOnEditorActionListener { v, actionId, event ->
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
|
||||||
//hide
|
|
||||||
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
||||||
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
|
|
||||||
|
|
||||||
val key = v.text.toString().toUpperCase()
|
|
||||||
val apps = ArrayList<AppInfo>()
|
|
||||||
if (TextUtils.isEmpty(key)) {
|
|
||||||
appsAll?.forEach {
|
|
||||||
apps.add(it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appsAll?.forEach {
|
|
||||||
if (it.appName.toUpperCase().indexOf(key) >= 0) {
|
|
||||||
apps.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
|
||||||
recycler_view.adapter = adapter
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
***/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -199,8 +114,10 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.select_all -> adapter?.let {
|
R.id.select_all -> adapter?.let { it ->
|
||||||
val pkgNames = it.apps.map { it.packageName }
|
val pkgNames = it.apps.map { it.packageName }
|
||||||
if (it.blacklist.containsAll(pkgNames)) {
|
if (it.blacklist.containsAll(pkgNames)) {
|
||||||
it.apps.forEach {
|
it.apps.forEach {
|
||||||
@@ -237,13 +154,20 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
|
|
||||||
private fun selectProxyApp() {
|
private fun selectProxyApp() {
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
|
binding.pbWaiting.show()
|
||||||
|
|
||||||
val url = AppConfig.androidpackagenamelistUrl
|
val url = AppConfig.androidpackagenamelistUrl
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val content = Utils.getUrlContext(url, 5000)
|
var content = HttpUtil.getUrlContent(url, 5000)
|
||||||
|
if (content.isNullOrEmpty()) {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
|
||||||
|
}
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
Log.d(ANG_PACKAGE, content)
|
Log.d(ANG_PACKAGE, content)
|
||||||
selectProxyApp(content, true)
|
selectProxyApp(content, true)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,7 +176,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
val content = Utils.getClipboard(applicationContext)
|
val content = Utils.getClipboard(applicationContext)
|
||||||
if (TextUtils.isEmpty(content)) return
|
if (TextUtils.isEmpty(content)) return
|
||||||
selectProxyApp(content, false)
|
selectProxyApp(content, false)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportProxyApp() {
|
private fun exportProxyApp() {
|
||||||
@@ -262,9 +186,10 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
lst = lst + System.getProperty("line.separator") + it
|
lst = lst + System.getProperty("line.separator") + it
|
||||||
}
|
}
|
||||||
Utils.setClipboard(applicationContext, lst)
|
Utils.setClipboard(applicationContext, lst)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun selectProxyApp(content: String, force: Boolean): Boolean {
|
private fun selectProxyApp(content: String, force: Boolean): Boolean {
|
||||||
try {
|
try {
|
||||||
val proxyApps = if (TextUtils.isEmpty(content)) {
|
val proxyApps = if (TextUtils.isEmpty(content)) {
|
||||||
@@ -277,7 +202,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
adapter?.blacklist?.clear()
|
adapter?.blacklist?.clear()
|
||||||
|
|
||||||
if (binding.switchBypassApps.isChecked) {
|
if (binding.switchBypassApps.isChecked) {
|
||||||
adapter?.let {
|
adapter?.let { it ->
|
||||||
it.apps.forEach block@{
|
it.apps.forEach block@{
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
Log.d(ANG_PACKAGE, packageName)
|
Log.d(ANG_PACKAGE, packageName)
|
||||||
@@ -290,7 +215,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
it.notifyDataSetChanged()
|
it.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
adapter?.let {
|
adapter?.let { it ->
|
||||||
it.apps.forEach block@{
|
it.apps.forEach block@{
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
Log.d(ANG_PACKAGE, packageName)
|
Log.d(ANG_PACKAGE, packageName)
|
||||||
@@ -339,7 +264,12 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
|
|
||||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun refreshData() {
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
|||||||
val view = View(ctx)
|
val view = View(ctx)
|
||||||
view.layoutParams = ViewGroup.LayoutParams(
|
view.layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0
|
0
|
||||||
)
|
)
|
||||||
BaseViewHolder(view)
|
BaseViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -78,7 +79,7 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsManager.saveRoutingRuleset(position, rulesetItem)
|
SettingsManager.saveRoutingRuleset(position, rulesetItem)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -12,12 +13,13 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.extension.toast
|
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.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
@@ -40,6 +42,16 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
resources.getStringArray(R.array.preset_rulesets)
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -48,6 +60,7 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
@@ -75,97 +88,74 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
R.id.add_rule -> {
|
R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true }
|
||||||
startActivity(Intent(this, RoutingEditActivity::class.java))
|
R.id.import_predefined_rulesets -> importPredefined().let { true }
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
R.id.user_asset_setting -> {
|
private fun importPredefined() {
|
||||||
startActivity(Intent(this, UserAssetActivity::class.java))
|
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_predefined_rulesets -> {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
try {
|
||||||
try {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
|
||||||
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
|
launch(Dispatchers.Main) {
|
||||||
launch(Dispatchers.Main) {
|
refreshData()
|
||||||
refreshData()
|
toastSuccess(R.string.toast_success)
|
||||||
toast(R.string.toast_success)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}.show()
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_rulesets_from_clipboard -> {
|
|
||||||
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) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
toast(R.string.toast_failure)
|
|
||||||
return@setPositiveButton
|
|
||||||
}
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = SettingsManager.resetRoutingRulesets(clipboard)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (result) {
|
|
||||||
refreshData()
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do nothing
|
//do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
true
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.import_rulesets_from_qrcode -> {
|
private fun importFromClipboard() {
|
||||||
RxPermissions(this)
|
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||||
.request(Manifest.permission.CAMERA)
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
.subscribe {
|
val clipboard = try {
|
||||||
if (it)
|
Utils.getClipboard(this)
|
||||||
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
|
} catch (e: Exception) {
|
||||||
else
|
e.printStackTrace()
|
||||||
toast(R.string.toast_permission_denied)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
R.id.export_rulesets_to_clipboard -> {
|
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
|
||||||
if (rulesetList.isNullOrEmpty()) {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
} else {
|
|
||||||
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
}
|
}
|
||||||
true
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
}
|
//do nothing
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
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()) {
|
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
@@ -182,24 +172,24 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (result) {
|
if (result) {
|
||||||
refreshData()
|
refreshData()
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do nothing
|
//do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun refreshData() {
|
fun refreshData() {
|
||||||
rulesets.clear()
|
rulesets.clear()
|
||||||
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -4,47 +4,49 @@ import android.Manifest
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
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.AngConfigManager
|
||||||
|
|
||||||
class ScScannerActivity : BaseActivity() {
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_none)
|
setContentView(R.layout.activity_none)
|
||||||
importQRcode()
|
importQRcode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importQRcode(): Boolean {
|
private fun importQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe { granted ->
|
|
||||||
if (granted) {
|
|
||||||
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == RESULT_OK) {
|
if (it.resultCode == RESULT_OK) {
|
||||||
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
|
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
|
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
|
||||||
|
|
||||||
if (count + countSub > 0) {
|
if (count + countSub > 0) {
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class ScSwitchActivity : BaseActivity() {
|
class ScSwitchActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -12,10 +11,10 @@ class ScSwitchActivity : BaseActivity() {
|
|||||||
|
|
||||||
setContentView(R.layout.activity_none)
|
setContentView(R.layout.activity_none)
|
||||||
|
|
||||||
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
if (V2RayServiceManager.isRunning()) {
|
||||||
Utils.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
} else {
|
} else {
|
||||||
Utils.startVServiceFromToggle(this)
|
V2RayServiceManager.startVServiceFromToggle(this)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package com.v2ray.ang.ui
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
@@ -21,6 +22,37 @@ import io.github.g00fy2.quickie.config.ScannerConfig
|
|||||||
class ScannerActivity : BaseActivity() {
|
class ScannerActivity : BaseActivity() {
|
||||||
|
|
||||||
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
|
||||||
|
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
val uri = it.data?.data
|
||||||
|
if (it.resultCode == RESULT_OK && uri != null) {
|
||||||
|
try {
|
||||||
|
val inputStream = contentResolver.openInputStream(uri)
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream?.close()
|
||||||
|
|
||||||
|
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||||
|
if (text.isNullOrEmpty()) {
|
||||||
|
toast(R.string.toast_decoding_failed)
|
||||||
|
} else {
|
||||||
|
finished(text)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
toast(R.string.toast_decoding_failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requestPermissionLauncher =
|
||||||
|
registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted: Boolean ->
|
||||||
|
if (isGranted) {
|
||||||
|
showFileChooser()
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -72,15 +104,12 @@ class ScannerActivity : BaseActivity() {
|
|||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
|
||||||
.request(permission)
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
.subscribe { granted ->
|
showFileChooser()
|
||||||
if (granted) {
|
} else {
|
||||||
showFileChooser()
|
requestPermissionLauncher.launch(permission)
|
||||||
} else {
|
}
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,26 +129,4 @@ class ScannerActivity : BaseActivity() {
|
|||||||
toast(R.string.toast_require_file_manager)
|
toast(R.string.toast_require_file_manager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
val uri = it.data?.data
|
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
|
||||||
try {
|
|
||||||
val inputStream = contentResolver.openInputStream(uri)
|
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
||||||
inputStream?.close()
|
|
||||||
|
|
||||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
|
||||||
if (text.isNullOrEmpty()) {
|
|
||||||
toast(R.string.toast_decoding_failed)
|
|
||||||
} else {
|
|
||||||
finished(text)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
toast(R.string.toast_decoding_failed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,7 @@ import com.v2ray.ang.dto.NetworkType
|
|||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
@@ -126,6 +127,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
|
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_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_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 et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
|
||||||
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
||||||
|
|
||||||
@@ -334,6 +337,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
et_port_hop?.text = Utils.getEditable(config.portHopping)
|
et_port_hop?.text = Utils.getEditable(config.portHopping)
|
||||||
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
|
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
|
||||||
et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
|
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 =
|
val securityEncryptions =
|
||||||
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||||
@@ -350,13 +355,13 @@ class ServerActivity : BaseActivity() {
|
|||||||
container_alpn?.visibility = View.VISIBLE
|
container_alpn?.visibility = View.VISIBLE
|
||||||
|
|
||||||
et_sni?.text = Utils.getEditable(config.sni)
|
et_sni?.text = Utils.getEditable(config.sni)
|
||||||
config.fingerPrint?.let {
|
config.fingerPrint?.let { it ->
|
||||||
val utlsIndex = Utils.arrayFind(uTlsItems, it)
|
val utlsIndex = Utils.arrayFind(uTlsItems, it)
|
||||||
sp_stream_fingerprint?.setSelection(utlsIndex)
|
utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) }
|
||||||
}
|
}
|
||||||
config.alpn?.let {
|
config.alpn?.let { it ->
|
||||||
val alpnIndex = Utils.arrayFind(alpns, it)
|
val alpnIndex = Utils.arrayFind(alpns, it)
|
||||||
sp_stream_alpn?.setSelection(alpnIndex)
|
alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) }
|
||||||
}
|
}
|
||||||
if (config.security == TLS) {
|
if (config.security == TLS) {
|
||||||
container_allow_insecure?.visibility = View.VISIBLE
|
container_allow_insecure?.visibility = View.VISIBLE
|
||||||
@@ -478,7 +483,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "")
|
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "")
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -513,6 +518,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
config.portHopping = et_port_hop?.text?.toString()
|
config.portHopping = et_port_hop?.text?.toString()
|
||||||
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
|
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
|
||||||
config.pinSHA256 = et_pinsha256?.text?.toString()
|
config.pinSHA256 = et_pinsha256?.text?.toString()
|
||||||
|
config.bandwidthDown = et_bandwidth_down?.text?.toString()
|
||||||
|
config.bandwidthUp = et_bandwidth_up?.text?.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,7 +607,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
MmkvManager.removeServer(editGuid)
|
MmkvManager.removeServer(editGuid)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.os.Bundle
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.blacksquircle.ui.editorkit.utils.EditorTheme
|
import com.blacksquircle.ui.editorkit.utils.EditorTheme
|
||||||
import com.blacksquircle.ui.language.json.JsonLanguage
|
import com.blacksquircle.ui.language.json.JsonLanguage
|
||||||
@@ -13,10 +12,10 @@ import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
|
|||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import me.drakeet.support.toast.ToastCompat
|
|
||||||
|
|
||||||
class ServerCustomConfigActivity : BaseActivity() {
|
class ServerCustomConfigActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
|
||||||
@@ -78,7 +77,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
CustomFmt.parse(binding.editor.text.toString())
|
CustomFmt.parse(binding.editor.text.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +90,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
|
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -106,7 +105,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
MmkvManager.removeServer(editGuid)
|
MmkvManager.removeServer(editGuid)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
private val appendHttpProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_APPEND_HTTP_PROXY) }
|
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 localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
||||||
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
|
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 mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
|
private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
|
||||||
private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) }
|
private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) }
|
||||||
@@ -60,6 +61,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
||||||
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
|
||||||
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_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 delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
|
||||||
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
|
||||||
|
|
||||||
@@ -152,6 +154,11 @@ class SettingsActivity : BaseActivity() {
|
|||||||
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
dnsHosts?.setOnPreferenceChangeListener { _, any ->
|
||||||
|
val nval = any as String
|
||||||
|
dnsHosts?.summary = nval
|
||||||
|
true
|
||||||
|
}
|
||||||
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
|
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
|
||||||
val nval = any as String
|
val nval = any as String
|
||||||
delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
|
delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
|
||||||
@@ -194,6 +201,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||||
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
||||||
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
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.DelayTestUrl)
|
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
|
||||||
|
|
||||||
initSharedPreference()
|
initSharedPreference()
|
||||||
@@ -230,6 +238,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
AppConfig.PREF_SPEED_ENABLED,
|
AppConfig.PREF_SPEED_ENABLED,
|
||||||
AppConfig.PREF_CONFIRM_REMOVE,
|
AppConfig.PREF_CONFIRM_REMOVE,
|
||||||
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
||||||
|
AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
|
||||||
AppConfig.PREF_PREFER_IPV6,
|
AppConfig.PREF_PREFER_IPV6,
|
||||||
AppConfig.PREF_PROXY_SHARING,
|
AppConfig.PREF_PROXY_SHARING,
|
||||||
AppConfig.PREF_ALLOW_INSECURE
|
AppConfig.PREF_ALLOW_INSECURE
|
||||||
@@ -239,6 +248,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
|
AppConfig.PREF_VPN_BYPASS_LAN,
|
||||||
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
||||||
AppConfig.PREF_MUX_XUDP_QUIC,
|
AppConfig.PREF_MUX_XUDP_QUIC,
|
||||||
AppConfig.PREF_FRAGMENT_PACKETS,
|
AppConfig.PREF_FRAGMENT_PACKETS,
|
||||||
@@ -262,6 +272,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
appendHttpProxy?.isEnabled = vpn
|
appendHttpProxy?.isEnabled = vpn
|
||||||
localDnsPort?.isEnabled = vpn
|
localDnsPort?.isEnabled = vpn
|
||||||
vpnDns?.isEnabled = vpn
|
vpnDns?.isEnabled = vpn
|
||||||
|
vpnBypassLan?.isEnabled = vpn
|
||||||
|
vpn
|
||||||
if (vpn) {
|
if (vpn) {
|
||||||
updateLocalDns(
|
updateLocalDns(
|
||||||
MmkvManager.decodeSettingsBool(
|
MmkvManager.decodeSettingsBool(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -18,8 +19,8 @@ import kotlinx.coroutines.launch
|
|||||||
class SubEditActivity : BaseActivity() {
|
class SubEditActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
private var del_config: MenuItem? = null
|
||||||
var save_config: MenuItem? = null
|
private var save_config: MenuItem? = null
|
||||||
|
|
||||||
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
|
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bingding seleced server config
|
* binding selected server config
|
||||||
*/
|
*/
|
||||||
private fun bindingServer(subItem: SubscriptionItem): Boolean {
|
private fun bindingServer(subItem: SubscriptionItem): Boolean {
|
||||||
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
||||||
@@ -94,7 +95,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MmkvManager.encodeSubscription(editSubId, subItem)
|
MmkvManager.encodeSubscription(editSubId, subItem)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -113,7 +114,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||||
import com.v2ray.ang.databinding.LayoutProgressBinding
|
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
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.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
@@ -35,6 +35,7 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
@@ -58,21 +59,18 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.sub_update -> {
|
R.id.sub_update -> {
|
||||||
val dialog = AlertDialog.Builder(this)
|
binding.pbWaiting.show()
|
||||||
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
|
||||||
.setCancelable(false)
|
|
||||||
.show()
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val count = AngConfigManager.updateConfigViaSubAll()
|
val count = AngConfigManager.updateConfigViaSubAll()
|
||||||
delay(500L)
|
delay(500L)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +81,7 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun refreshData() {
|
fun refreshData() {
|
||||||
subscriptions = MmkvManager.decodeSubscriptions()
|
subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
|||||||
|
|
||||||
if (TextUtils.isEmpty(subItem.url)) {
|
if (TextUtils.isEmpty(subItem.url)) {
|
||||||
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
|
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
|
||||||
|
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
|
||||||
} else {
|
} else {
|
||||||
|
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||||
AlertDialog.Builder(mActivity)
|
AlertDialog.Builder(mActivity)
|
||||||
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class TaskerActivity : BaseActivity() {
|
|||||||
lstData.add("Default")
|
lstData.add("Default")
|
||||||
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
|
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
|
||||||
|
|
||||||
MmkvManager.decodeServerList()?.forEach { key ->
|
MmkvManager.decodeServerList().forEach { key ->
|
||||||
MmkvManager.decodeServerConfig(key)?.let { config ->
|
MmkvManager.decodeServerConfig(key)?.let { config ->
|
||||||
lstData.add(config.remarks)
|
lstData.add(config.remarks)
|
||||||
lstGuid.add(key)
|
lstGuid.add(key)
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
class UrlSchemeActivity : BaseActivity() {
|
class UrlSchemeActivity : BaseActivity() {
|
||||||
@@ -40,7 +45,7 @@ class UrlSchemeActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,11 +71,15 @@ class UrlSchemeActivity : BaseActivity() {
|
|||||||
decodedUrl += "#${fragment}"
|
decodedUrl += "#${fragment}"
|
||||||
}
|
}
|
||||||
Log.d("UrlScheme-decodedUrl", decodedUrl)
|
Log.d("UrlScheme-decodedUrl", decodedUrl)
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if (count + countSub > 0) {
|
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
||||||
toast(R.string.import_subscription_success)
|
withContext(Dispatchers.Main) {
|
||||||
} else {
|
if (count + countSub > 0) {
|
||||||
toast(R.string.import_subscription_failure)
|
toast(R.string.import_subscription_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.import_subscription_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,18 +19,18 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||||
import com.v2ray.ang.databinding.LayoutProgressBinding
|
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
import com.v2ray.ang.extension.toTrafficString
|
import com.v2ray.ang.extension.toTrafficString
|
||||||
import com.v2ray.ang.extension.toast
|
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.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -38,9 +38,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.Proxy
|
|
||||||
import java.net.URL
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@@ -50,6 +47,38 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -58,12 +87,13 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = UserAssetAdapter()
|
binding.recyclerView.adapter = UserAssetAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -86,30 +116,10 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
requestStoragePermissionLauncher.launch(permission)
|
||||||
.request(permission)
|
|
||||||
.subscribe {
|
|
||||||
if (it) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val uri = result.data?.data
|
val uri = result.data?.data
|
||||||
if (result.resultCode == RESULT_OK && uri != null) {
|
if (result.resultCode == RESULT_OK && uri != null) {
|
||||||
val assetId = Utils.getUuid()
|
val assetId = Utils.getUuid()
|
||||||
@@ -127,7 +137,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
copyFile(uri)
|
copyFile(uri)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
toast(R.string.toast_asset_copy_failed)
|
toastError(R.string.toast_asset_copy_failed)
|
||||||
MmkvManager.removeAssetUrl(assetId)
|
MmkvManager.removeAssetUrl(assetId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,8 +148,8 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
contentResolver.openInputStream(uri).use { inputStream ->
|
contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
targetFile.outputStream().use { fileOut ->
|
targetFile.outputStream().use { fileOut ->
|
||||||
inputStream?.copyTo(fileOut)
|
inputStream?.copyTo(fileOut)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return targetFile.path
|
return targetFile.path
|
||||||
@@ -158,14 +168,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun importAssetFromQRcode(): Boolean {
|
private fun importAssetFromQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe {
|
|
||||||
if (it)
|
|
||||||
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +185,10 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Send URL to UserAssetUrlActivity for Processing
|
// Send URL to UserAssetUrlActivity for Processing
|
||||||
startActivity(Intent(this, UserAssetUrlActivity::class.java)
|
startActivity(
|
||||||
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url))
|
Intent(this, UserAssetUrlActivity::class.java)
|
||||||
|
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return false
|
return false
|
||||||
@@ -192,32 +197,31 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadGeoFiles() {
|
private fun downloadGeoFiles() {
|
||||||
val dialog = AlertDialog.Builder(this)
|
binding.pbWaiting.show()
|
||||||
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
|
||||||
.setCancelable(false)
|
|
||||||
.show()
|
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
var assets = MmkvManager.decodeAssetUrls()
|
||||||
assets = addBuiltInGeoItems(assets)
|
assets = addBuiltInGeoItems(assets)
|
||||||
|
|
||||||
assets.forEach {
|
var resultCount = 0
|
||||||
//toast(getString(R.string.msg_downloading_content) + it)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
assets.forEach {
|
||||||
var result = downloadGeo(it.second, 60000, httpPort)
|
var result = downloadGeo(it.second, 15000, httpPort)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = downloadGeo(it.second, 60000, 0)
|
result = downloadGeo(it.second, 15000, 0)
|
||||||
}
|
}
|
||||||
launch(Dispatchers.Main) {
|
if (result)
|
||||||
if (result) {
|
resultCount++
|
||||||
toast(getString(R.string.toast_success) + " " + it.second.remarks)
|
}
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
withContext(Dispatchers.Main) {
|
||||||
} else {
|
if (resultCount > 0) {
|
||||||
toast(getString(R.string.toast_failure) + " " + it.second.remarks)
|
toast(getString(R.string.title_update_config_count, resultCount))
|
||||||
}
|
refreshData()
|
||||||
dialog.dismiss()
|
} else {
|
||||||
|
toast(getString(R.string.toast_failure))
|
||||||
}
|
}
|
||||||
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,22 +229,10 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
||||||
val targetTemp = File(extDir, item.remarks + "_temp")
|
val targetTemp = File(extDir, item.remarks + "_temp")
|
||||||
val target = File(extDir, item.remarks)
|
val target = File(extDir, item.remarks)
|
||||||
var conn: HttpURLConnection? = null
|
|
||||||
//Log.d(AppConfig.ANG_PACKAGE, url)
|
//Log.d(AppConfig.ANG_PACKAGE, url)
|
||||||
|
|
||||||
|
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
|
||||||
try {
|
try {
|
||||||
conn = if (httpPort == 0) {
|
|
||||||
URL(item.url).openConnection() as HttpURLConnection
|
|
||||||
} else {
|
|
||||||
URL(item.url).openConnection(
|
|
||||||
Proxy(
|
|
||||||
Proxy.Type.HTTP,
|
|
||||||
InetSocketAddress(LOOPBACK, httpPort)
|
|
||||||
)
|
|
||||||
) as HttpURLConnection
|
|
||||||
}
|
|
||||||
conn.connectTimeout = timeout
|
|
||||||
conn.readTimeout = timeout
|
|
||||||
val inputStream = conn.inputStream
|
val inputStream = conn.inputStream
|
||||||
val responseCode = conn.responseCode
|
val responseCode = conn.responseCode
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
@@ -279,11 +271,16 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
lifecycleScope.launch(Dispatchers.Default) {
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun refreshData() {
|
||||||
|
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||||
return UserAssetViewHolder(
|
return UserAssetViewHolder(
|
||||||
@@ -333,7 +330,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
MmkvManager.removeAssetUrl(item.first)
|
MmkvManager.removeAssetUrl(item.first)
|
||||||
initAssets()
|
initAssets()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do noting
|
//do noting
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -349,4 +346,4 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
|
|
||||||
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
||||||
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
|
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -21,10 +22,10 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
|
|
||||||
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
private var del_config: MenuItem? = null
|
||||||
var save_config: MenuItem? = null
|
private var save_config: MenuItem? = null
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
private val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
|
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -41,6 +42,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
binding.etRemarks.setText(assetNameQrcode)
|
binding.etRemarks.setText(assetNameQrcode)
|
||||||
binding.etUrl.setText(assetUrlQrcode)
|
binding.etUrl.setText(assetUrlQrcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> clearAsset()
|
else -> clearAsset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +103,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MmkvManager.encodeAsset(assetId, assetItem)
|
MmkvManager.encodeAsset(assetId, assetItem)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -116,7 +118,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
MmkvManager.removeAssetUrl(editAssetId)
|
MmkvManager.removeAssetUrl(editAssetId)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
|
|||||||
@@ -4,36 +4,33 @@ import android.content.Context
|
|||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
object AppManagerUtil {
|
object AppManagerUtil {
|
||||||
private fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
|
/**
|
||||||
val packageManager = ctx.packageManager
|
* Load the list of network applications.
|
||||||
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
*
|
||||||
val apps = ArrayList<AppInfo>()
|
* @param context The context to use.
|
||||||
|
* @return A list of AppInfo objects representing the network applications.
|
||||||
|
*/
|
||||||
|
suspend fun loadNetworkAppList(context: Context): ArrayList<AppInfo> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||||
|
val apps = ArrayList<AppInfo>()
|
||||||
|
|
||||||
for (pkg in packages) {
|
for (pkg in packages) {
|
||||||
val applicationInfo = pkg.applicationInfo ?: continue
|
val applicationInfo = pkg.applicationInfo ?: continue
|
||||||
|
|
||||||
val appName = applicationInfo.loadLabel(packageManager).toString()
|
val appName = applicationInfo.loadLabel(packageManager).toString()
|
||||||
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
|
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
|
||||||
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
|
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
|
||||||
|
|
||||||
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
||||||
apps.add(appInfo)
|
apps.add(appInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext apps
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return apps
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
|
|
||||||
Observable.unsafeCreate {
|
|
||||||
it.onNext(loadNetworkAppList(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
// val PackageInfo.hasInternetPermission: Boolean
|
|
||||||
// get() {
|
|
||||||
// val permissions = requestedPermissions
|
|
||||||
// return permissions?.any { it == Manifest.permission.INTERNET } ?: false
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
153
V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
Normal file
153
V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
|
import com.v2ray.ang.util.Utils.encode
|
||||||
|
import com.v2ray.ang.util.Utils.urlDecode
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object HttpUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a URL string to its ASCII representation.
|
||||||
|
*
|
||||||
|
* @param str The URL string to convert.
|
||||||
|
* @return The ASCII representation of the URL.
|
||||||
|
*/
|
||||||
|
fun idnToASCII(str: String): String {
|
||||||
|
val url = URI(str)
|
||||||
|
val host = url.host
|
||||||
|
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
|
||||||
|
if (host != asciiHost) {
|
||||||
|
return str.replace(host, asciiHost)
|
||||||
|
} else {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the content of a URL as a string.
|
||||||
|
*
|
||||||
|
* @param url The URL to fetch content from.
|
||||||
|
* @param timeout The timeout value in milliseconds.
|
||||||
|
* @param httpPort The HTTP port to use.
|
||||||
|
* @return The content of the URL as a string.
|
||||||
|
*/
|
||||||
|
fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? {
|
||||||
|
val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null
|
||||||
|
try {
|
||||||
|
return conn.inputStream.bufferedReader().readText()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the content of a URL as a string with a custom User-Agent header.
|
||||||
|
*
|
||||||
|
* @param url The URL to fetch content from.
|
||||||
|
* @param timeout The timeout value in milliseconds.
|
||||||
|
* @param httpPort The HTTP port to use.
|
||||||
|
* @return The content of the URL as a string.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getUrlContentWithUserAgent(url: String?, timeout: Int = 15000, httpPort: Int = 0): String {
|
||||||
|
var currentUrl = url
|
||||||
|
var redirects = 0
|
||||||
|
val maxRedirects = 3
|
||||||
|
|
||||||
|
while (redirects++ < maxRedirects) {
|
||||||
|
if (currentUrl == null) continue
|
||||||
|
val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue
|
||||||
|
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
||||||
|
conn.connect()
|
||||||
|
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
when (responseCode) {
|
||||||
|
in 300..399 -> {
|
||||||
|
val location = conn.getHeaderField("Location")
|
||||||
|
conn.disconnect()
|
||||||
|
if (location.isNullOrEmpty()) {
|
||||||
|
throw IOException("Redirect location not found")
|
||||||
|
}
|
||||||
|
currentUrl = location
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> try {
|
||||||
|
return conn.inputStream.use { it.bufferedReader().readText() }
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IOException("Too many redirects")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an HttpURLConnection object connected through a proxy.
|
||||||
|
*
|
||||||
|
* @param urlStr The target URL address.
|
||||||
|
* @param port The port of the proxy server.
|
||||||
|
* @param connectTimeout The connection timeout in milliseconds (default is 15000 ms).
|
||||||
|
* @param readTimeout The read timeout in milliseconds (default is 15000 ms).
|
||||||
|
* @param needStream Whether the connection needs to support streaming.
|
||||||
|
* @return Returns a configured HttpURLConnection object, or null if it fails.
|
||||||
|
*/
|
||||||
|
fun createProxyConnection(
|
||||||
|
urlStr: String,
|
||||||
|
port: Int,
|
||||||
|
connectTimeout: Int = 15000,
|
||||||
|
readTimeout: Int = 15000,
|
||||||
|
needStream: Boolean = false
|
||||||
|
): HttpURLConnection? {
|
||||||
|
|
||||||
|
var conn: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
val url = URL(urlStr)
|
||||||
|
// Create a connection
|
||||||
|
conn = if (port == 0) {
|
||||||
|
url.openConnection()
|
||||||
|
} else {
|
||||||
|
url.openConnection(
|
||||||
|
Proxy(
|
||||||
|
Proxy.Type.HTTP,
|
||||||
|
InetSocketAddress(LOOPBACK, port)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} as HttpURLConnection
|
||||||
|
|
||||||
|
// Set connection and read timeouts
|
||||||
|
conn.connectTimeout = connectTimeout
|
||||||
|
conn.readTimeout = readTimeout
|
||||||
|
if (!needStream) {
|
||||||
|
// Set request headers
|
||||||
|
conn.setRequestProperty("Connection", "close")
|
||||||
|
// Disable automatic redirects
|
||||||
|
conn.instanceFollowRedirects = false
|
||||||
|
// Disable caching
|
||||||
|
conn.useCaches = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add Basic Authorization
|
||||||
|
url.userInfo?.let {
|
||||||
|
conn.setRequestProperty(
|
||||||
|
"Authorization",
|
||||||
|
"Basic ${encode(urlDecode(it))}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
// If an exception occurs, close the connection and return null
|
||||||
|
conn?.disconnect()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,14 +13,33 @@ import java.lang.reflect.Type
|
|||||||
object JsonUtil {
|
object JsonUtil {
|
||||||
private var gson = Gson()
|
private var gson = Gson()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an object to its JSON representation.
|
||||||
|
*
|
||||||
|
* @param src The object to convert.
|
||||||
|
* @return The JSON representation of the object.
|
||||||
|
*/
|
||||||
fun toJson(src: Any?): String {
|
fun toJson(src: Any?): String {
|
||||||
return gson.toJson(src)
|
return gson.toJson(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a JSON string into an object of the specified class.
|
||||||
|
*
|
||||||
|
* @param src The JSON string to parse.
|
||||||
|
* @param cls The class of the object to parse into.
|
||||||
|
* @return The parsed object.
|
||||||
|
*/
|
||||||
fun <T> fromJson(src: String, cls: Class<T>): T {
|
fun <T> fromJson(src: String, cls: Class<T>): T {
|
||||||
return gson.fromJson(src, cls)
|
return gson.fromJson(src, cls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an object to its pretty-printed JSON representation.
|
||||||
|
*
|
||||||
|
* @param src The object to convert.
|
||||||
|
* @return The pretty-printed JSON representation of the object, or null if the object is null.
|
||||||
|
*/
|
||||||
fun toJsonPretty(src: Any?): String? {
|
fun toJsonPretty(src: Any?): String? {
|
||||||
if (src == null)
|
if (src == null)
|
||||||
return null
|
return null
|
||||||
@@ -39,6 +58,12 @@ object JsonUtil {
|
|||||||
return gsonPre.toJson(src)
|
return gsonPre.toJson(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a JSON string into a JsonObject.
|
||||||
|
*
|
||||||
|
* @param src The JSON string to parse.
|
||||||
|
* @return The parsed JsonObject, or null if parsing fails.
|
||||||
|
*/
|
||||||
fun parseString(src: String?): JsonObject? {
|
fun parseString(src: String?): JsonObject? {
|
||||||
if (src == null)
|
if (src == null)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -7,17 +7,37 @@ import com.v2ray.ang.AppConfig
|
|||||||
import com.v2ray.ang.service.V2RayTestService
|
import com.v2ray.ang.service.V2RayTestService
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
|
|
||||||
object MessageUtil {
|
object MessageUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the service.
|
||||||
|
*
|
||||||
|
* @param ctx The context.
|
||||||
|
* @param what The message identifier.
|
||||||
|
* @param content The message content.
|
||||||
|
*/
|
||||||
fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) {
|
fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) {
|
||||||
sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
|
sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the UI.
|
||||||
|
*
|
||||||
|
* @param ctx The context.
|
||||||
|
* @param what The message identifier.
|
||||||
|
* @param content The message content.
|
||||||
|
*/
|
||||||
fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
|
fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
|
||||||
sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
|
sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the test service.
|
||||||
|
*
|
||||||
|
* @param ctx The context.
|
||||||
|
* @param what The message identifier.
|
||||||
|
* @param content The message content.
|
||||||
|
*/
|
||||||
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
|
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
|
||||||
try {
|
try {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
@@ -30,6 +50,14 @@ object MessageUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message with the specified action.
|
||||||
|
*
|
||||||
|
* @param ctx The context.
|
||||||
|
* @param action The action string.
|
||||||
|
* @param what The message identifier.
|
||||||
|
* @param content The message content.
|
||||||
|
*/
|
||||||
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
|
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
|
||||||
try {
|
try {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ import java.util.Locale
|
|||||||
|
|
||||||
open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
|
open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Wraps the context with a new locale.
|
||||||
|
*
|
||||||
|
* @param context The original context.
|
||||||
|
* @param newLocale The new locale to set.
|
||||||
|
* @return A ContextWrapper with the new locale.
|
||||||
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun wrap(context: Context, newLocale: Locale?): ContextWrapper {
|
fun wrap(context: Context, newLocale: Locale?): ContextWrapper {
|
||||||
var mContext = context
|
var mContext = context
|
||||||
val res: Resources = mContext.resources
|
val res: Resources = mContext.resources
|
||||||
val configuration: Configuration = res.configuration
|
val configuration: Configuration = res.configuration
|
||||||
//注意 Android 7.0 前后的不同处理方法
|
|
||||||
mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
configuration.setLocale(newLocale)
|
configuration.setLocale(newLocale)
|
||||||
val localeList = LocaleList(newLocale)
|
val localeList = LocaleList(newLocale)
|
||||||
|
|||||||
@@ -7,21 +7,24 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.service.ProcessService
|
import com.v2ray.ang.service.ProcessService
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object PluginUtil {
|
object PluginUtil {
|
||||||
//private const val HYSTERIA2 = "hysteria2-plugin"
|
|
||||||
private const val HYSTERIA2 = "libhysteria2.so"
|
private const val HYSTERIA2 = "libhysteria2.so"
|
||||||
private const val TAG = ANG_PACKAGE
|
private const val TAG = ANG_PACKAGE
|
||||||
private val procService: ProcessService by lazy {
|
private val procService: ProcessService by lazy {
|
||||||
ProcessService()
|
ProcessService()
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun initPlugin(name: String): PluginManager.InitResult {
|
/**
|
||||||
// return PluginManager.init(name)!!
|
* Run the plugin based on the provided configuration.
|
||||||
// }
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param config The profile configuration.
|
||||||
|
* @param domainPort The domain and port information.
|
||||||
|
*/
|
||||||
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
|
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
|
||||||
Log.d(TAG, "runPlugin")
|
Log.d(TAG, "runPlugin")
|
||||||
|
|
||||||
@@ -33,10 +36,20 @@ object PluginUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the running plugin.
|
||||||
|
*/
|
||||||
fun stopPlugin() {
|
fun stopPlugin() {
|
||||||
stopHy2()
|
stopHy2()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a real ping using Hysteria2.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param config The profile configuration.
|
||||||
|
* @return The ping delay in milliseconds, or -1 if it fails.
|
||||||
|
*/
|
||||||
fun realPingHy2(context: Context, config: ProfileItem?): Long {
|
fun realPingHy2(context: Context, config: ProfileItem?): Long {
|
||||||
Log.d(TAG, "realPingHy2")
|
Log.d(TAG, "realPingHy2")
|
||||||
val retFailure = -1L
|
val retFailure = -1L
|
||||||
@@ -49,7 +62,7 @@ object PluginUtil {
|
|||||||
val proc = ProcessService()
|
val proc = ProcessService()
|
||||||
proc.runProcess(context, cmd)
|
proc.runProcess(context, cmd)
|
||||||
Thread.sleep(1000L)
|
Thread.sleep(1000L)
|
||||||
val delay = SpeedtestUtil.testConnection(context, socksPort)
|
val delay = SpeedtestManager.testConnection(context, socksPort)
|
||||||
proc.stopProcess()
|
proc.stopProcess()
|
||||||
|
|
||||||
return delay.first
|
return delay.first
|
||||||
@@ -57,6 +70,14 @@ object PluginUtil {
|
|||||||
return retFailure
|
return retFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the configuration file for Hysteria2.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param config The profile configuration.
|
||||||
|
* @param domainPort The domain and port information.
|
||||||
|
* @return The generated configuration file.
|
||||||
|
*/
|
||||||
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
|
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
|
||||||
Log.d(TAG, "runPlugin $HYSTERIA2")
|
Log.d(TAG, "runPlugin $HYSTERIA2")
|
||||||
|
|
||||||
@@ -74,10 +95,16 @@ object PluginUtil {
|
|||||||
return configFile
|
return configFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the command to run Hysteria2.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param configFile The configuration file.
|
||||||
|
* @return The command to run Hysteria2.
|
||||||
|
*/
|
||||||
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
|
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
|
||||||
return mutableListOf(
|
return mutableListOf(
|
||||||
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
|
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
|
||||||
//initPlugin(HYSTERIA2).path,
|
|
||||||
"--disable-update-check",
|
"--disable-update-check",
|
||||||
"--config",
|
"--config",
|
||||||
configFile.absolutePath,
|
configFile.absolutePath,
|
||||||
@@ -87,6 +114,9 @@ object PluginUtil {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the Hysteria2 process.
|
||||||
|
*/
|
||||||
private fun stopHy2() {
|
private fun stopHy2() {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "$HYSTERIA2 destroy")
|
Log.d(TAG, "$HYSTERIA2 destroy")
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ import com.google.zxing.qrcode.QRCodeWriter
|
|||||||
import java.util.EnumMap
|
import java.util.EnumMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 描述:解析二维码图片
|
* QR code decoder utility.
|
||||||
*/
|
*/
|
||||||
object QRCodeDecoder {
|
object QRCodeDecoder {
|
||||||
val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java)
|
val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create qrcode using zxing
|
* Creates a QR code bitmap from the given text.
|
||||||
|
*
|
||||||
|
* @param text The text to encode in the QR code.
|
||||||
|
* @param size The size of the QR code bitmap.
|
||||||
|
* @return The generated QR code bitmap, or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
fun createQRCode(text: String, size: Int = 800): Bitmap? {
|
fun createQRCode(text: String, size: Int = 800): Bitmap? {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
@@ -35,22 +39,21 @@ object QRCodeDecoder {
|
|||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
* Decodes a QR code from a local image file. This method is time-consuming and should be called in a background thread.
|
||||||
*
|
*
|
||||||
* @param picturePath 要解析的二维码图片本地路径
|
* @param picturePath The local path of the image file to decode.
|
||||||
* @return 返回二维码图片里的内容 或 null
|
* @return The content of the QR code, or null if decoding fails.
|
||||||
*/
|
*/
|
||||||
fun syncDecodeQRCode(picturePath: String): String? {
|
fun syncDecodeQRCode(picturePath: String): String? {
|
||||||
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath))
|
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
|
* Decodes a QR code from a bitmap. This method is time-consuming and should be called in a background thread.
|
||||||
*
|
*
|
||||||
* @param bitmap 要解析的二维码图片
|
* @param bitmap The bitmap to decode.
|
||||||
* @return 返回二维码图片里的内容 或 null
|
* @return The content of the QR code, or null if decoding fails.
|
||||||
*/
|
*/
|
||||||
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
||||||
return bitmap?.let {
|
return bitmap?.let {
|
||||||
@@ -70,12 +73,11 @@ object QRCodeDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
|
* Converts a local image file to a bitmap that can be decoded as a QR code. The image is compressed to avoid being too large.
|
||||||
*
|
*
|
||||||
* @param picturePath 本地图片文件路径
|
* @param picturePath The local path of the image file.
|
||||||
* @return
|
* @return The decoded bitmap, or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
private fun getDecodeAbleBitmap(picturePath: String): Bitmap? {
|
private fun getDecodeAbleBitmap(picturePath: String): Bitmap? {
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.LocaleList
|
import android.os.LocaleList
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -16,35 +14,36 @@ import android.util.Base64
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
import com.v2ray.ang.BuildConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.Language
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.*
|
import java.net.ServerSocket
|
||||||
import java.util.*
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
object Utils {
|
object Utils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert string to editalbe for kotlin
|
* Convert string to editable for Kotlin.
|
||||||
*
|
*
|
||||||
* @param text
|
* @param text The string to convert.
|
||||||
* @return
|
* @return An Editable instance containing the text.
|
||||||
*/
|
*/
|
||||||
fun getEditable(text: String?): Editable {
|
fun getEditable(text: String?): Editable {
|
||||||
return Editable.Factory.getInstance().newEditable(text.orEmpty())
|
return Editable.Factory.getInstance().newEditable(text.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* find value in array position
|
* Find the position of a value in an array.
|
||||||
|
*
|
||||||
|
* @param array The array to search.
|
||||||
|
* @param value The value to find.
|
||||||
|
* @return The index of the value in the array, or -1 if not found.
|
||||||
*/
|
*/
|
||||||
fun arrayFind(array: Array<out String>, value: String): Int {
|
fun arrayFind(array: Array<out String>, value: String): Int {
|
||||||
for (i in array.indices) {
|
for (i in array.indices) {
|
||||||
@@ -56,19 +55,31 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* parseInt
|
* Parse a string to an integer.
|
||||||
|
*
|
||||||
|
* @param str The string to parse.
|
||||||
|
* @return The parsed integer, or 0 if parsing fails.
|
||||||
*/
|
*/
|
||||||
fun parseInt(str: String): Int {
|
fun parseInt(str: String): Int {
|
||||||
return parseInt(str, 0)
|
return parseInt(str, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a string to an integer with a default value.
|
||||||
|
*
|
||||||
|
* @param str The string to parse.
|
||||||
|
* @param default The default value if parsing fails.
|
||||||
|
* @return The parsed integer, or the default value if parsing fails.
|
||||||
|
*/
|
||||||
fun parseInt(str: String?, default: Int): Int {
|
fun parseInt(str: String?, default: Int): Int {
|
||||||
return str?.toIntOrNull() ?: default
|
return str?.toIntOrNull() ?: default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get text from clipboard
|
* Get text from the clipboard.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @return The text from the clipboard, or an empty string if an error occurs.
|
||||||
*/
|
*/
|
||||||
fun getClipboard(context: Context): String {
|
fun getClipboard(context: Context): String {
|
||||||
return try {
|
return try {
|
||||||
@@ -81,7 +92,10 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set text to clipboard
|
* Set text to the clipboard.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param content The text to set to the clipboard.
|
||||||
*/
|
*/
|
||||||
fun setClipboard(context: Context, content: String) {
|
fun setClipboard(context: Context, content: String) {
|
||||||
try {
|
try {
|
||||||
@@ -94,13 +108,21 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* base64 decode
|
* Decode a base64 encoded string.
|
||||||
|
*
|
||||||
|
* @param text The base64 encoded string.
|
||||||
|
* @return The decoded string, or an empty string if decoding fails.
|
||||||
*/
|
*/
|
||||||
fun decode(text: String?): String {
|
fun decode(text: String?): String {
|
||||||
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
|
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to decode a base64 encoded string.
|
||||||
|
*
|
||||||
|
* @param text The base64 encoded string.
|
||||||
|
* @return The decoded string, or null if decoding fails.
|
||||||
|
*/
|
||||||
fun tryDecodeBase64(text: String?): String? {
|
fun tryDecodeBase64(text: String?): String? {
|
||||||
try {
|
try {
|
||||||
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
||||||
@@ -116,7 +138,10 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* base64 encode
|
* Encode a string to base64.
|
||||||
|
*
|
||||||
|
* @param text The string to encode.
|
||||||
|
* @return The base64 encoded string, or an empty string if encoding fails.
|
||||||
*/
|
*/
|
||||||
fun encode(text: String): String {
|
fun encode(text: String): String {
|
||||||
return try {
|
return try {
|
||||||
@@ -128,39 +153,10 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get remote dns servers from preference
|
* Check if a string is a valid IP address.
|
||||||
*/
|
*
|
||||||
fun getRemoteDnsServers(): List<String> {
|
* @param value The string to check.
|
||||||
val remoteDns =
|
* @return True if the string is a valid IP address, false otherwise.
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
|
||||||
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
|
||||||
if (ret.isEmpty()) {
|
|
||||||
return listOf(AppConfig.DNS_PROXY)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getVpnDnsServers(): List<String> {
|
|
||||||
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
|
|
||||||
return vpnDns.split(",").filter { isPureIpAddress(it) }
|
|
||||||
// allow empty, in that case dns will use system default
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get remote dns servers from preference
|
|
||||||
*/
|
|
||||||
fun getDomesticDnsServers(): List<String> {
|
|
||||||
val domesticDns =
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
|
|
||||||
val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
|
||||||
if (ret.isEmpty()) {
|
|
||||||
return listOf(AppConfig.DNS_DIRECT)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is ip address
|
|
||||||
*/
|
*/
|
||||||
fun isIpAddress(value: String?): Boolean {
|
fun isIpAddress(value: String?): Boolean {
|
||||||
try {
|
try {
|
||||||
@@ -204,17 +200,35 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a pure IP address (IPv4 or IPv6).
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a pure IP address, false otherwise.
|
||||||
|
*/
|
||||||
fun isPureIpAddress(value: String): Boolean {
|
fun isPureIpAddress(value: String): Boolean {
|
||||||
return isIpv4Address(value) || isIpv6Address(value)
|
return isIpv4Address(value) || isIpv6Address(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isIpv4Address(value: String): Boolean {
|
/**
|
||||||
|
* Check if a string is a valid IPv4 address.
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a valid IPv4 address, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isIpv4Address(value: String): Boolean {
|
||||||
val regV4 =
|
val regV4 =
|
||||||
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
||||||
return regV4.matches(value)
|
return regV4.matches(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isIpv6Address(value: String): Boolean {
|
/**
|
||||||
|
* Check if a string is a valid IPv6 address.
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a valid IPv6 address, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isIpv6Address(value: String): Boolean {
|
||||||
var addr = value
|
var addr = value
|
||||||
if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) {
|
if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) {
|
||||||
addr = addr.drop(1)
|
addr = addr.drop(1)
|
||||||
@@ -225,12 +239,24 @@ object Utils {
|
|||||||
return regV6.matches(addr)
|
return regV6.matches(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isCoreDNSAddress(s: String): Boolean {
|
/**
|
||||||
return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") || s == "localhost"
|
* Check if a string is a CoreDNS address.
|
||||||
|
*
|
||||||
|
* @param s The string to check.
|
||||||
|
* @return True if the string is a CoreDNS address, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isCoreDNSAddress(s: String): Boolean {
|
||||||
|
return s.startsWith("https")
|
||||||
|
|| s.startsWith("tcp")
|
||||||
|
|| s.startsWith("quic")
|
||||||
|
|| s == "localhost"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* is valid url
|
* Check if a string is a valid URL.
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a valid URL, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isValidUrl(value: String?): Boolean {
|
fun isValidUrl(value: String?): Boolean {
|
||||||
try {
|
try {
|
||||||
@@ -250,30 +276,21 @@ object Utils {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startVServiceFromToggle(context: Context): Boolean {
|
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
|
||||||
context.toast(R.string.app_tile_first_use)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
V2RayServiceManager.startV2Ray(context)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* stopVService
|
* Open a URI in a browser.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param uriString The URI string to open.
|
||||||
*/
|
*/
|
||||||
fun stopVService(context: Context) {
|
|
||||||
context.toast(R.string.toast_services_stop)
|
|
||||||
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openUri(context: Context, uriString: String) {
|
fun openUri(context: Context, uriString: String) {
|
||||||
val uri = Uri.parse(uriString)
|
val uri = uriString.toUri()
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* uuid
|
* Generate a UUID.
|
||||||
|
*
|
||||||
|
* @return A UUID string without dashes.
|
||||||
*/
|
*/
|
||||||
fun getUuid(): String {
|
fun getUuid(): String {
|
||||||
return try {
|
return try {
|
||||||
@@ -284,6 +301,12 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a URL-encoded string.
|
||||||
|
*
|
||||||
|
* @param url The URL-encoded string.
|
||||||
|
* @return The decoded string, or the original string if decoding fails.
|
||||||
|
*/
|
||||||
fun urlDecode(url: String): String {
|
fun urlDecode(url: String): String {
|
||||||
return try {
|
return try {
|
||||||
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
||||||
@@ -293,6 +316,12 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a string to URL-encoded format.
|
||||||
|
*
|
||||||
|
* @param url The string to encode.
|
||||||
|
* @return The URL-encoded string, or the original string if encoding fails.
|
||||||
|
*/
|
||||||
fun urlEncode(url: String): String {
|
fun urlEncode(url: String): String {
|
||||||
return try {
|
return try {
|
||||||
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
|
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
|
||||||
@@ -302,9 +331,12 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* readTextFromAssets
|
* Read text from an asset file.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param fileName The name of the asset file.
|
||||||
|
* @return The content of the asset file as a string.
|
||||||
*/
|
*/
|
||||||
fun readTextFromAssets(context: Context?, fileName: String): String {
|
fun readTextFromAssets(context: Context?, fileName: String): String {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
@@ -316,6 +348,12 @@ object Utils {
|
|||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the user asset directory.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @return The path to the user asset directory.
|
||||||
|
*/
|
||||||
fun userAssetPath(context: Context?): String {
|
fun userAssetPath(context: Context?): String {
|
||||||
if (context == null)
|
if (context == null)
|
||||||
return ""
|
return ""
|
||||||
@@ -324,6 +362,12 @@ object Utils {
|
|||||||
return extDir.absolutePath
|
return extDir.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the backup directory.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @return The path to the backup directory.
|
||||||
|
*/
|
||||||
fun backupPath(context: Context?): String {
|
fun backupPath(context: Context?): String {
|
||||||
if (context == null)
|
if (context == null)
|
||||||
return ""
|
return ""
|
||||||
@@ -332,78 +376,32 @@ object Utils {
|
|||||||
return extDir.absolutePath
|
return extDir.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device ID for XUDP base key.
|
||||||
|
*
|
||||||
|
* @return The device ID for XUDP base key.
|
||||||
|
*/
|
||||||
fun getDeviceIdForXUDPBaseKey(): String {
|
fun getDeviceIdForXUDPBaseKey(): String {
|
||||||
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
||||||
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUrlContext(url: String, timeout: Int): String {
|
/**
|
||||||
var result: String
|
* Get the dark mode status.
|
||||||
var conn: HttpURLConnection? = null
|
*
|
||||||
|
* @param context The context to use.
|
||||||
try {
|
* @return True if dark mode is enabled, false otherwise.
|
||||||
conn = URL(url).openConnection() as HttpURLConnection
|
*/
|
||||||
conn.connectTimeout = timeout
|
|
||||||
conn.readTimeout = timeout
|
|
||||||
conn.setRequestProperty("Connection", "close")
|
|
||||||
conn.instanceFollowRedirects = false
|
|
||||||
conn.useCaches = false
|
|
||||||
//val code = conn.responseCode
|
|
||||||
result = conn.inputStream.bufferedReader().readText()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
result = ""
|
|
||||||
} finally {
|
|
||||||
conn?.disconnect()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getUrlContentWithCustomUserAgent(
|
|
||||||
urlStr: String?,
|
|
||||||
timeout: Int = 30000,
|
|
||||||
httpPort: Int = 0
|
|
||||||
): String {
|
|
||||||
val url = URL(urlStr)
|
|
||||||
val conn = if (httpPort == 0) {
|
|
||||||
url.openConnection()
|
|
||||||
} else {
|
|
||||||
url.openConnection(
|
|
||||||
Proxy(
|
|
||||||
Proxy.Type.HTTP,
|
|
||||||
InetSocketAddress(LOOPBACK, httpPort)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
conn.connectTimeout = timeout
|
|
||||||
conn.readTimeout = timeout
|
|
||||||
conn.setRequestProperty("Connection", "close")
|
|
||||||
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
|
||||||
url.userInfo?.let {
|
|
||||||
conn.setRequestProperty(
|
|
||||||
"Authorization",
|
|
||||||
"Basic ${encode(urlDecode(it))}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
conn.useCaches = false
|
|
||||||
return conn.inputStream.use {
|
|
||||||
it.bufferedReader().readText()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDarkModeStatus(context: Context): Boolean {
|
fun getDarkModeStatus(context: Context): Boolean {
|
||||||
return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
|
return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
fun setNightMode() {
|
* Get the IPv6 address in a formatted string.
|
||||||
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
*
|
||||||
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
* @param address The IPv6 address.
|
||||||
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
* @return The formatted IPv6 address, or the original address if not valid.
|
||||||
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
*/
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIpv6Address(address: String?): String {
|
fun getIpv6Address(address: String?): String {
|
||||||
if (address == null) {
|
if (address == null) {
|
||||||
return ""
|
return ""
|
||||||
@@ -415,59 +413,36 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocale(): Locale {
|
/**
|
||||||
val langCode =
|
* Get the system locale.
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
|
*
|
||||||
val language = Language.fromCode(langCode)
|
* @return The system locale.
|
||||||
|
*/
|
||||||
return when (language) {
|
fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
Language.AUTO -> 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.BANGLA -> Locale("bn")
|
|
||||||
Language.BAKHTIARI -> Locale("bqi", "IR")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
LocaleList.getDefault()[0]
|
LocaleList.getDefault()[0]
|
||||||
} else {
|
} else {
|
||||||
Locale.getDefault()
|
Locale.getDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix illegal characters in a URL.
|
||||||
|
*
|
||||||
|
* @param str The URL string.
|
||||||
|
* @return The URL string with illegal characters replaced.
|
||||||
|
*/
|
||||||
fun fixIllegalUrl(str: String): String {
|
fun fixIllegalUrl(str: String): String {
|
||||||
return str
|
return str
|
||||||
.replace(" ", "%20")
|
.replace(" ", "%20")
|
||||||
.replace("|", "%7C")
|
.replace("|", "%7C")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeWhiteSpace(str: String?): String? {
|
/**
|
||||||
return str?.replace(" ", "")
|
* Find a free port from a list of ports.
|
||||||
}
|
*
|
||||||
|
* @param ports The list of ports to check.
|
||||||
fun idnToASCII(str: String): String {
|
* @return The first free port found.
|
||||||
val url = URL(str)
|
* @throws IOException If no free port is found.
|
||||||
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file)
|
*/
|
||||||
.toExternalForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTv(context: Context): Boolean =
|
|
||||||
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
|
||||||
|
|
||||||
fun getDelayTestUrl(second: Boolean = false): String {
|
|
||||||
return if (second) {
|
|
||||||
AppConfig.DelayTestUrl2
|
|
||||||
} else {
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
|
||||||
?: AppConfig.DelayTestUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findFreePort(ports: List<Int>): Int {
|
fun findFreePort(ports: List<Int>): Int {
|
||||||
for (port in ports) {
|
for (port in ports) {
|
||||||
try {
|
try {
|
||||||
@@ -481,6 +456,12 @@ object Utils {
|
|||||||
throw IOException("no free port found")
|
throw IOException("no free port found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid subscription URL.
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a valid subscription URL, false otherwise.
|
||||||
|
*/
|
||||||
fun isValidSubUrl(value: String?): Boolean {
|
fun isValidSubUrl(value: String?): Boolean {
|
||||||
try {
|
try {
|
||||||
if (value.isNullOrEmpty()) return false
|
if (value.isNullOrEmpty()) return false
|
||||||
@@ -492,13 +473,23 @@ object Utils {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the receiver flags based on the Android version.
|
||||||
|
*
|
||||||
|
* @return The receiver flags.
|
||||||
|
*/
|
||||||
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
ContextCompat.RECEIVER_EXPORTED
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
} else {
|
} else {
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isXray(): Boolean = (ANG_PACKAGE == "com.v2ray.ang")
|
/**
|
||||||
|
* Check if the package is Xray.
|
||||||
|
*
|
||||||
|
* @return True if the package is Xray, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isXray(): Boolean = (ANG_PACKAGE.startsWith("com.v2ray.ang"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ import java.util.zip.ZipOutputStream
|
|||||||
object ZipUtil {
|
object ZipUtil {
|
||||||
private const val BUFFER_SIZE = 4096
|
private const val BUFFER_SIZE = 4096
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip the contents of a folder.
|
||||||
|
*
|
||||||
|
* @param folderPath The path to the folder to zip.
|
||||||
|
* @param outputZipFilePath The path to the output zip file.
|
||||||
|
* @return True if the operation is successful, false otherwise.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean {
|
fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean {
|
||||||
val buffer = ByteArray(BUFFER_SIZE)
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
@@ -59,6 +67,14 @@ object ZipUtil {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unzip the contents of a zip file to a folder.
|
||||||
|
*
|
||||||
|
* @param zipFile The zip file to unzip.
|
||||||
|
* @param destDirectory The destination directory.
|
||||||
|
* @return True if the operation is successful, false otherwise.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun unzipToFolder(zipFile: File, destDirectory: String): Boolean {
|
fun unzipToFolder(zipFile: File, destDirectory: String): Boolean {
|
||||||
File(destDirectory).run {
|
File(destDirectory).run {
|
||||||
@@ -72,10 +88,8 @@ object ZipUtil {
|
|||||||
zip.getInputStream(entry).use { input ->
|
zip.getInputStream(entry).use { input ->
|
||||||
val filePath = destDirectory + File.separator + entry.name
|
val filePath = destDirectory + File.separator + entry.name
|
||||||
if (!entry.isDirectory) {
|
if (!entry.isDirectory) {
|
||||||
// if the entry is a file, extracts it
|
|
||||||
extractFile(input, filePath)
|
extractFile(input, filePath)
|
||||||
} else {
|
} else {
|
||||||
// if the entry is a directory, make the directory
|
|
||||||
val dir = File(filePath)
|
val dir = File(filePath)
|
||||||
dir.mkdir()
|
dir.mkdir()
|
||||||
}
|
}
|
||||||
@@ -89,6 +103,13 @@ object ZipUtil {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a file from an input stream.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream to read from.
|
||||||
|
* @param destFilePath The destination file path.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun extractFile(inputStream: InputStream, destFilePath: String) {
|
private fun extractFile(inputStream: InputStream, destFilePath: String) {
|
||||||
val bos = BufferedOutputStream(FileOutputStream(destFilePath))
|
val bos = BufferedOutputStream(FileOutputStream(destFilePath))
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.ServersCache
|
import com.v2ray.ang.dto.ServersCache
|
||||||
import com.v2ray.ang.extension.serializable
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -49,7 +49,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* 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):
|
* 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)`.
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startListenBroadcast() {
|
fun startListenBroadcast() {
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||||
@@ -57,20 +56,30 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the ViewModel is cleared.
|
||||||
|
*/
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
||||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
SpeedtestUtil.closeAllTcpSockets()
|
SpeedtestManager.closeAllTcpSockets()
|
||||||
Log.i(ANG_PACKAGE, "Main ViewModel is cleared")
|
Log.i(ANG_PACKAGE, "Main ViewModel is cleared")
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the server list.
|
||||||
|
*/
|
||||||
fun reloadServerList() {
|
fun reloadServerList() {
|
||||||
serverList = MmkvManager.decodeServerList()
|
serverList = MmkvManager.decodeServerList()
|
||||||
updateCache()
|
updateCache()
|
||||||
updateListAction.value = -1
|
updateListAction.value = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a server by its GUID.
|
||||||
|
* @param guid The GUID of the server to remove.
|
||||||
|
*/
|
||||||
fun removeServer(guid: String) {
|
fun removeServer(guid: String) {
|
||||||
serverList.remove(guid)
|
serverList.remove(guid)
|
||||||
MmkvManager.removeServer(guid)
|
MmkvManager.removeServer(guid)
|
||||||
@@ -80,33 +89,43 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun appendCustomConfigServer(server: String): Boolean {
|
// /**
|
||||||
if (server.contains("inbounds")
|
// * Appends a custom configuration server.
|
||||||
&& server.contains("outbounds")
|
// * @param server The server configuration to append.
|
||||||
&& server.contains("routing")
|
// * @return True if the server was successfully appended, false otherwise.
|
||||||
) {
|
// */
|
||||||
try {
|
// fun appendCustomConfigServer(server: String): Boolean {
|
||||||
val config = CustomFmt.parse(server) ?: return false
|
// if (server.contains("inbounds")
|
||||||
config.subscriptionId = subscriptionId
|
// && server.contains("outbounds")
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
// && server.contains("routing")
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
// ) {
|
||||||
serverList.add(0, key)
|
// try {
|
||||||
// val profile = ProfileLiteItem(
|
// val config = CustomFmt.parse(server) ?: return false
|
||||||
// configType = config.configType,
|
// config.subscriptionId = subscriptionId
|
||||||
// subscriptionId = config.subscriptionId,
|
// val key = MmkvManager.encodeServerConfig("", config)
|
||||||
// remarks = config.remarks,
|
// MmkvManager.encodeServerRaw(key, server)
|
||||||
// server = config.getProxyOutbound()?.getServerAddress(),
|
// serverList.add(0, key)
|
||||||
// serverPort = config.getProxyOutbound()?.getServerPort(),
|
//// val profile = ProfileLiteItem(
|
||||||
// )
|
//// configType = config.configType,
|
||||||
serversCache.add(0, ServersCache(key, config))
|
//// subscriptionId = config.subscriptionId,
|
||||||
return true
|
//// remarks = config.remarks,
|
||||||
} catch (e: Exception) {
|
//// server = config.getProxyOutbound()?.getServerAddress(),
|
||||||
e.printStackTrace()
|
//// serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||||
}
|
//// )
|
||||||
}
|
// serversCache.add(0, ServersCache(key, config))
|
||||||
return false
|
// return true
|
||||||
}
|
// } catch (e: Exception) {
|
||||||
|
// e.printStackTrace()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps the positions of two servers.
|
||||||
|
* @param fromPosition The initial position of the server.
|
||||||
|
* @param toPosition The target position of the server.
|
||||||
|
*/
|
||||||
fun swapServer(fromPosition: Int, toPosition: Int) {
|
fun swapServer(fromPosition: Int, toPosition: Int) {
|
||||||
if (subscriptionId.isEmpty()) {
|
if (subscriptionId.isEmpty()) {
|
||||||
Collections.swap(serverList, fromPosition, toPosition)
|
Collections.swap(serverList, fromPosition, toPosition)
|
||||||
@@ -119,6 +138,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
MmkvManager.encodeServerList(serverList)
|
MmkvManager.encodeServerList(serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the cache of servers.
|
||||||
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun updateCache() {
|
fun updateCache() {
|
||||||
serversCache.clear()
|
serversCache.clear()
|
||||||
@@ -141,12 +163,16 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keywordFilter.isEmpty() || profile.remarks.contains(keywordFilter)) {
|
if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) {
|
||||||
serversCache.add(ServersCache(guid, profile))
|
serversCache.add(ServersCache(guid, profile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the configuration via subscription for all servers.
|
||||||
|
* @return The number of updated configurations.
|
||||||
|
*/
|
||||||
fun updateConfigViaSubAll(): Int {
|
fun updateConfigViaSubAll(): Int {
|
||||||
if (subscriptionId.isEmpty()) {
|
if (subscriptionId.isEmpty()) {
|
||||||
return AngConfigManager.updateConfigViaSubAll()
|
return AngConfigManager.updateConfigViaSubAll()
|
||||||
@@ -156,6 +182,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all servers.
|
||||||
|
* @return The number of exported servers.
|
||||||
|
*/
|
||||||
fun exportAllServer(): Int {
|
fun exportAllServer(): Int {
|
||||||
val serverListCopy =
|
val serverListCopy =
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
@@ -171,21 +201,22 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the TCP ping for all servers.
|
||||||
|
*/
|
||||||
fun testAllTcping() {
|
fun testAllTcping() {
|
||||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
SpeedtestUtil.closeAllTcpSockets()
|
SpeedtestManager.closeAllTcpSockets()
|
||||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||||
//updateListAction.value = -1 // update all
|
|
||||||
|
|
||||||
val serversCopy = serversCache.toList() // Create a copy of the list
|
val serversCopy = serversCache.toList()
|
||||||
for (item in serversCopy) {
|
for (item in serversCopy) {
|
||||||
item.profile.let { outbound ->
|
item.profile.let { outbound ->
|
||||||
val serverAddress = outbound.server
|
val serverAddress = outbound.server
|
||||||
val serverPort = outbound.serverPort
|
val serverPort = outbound.serverPort
|
||||||
if (serverAddress != null && serverPort != null) {
|
if (serverAddress != null && serverPort != null) {
|
||||||
tcpingTestScope.launch {
|
tcpingTestScope.launch {
|
||||||
val testResult = SpeedtestUtil.tcping(serverAddress, serverPort.toInt())
|
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
||||||
updateListAction.value = getPosition(item.guid)
|
updateListAction.value = getPosition(item.guid)
|
||||||
@@ -196,23 +227,33 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the real ping for all servers.
|
||||||
|
*/
|
||||||
fun testAllRealPing() {
|
fun testAllRealPing() {
|
||||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
|
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
|
||||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||||
updateListAction.value = -1 // update all
|
updateListAction.value = -1
|
||||||
|
|
||||||
val serversCopy = serversCache.toList() // Create a copy of the list
|
val serversCopy = serversCache.toList()
|
||||||
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
for (item in serversCopy) {
|
for (item in serversCopy) {
|
||||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
|
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the real ping for the current server.
|
||||||
|
*/
|
||||||
fun testCurrentServerRealPing() {
|
fun testCurrentServerRealPing() {
|
||||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
|
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the subscription ID.
|
||||||
|
* @param id The new subscription ID.
|
||||||
|
*/
|
||||||
fun subscriptionIdChanged(id: String) {
|
fun subscriptionIdChanged(id: String) {
|
||||||
if (subscriptionId != id) {
|
if (subscriptionId != id) {
|
||||||
subscriptionId = id
|
subscriptionId = id
|
||||||
@@ -221,6 +262,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the subscriptions.
|
||||||
|
* @param context The context.
|
||||||
|
* @return A pair of lists containing the subscription IDs and remarks.
|
||||||
|
*/
|
||||||
fun getSubscriptions(context: Context): Pair<MutableList<String>?, MutableList<String>?> {
|
fun getSubscriptions(context: Context): Pair<MutableList<String>?, MutableList<String>?> {
|
||||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
if (subscriptionId.isNotEmpty()
|
if (subscriptionId.isNotEmpty()
|
||||||
@@ -239,6 +285,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return listId to listRemarks
|
return listId to listRemarks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the position of a server by its GUID.
|
||||||
|
* @param guid The GUID of the server.
|
||||||
|
* @return The position of the server.
|
||||||
|
*/
|
||||||
fun getPosition(guid: String): Int {
|
fun getPosition(guid: String): Int {
|
||||||
serversCache.forEachIndexed { index, it ->
|
serversCache.forEachIndexed { index, it ->
|
||||||
if (it.guid == guid)
|
if (it.guid == guid)
|
||||||
@@ -247,6 +298,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes duplicate servers.
|
||||||
|
* @return The number of removed servers.
|
||||||
|
*/
|
||||||
fun removeDuplicateServer(): Int {
|
fun removeDuplicateServer(): Int {
|
||||||
val serversCacheCopy = mutableListOf<Pair<String, ProfileItem>>()
|
val serversCacheCopy = mutableListOf<Pair<String, ProfileItem>>()
|
||||||
for (it in serversCache) {
|
for (it in serversCache) {
|
||||||
@@ -273,6 +328,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return deleteServer.count()
|
return deleteServer.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all servers.
|
||||||
|
* @return The number of removed servers.
|
||||||
|
*/
|
||||||
fun removeAllServer(): Int {
|
fun removeAllServer(): Int {
|
||||||
val count =
|
val count =
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
@@ -287,6 +346,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes invalid servers.
|
||||||
|
* @return The number of removed servers.
|
||||||
|
*/
|
||||||
fun removeInvalidServer(): Int {
|
fun removeInvalidServer(): Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
@@ -300,6 +363,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts servers by their test results.
|
||||||
|
*/
|
||||||
fun sortByTestResults() {
|
fun sortByTestResults() {
|
||||||
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
||||||
|
|
||||||
@@ -319,12 +385,20 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
MmkvManager.encodeServerList(serverList)
|
MmkvManager.encodeServerList(serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes assets.
|
||||||
|
* @param assets The asset manager.
|
||||||
|
*/
|
||||||
fun initAssets(assets: AssetManager) {
|
fun initAssets(assets: AssetManager) {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
SettingsManager.initAssets(getApplication<AngApplication>(), assets)
|
SettingsManager.initAssets(getApplication<AngApplication>(), assets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the configuration by a keyword.
|
||||||
|
* @param keyword The keyword to filter by.
|
||||||
|
*/
|
||||||
fun filterConfig(keyword: String) {
|
fun filterConfig(keyword: String) {
|
||||||
if (keyword == keywordFilter) {
|
if (keyword == keywordFilter) {
|
||||||
return
|
return
|
||||||
@@ -346,12 +420,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START_SUCCESS -> {
|
AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||||
getApplication<AngApplication>().toast(R.string.toast_services_success)
|
getApplication<AngApplication>().toastSuccess(R.string.toast_services_success)
|
||||||
isRunning.value = true
|
isRunning.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START_FAILURE -> {
|
AppConfig.MSG_STATE_START_FAILURE -> {
|
||||||
getApplication<AngApplication>().toast(R.string.toast_services_failure)
|
getApplication<AngApplication>().toastError(R.string.toast_services_failure)
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,22 @@ import androidx.lifecycle.AndroidViewModel
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
|
||||||
class SettingsViewModel(application: Application) : AndroidViewModel(application),
|
class SettingsViewModel(application: Application) : AndroidViewModel(application),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts listening for preference changes.
|
||||||
|
*/
|
||||||
fun startListenPreferenceChange() {
|
fun startListenPreferenceChange() {
|
||||||
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
||||||
.registerOnSharedPreferenceChangeListener(this)
|
.registerOnSharedPreferenceChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the ViewModel is cleared.
|
||||||
|
*/
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
||||||
.unregisterOnSharedPreferenceChangeListener(this)
|
.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
@@ -24,13 +30,20 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a shared preference is changed.
|
||||||
|
* @param sharedPreferences The shared preferences.
|
||||||
|
* @param key The key of the changed preference.
|
||||||
|
*/
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key")
|
Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key")
|
||||||
when (key) {
|
when (key) {
|
||||||
AppConfig.PREF_MODE,
|
AppConfig.PREF_MODE,
|
||||||
AppConfig.PREF_VPN_DNS,
|
AppConfig.PREF_VPN_DNS,
|
||||||
|
AppConfig.PREF_VPN_BYPASS_LAN,
|
||||||
AppConfig.PREF_REMOTE_DNS,
|
AppConfig.PREF_REMOTE_DNS,
|
||||||
AppConfig.PREF_DOMESTIC_DNS,
|
AppConfig.PREF_DOMESTIC_DNS,
|
||||||
|
AppConfig.PREF_DNS_HOSTS,
|
||||||
AppConfig.PREF_DELAY_TEST_URL,
|
AppConfig.PREF_DELAY_TEST_URL,
|
||||||
AppConfig.PREF_LOCAL_DNS_PORT,
|
AppConfig.PREF_LOCAL_DNS_PORT,
|
||||||
AppConfig.PREF_SOCKS_PORT,
|
AppConfig.PREF_SOCKS_PORT,
|
||||||
@@ -60,6 +73,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
AppConfig.PREF_BYPASS_APPS,
|
AppConfig.PREF_BYPASS_APPS,
|
||||||
AppConfig.PREF_CONFIRM_REMOVE,
|
AppConfig.PREF_CONFIRM_REMOVE,
|
||||||
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
||||||
|
AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
|
||||||
AppConfig.SUBSCRIPTION_AUTO_UPDATE,
|
AppConfig.SUBSCRIPTION_AUTO_UPDATE,
|
||||||
AppConfig.PREF_FRAGMENT_ENABLED,
|
AppConfig.PREF_FRAGMENT_ENABLED,
|
||||||
AppConfig.PREF_MUX_ENABLED,
|
AppConfig.PREF_MUX_ENABLED,
|
||||||
@@ -75,13 +89,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
AppConfig.PREF_MUX_XUDP_CONCURRENCY -> {
|
AppConfig.PREF_MUX_XUDP_CONCURRENCY -> {
|
||||||
MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "8"))
|
MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppConfig.PREF_PER_APP_PROXY_SET -> {
|
|
||||||
// MmkvManager.encodeSettings(key, sharedPreferences.getStringSet(key, setOf()))
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
|
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
|
||||||
Utils.setNightMode()
|
SettingsManager.setNightMode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
android:viewportWidth="1024"
|
android:viewportWidth="1024"
|
||||||
android:viewportHeight="1024">
|
android:viewportHeight="1024">
|
||||||
<path
|
<path
|
||||||
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:fillColor="#FFFFFFFF" />
|
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
android:viewportWidth="1024"
|
android:viewportWidth="1024"
|
||||||
android:viewportHeight="1024">
|
android:viewportHeight="1024">
|
||||||
<path
|
<path
|
||||||
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:fillColor="#FFFFFFFF" />
|
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" />
|
||||||
<path
|
<path
|
||||||
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:fillColor="#FFFFFFFF" />
|
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" />
|
||||||
<path
|
<path
|
||||||
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:fillColor="#FFFFFFFF" />
|
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector android:height="24dp"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:tint="#FFFFFF"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="24dp"
|
||||||
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
|
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user