Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7438ee8308 | ||
|
|
f01cf7fcb5 | ||
|
|
7a852f78e4 | ||
|
|
0923659a49 | ||
|
|
f9feb08607 | ||
|
|
3b43fe39e5 | ||
|
|
de30fa15b3 | ||
|
|
25a4d7c14d | ||
|
|
c8d3607efe | ||
|
|
a0e73a9aa9 | ||
|
|
eaccf237a4 | ||
|
|
5124266346 | ||
|
|
5cf2ea5a1e | ||
|
|
7a1af5914e | ||
|
|
e61f5eeb76 | ||
|
|
6d92106f9d | ||
|
|
8b06745e86 | ||
|
|
85ad999975 | ||
|
|
4c0f2d84cc | ||
|
|
6fc9803431 | ||
|
|
59a710bae5 | ||
|
|
98c642e1a8 | ||
|
|
e91f4470fb | ||
|
|
b33cc5284f | ||
|
|
eea6db6814 | ||
|
|
ba622c7edf | ||
|
|
b52e89d614 | ||
|
|
49cd3a0494 | ||
|
|
75cc16c5e0 | ||
|
|
e674d22ecd | ||
|
|
dca5011eb1 | ||
|
|
4d8e38b704 | ||
|
|
5ba4352641 | ||
|
|
1b2cc11a97 | ||
|
|
a3591e4bbb | ||
|
|
aa0f5639b1 | ||
|
|
f3abd0d9fc | ||
|
|
4a62aff7d2 | ||
|
|
c78e624eaf | ||
|
|
934cf5d21c | ||
|
|
f252d1395a | ||
|
|
2dc0472c69 | ||
|
|
3e09adc4d1 | ||
|
|
11750b9382 | ||
|
|
30a4c2199a | ||
|
|
406a9f996e | ||
|
|
5373579bd5 | ||
|
|
9b4cc201e7 | ||
|
|
6f2c96c2b6 | ||
|
|
1f6104de8b | ||
|
|
e078a2ab27 | ||
|
|
5695c17908 | ||
|
|
e2c1081d5a | ||
|
|
bcbcbc91c7 | ||
|
|
5eb3566e8d | ||
|
|
d75eca8dd4 | ||
|
|
640c16d8dc | ||
|
|
1ba5c5a7a6 | ||
|
|
f67af69dda | ||
|
|
5d47777307 | ||
|
|
ab22bb9804 | ||
|
|
2626462e49 | ||
|
|
41893d79c0 | ||
|
|
834c1ba63d | ||
|
|
633ee63891 | ||
|
|
eb5627c0d0 | ||
|
|
6db38f6e3d | ||
|
|
c69a758429 | ||
|
|
cee3a0ffec | ||
|
|
18c0143186 | ||
|
|
bbf0b05b49 | ||
|
|
44723c56ad | ||
|
|
e53c36b53b | ||
|
|
80f26cd4b8 | ||
|
|
b023414cd0 | ||
|
|
2f56104565 | ||
|
|
9cd5fefdca | ||
|
|
25c42c475f | ||
|
|
96e66da071 | ||
|
|
6914b9ee1b | ||
|
|
cfc6546c97 | ||
|
|
0880313659 | ||
|
|
875ca02126 | ||
|
|
c2d5925053 | ||
|
|
547bbf8e95 | ||
|
|
2218251b03 | ||
|
|
4da3a23162 | ||
|
|
28a90baf88 | ||
|
|
7bbdda2f2f | ||
|
|
884b444a41 | ||
|
|
e1ff2df36e | ||
|
|
61c0111778 | ||
|
|
bfc9a64e07 | ||
|
|
2ba92045cc | ||
|
|
aeca9f51c8 | ||
|
|
b107c0ac1d | ||
|
|
65a04b4784 | ||
|
|
eab9f50cfd | ||
|
|
b8bb83b524 | ||
|
|
93eb9fe3b9 | ||
|
|
5f167512f5 | ||
|
|
a727b81263 | ||
|
|
f27c9192d1 | ||
|
|
90153fa17f | ||
|
|
cdfaa01852 | ||
|
|
33d2c3b00d | ||
|
|
b7f992cdc0 | ||
|
|
0a6a24e309 | ||
|
|
153b4cffef | ||
|
|
f09a413232 | ||
|
|
cba58f6ae2 | ||
|
|
2bf4b91488 |
167
.github/workflows/build.yml
vendored
167
.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:
|
||||||
@@ -16,6 +17,102 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
fetch-depth: '0'
|
||||||
|
|
||||||
|
- name: Restore cached libtun2socks
|
||||||
|
id: cache-libtun2socks-restore
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/libs
|
||||||
|
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
|
||||||
|
|
||||||
|
- name: Setup Android NDK
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
id: setup-ndk
|
||||||
|
# Same version as https://gitlab.com/fdroid/fdroiddata/metadata/com.v2ray.ang.yml
|
||||||
|
with:
|
||||||
|
ndk-version: r27
|
||||||
|
add-to-path: true
|
||||||
|
link-to-sdk: true
|
||||||
|
local-cache: true
|
||||||
|
|
||||||
|
- name: Restore Android Symlinks
|
||||||
|
run: |
|
||||||
|
directory="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||||
|
find "$directory" -type l | while read link; do
|
||||||
|
current_target=$(readlink "$link")
|
||||||
|
new_target="$directory/$(basename "$current_target")"
|
||||||
|
ln -sf "$new_target" "$link"
|
||||||
|
echo "Changed $(basename "$link") from $current_target to $new_target"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Build libtun2socks
|
||||||
|
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
bash compile-tun2socks.sh
|
||||||
|
tar -xvzf libtun2socks.so.tgz
|
||||||
|
env:
|
||||||
|
NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
|
||||||
|
- 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 }}-${{ 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
|
||||||
|
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 }}-${{ 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
|
||||||
|
with:
|
||||||
|
go-version-file: 'AndroidLibXrayLite/go.mod'
|
||||||
|
|
||||||
|
- name: Build libhysteria2
|
||||||
|
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
bash libhysteria2.sh
|
||||||
|
env:
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
|
||||||
|
- 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 }}-${{ 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
|
||||||
@@ -23,71 +120,51 @@ jobs:
|
|||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Setup Golang
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.23.2'
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- 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
|
- name: Setup Android environment
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
- name: Build dependencies
|
- name: Decode Keystore
|
||||||
run: |
|
uses: timheuer/base64-to-file@v1
|
||||||
mkdir ${{ github.workspace }}/build
|
id: android_keystore
|
||||||
cd ${{ github.workspace }}/build
|
with:
|
||||||
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
|
fileName: "android_keystore.jks"
|
||||||
cd AndroidLibXrayLite
|
encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }}
|
||||||
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
|
||||||
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 }}
|
||||||
|
env:
|
||||||
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
|
|
||||||
- name: Upload arm64-v8a APK
|
- name: Upload arm64-v8a APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
V2rayNG/app/release/output.json
|
V2rayNG/app/release/output.json
|
||||||
.idea/
|
.idea/
|
||||||
.gradle/
|
.gradle/
|
||||||
|
*.so
|
||||||
|
|||||||
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 08c4a038f0
@@ -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)
|
||||||
|
|||||||
@@ -1,46 +1,63 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
alias(libs.plugins.android.application)
|
||||||
id("org.jetbrains.kotlin.android")
|
alias(libs.plugins.kotlin.android)
|
||||||
|
id("com.jaredsburrows.license")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.v2ray.ang"
|
namespace = "com.v2ray.ang"
|
||||||
compileSdk = 34
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 605
|
versionCode = 636
|
||||||
versionName = "1.9.11"
|
versionName = "1.9.39"
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
debug {
|
}
|
||||||
isMinifyEnabled = false
|
|
||||||
|
|
||||||
|
flavorDimensions.add("distribution")
|
||||||
|
productFlavors {
|
||||||
|
create("fdroid") {
|
||||||
|
dimension = "distribution"
|
||||||
|
applicationIdSuffix = ".fdroid"
|
||||||
|
buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"")
|
||||||
|
}
|
||||||
|
create("playstore") {
|
||||||
|
dimension = "distribution"
|
||||||
|
buildConfigField("String", "DISTRIBUTION", "\"Play Store\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,30 +67,57 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.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 {
|
||||||
@@ -86,49 +130,61 @@ android {
|
|||||||
useLegacyPackaging = true
|
useLegacyPackaging = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Core Libraries
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
||||||
testImplementation(libs.junit)
|
|
||||||
testImplementation(libs.org.mockito.mockito.inline)
|
|
||||||
testImplementation(libs.mockito.kotlin)
|
|
||||||
|
|
||||||
implementation(libs.flexbox)
|
// AndroidX Core Libraries
|
||||||
// Androidx
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.constraintlayout)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.legacy.support.v4)
|
implementation(libs.androidx.activity)
|
||||||
implementation(libs.appcompat)
|
implementation(libs.androidx.constraintlayout)
|
||||||
implementation(libs.material)
|
|
||||||
implementation(libs.cardview)
|
|
||||||
implementation(libs.preference.ktx)
|
implementation(libs.preference.ktx)
|
||||||
implementation(libs.recyclerview)
|
implementation(libs.recyclerview)
|
||||||
implementation(libs.fragment.ktx)
|
implementation(libs.androidx.swiperefreshlayout)
|
||||||
implementation(libs.multidex)
|
|
||||||
implementation(libs.viewpager2)
|
|
||||||
|
|
||||||
// Androidx ktx
|
// UI Libraries
|
||||||
implementation(libs.activity.ktx)
|
implementation(libs.material)
|
||||||
|
implementation(libs.toastcompat)
|
||||||
|
implementation(libs.editorkit)
|
||||||
|
implementation(libs.flexbox)
|
||||||
|
|
||||||
|
// Data and Storage Libraries
|
||||||
|
implementation(libs.mmkv.static)
|
||||||
|
implementation(libs.gson)
|
||||||
|
|
||||||
|
// Reactive and Utility Libraries
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
|
// Language and Processing Libraries
|
||||||
|
implementation(libs.language.base)
|
||||||
|
implementation(libs.language.json)
|
||||||
|
|
||||||
|
// Intent and Utility Libraries
|
||||||
|
implementation(libs.quickie.foss)
|
||||||
|
implementation(libs.core)
|
||||||
|
|
||||||
|
// AndroidX Lifecycle and Architecture Components
|
||||||
implementation(libs.lifecycle.viewmodel.ktx)
|
implementation(libs.lifecycle.viewmodel.ktx)
|
||||||
implementation(libs.lifecycle.livedata.ktx)
|
implementation(libs.lifecycle.livedata.ktx)
|
||||||
implementation(libs.lifecycle.runtime.ktx)
|
implementation(libs.lifecycle.runtime.ktx)
|
||||||
|
|
||||||
//kotlin
|
// Background Task Libraries
|
||||||
implementation(libs.kotlin.reflect)
|
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
|
||||||
|
|
||||||
implementation(libs.mmkv.static)
|
|
||||||
implementation(libs.gson)
|
|
||||||
implementation(libs.rxjava)
|
|
||||||
implementation(libs.rxandroid)
|
|
||||||
implementation(libs.rxpermissions)
|
|
||||||
implementation(libs.toastcompat)
|
|
||||||
implementation(libs.editorkit)
|
|
||||||
implementation(libs.language.base)
|
|
||||||
implementation(libs.language.json)
|
|
||||||
implementation(libs.quickie.bundled)
|
|
||||||
implementation(libs.core)
|
|
||||||
implementation(libs.work.runtime.ktx)
|
implementation(libs.work.runtime.ktx)
|
||||||
implementation(libs.work.multiprocess)
|
implementation(libs.work.multiprocess)
|
||||||
}
|
|
||||||
|
// Multidex Support
|
||||||
|
implementation(libs.multidex)
|
||||||
|
|
||||||
|
// Testing Libraries
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
testImplementation(libs.org.mockito.mockito.inline)
|
||||||
|
testImplementation(libs.mockito.kotlin)
|
||||||
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
21
V2rayNG/app/proguard-rules.pro
vendored
21
V2rayNG/app/proguard-rules.pro
vendored
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
@@ -41,14 +41,90 @@
|
|||||||
"geosite:private"
|
"geosite:private"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理海外公共DNSIP",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"ip": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"1.0.0.1",
|
||||||
|
"2606:4700:4700::1111",
|
||||||
|
"2606:4700:4700::1001",
|
||||||
|
"1.1.1.2",
|
||||||
|
"1.0.0.2",
|
||||||
|
"2606:4700:4700::1112",
|
||||||
|
"2606:4700:4700::1002",
|
||||||
|
"1.1.1.3",
|
||||||
|
"1.0.0.3",
|
||||||
|
"2606:4700:4700::1113",
|
||||||
|
"2606:4700:4700::1003",
|
||||||
|
"8.8.8.8",
|
||||||
|
"8.8.4.4",
|
||||||
|
"2001:4860:4860::8888",
|
||||||
|
"2001:4860:4860::8844",
|
||||||
|
"94.140.14.14",
|
||||||
|
"94.140.15.15",
|
||||||
|
"2a10:50c0::ad1:ff",
|
||||||
|
"2a10:50c0::ad2:ff",
|
||||||
|
"94.140.14.15",
|
||||||
|
"94.140.15.16",
|
||||||
|
"2a10:50c0::bad1:ff",
|
||||||
|
"2a10:50c0::bad2:ff",
|
||||||
|
"94.140.14.140",
|
||||||
|
"94.140.14.141",
|
||||||
|
"2a10:50c0::1:ff",
|
||||||
|
"2a10:50c0::2:ff",
|
||||||
|
"208.67.222.222",
|
||||||
|
"208.67.220.220",
|
||||||
|
"2620:119:35::35",
|
||||||
|
"2620:119:53::53",
|
||||||
|
"208.67.222.123",
|
||||||
|
"208.67.220.123",
|
||||||
|
"2620:119:35::123",
|
||||||
|
"2620:119:53::123",
|
||||||
|
"9.9.9.9",
|
||||||
|
"149.112.112.112",
|
||||||
|
"2620:fe::9",
|
||||||
|
"2620:fe::fe",
|
||||||
|
"9.9.9.11",
|
||||||
|
"149.112.112.11",
|
||||||
|
"2620:fe::11",
|
||||||
|
"2620:fe::fe:11",
|
||||||
|
"9.9.9.10",
|
||||||
|
"149.112.112.10",
|
||||||
|
"2620:fe::10",
|
||||||
|
"2620:fe::fe:10",
|
||||||
|
"77.88.8.8",
|
||||||
|
"77.88.8.1",
|
||||||
|
"2a02:6b8::feed:0ff",
|
||||||
|
"2a02:6b8:0:1::feed:0ff",
|
||||||
|
"77.88.8.88",
|
||||||
|
"77.88.8.2",
|
||||||
|
"2a02:6b8::feed:bad",
|
||||||
|
"2a02:6b8:0:1::feed:bad",
|
||||||
|
"77.88.8.7",
|
||||||
|
"77.88.8.3",
|
||||||
|
"2a02:6b8::feed:a11",
|
||||||
|
"2a02:6b8:0:1::feed:a11"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理海外公共DNS域名",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"domain:cloudflare-dns.com",
|
||||||
|
"domain:one.one.one.one",
|
||||||
|
"domain:dns.google",
|
||||||
|
"domain:adguard-dns.com",
|
||||||
|
"domain:opendns.com",
|
||||||
|
"domain:umbrella.com",
|
||||||
|
"domain:quad9.net",
|
||||||
|
"domain:yandex.net"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"remarks": "代理IP",
|
"remarks": "代理IP",
|
||||||
"outboundTag": "proxy",
|
"outboundTag": "proxy",
|
||||||
"ip": [
|
"ip": [
|
||||||
"1.0.0.1",
|
|
||||||
"1.1.1.1",
|
|
||||||
"8.8.8.8",
|
|
||||||
"8.8.4.4",
|
|
||||||
"geoip:facebook",
|
"geoip:facebook",
|
||||||
"geoip:fastly",
|
"geoip:fastly",
|
||||||
"geoip:google",
|
"geoip:google",
|
||||||
|
|||||||
@@ -34,29 +34,62 @@
|
|||||||
"geosite:private"
|
"geosite:private"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国公共DNSIP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"223.5.5.5",
|
||||||
|
"223.6.6.6",
|
||||||
|
"2400:3200::1",
|
||||||
|
"2400:3200:baba::1",
|
||||||
|
"119.29.29.29",
|
||||||
|
"1.12.12.12",
|
||||||
|
"120.53.53.53",
|
||||||
|
"2402:4e00::",
|
||||||
|
"2402:4e00:1::",
|
||||||
|
"180.76.76.76",
|
||||||
|
"2400:da00::6666",
|
||||||
|
"114.114.114.114",
|
||||||
|
"114.114.115.115",
|
||||||
|
"114.114.114.119",
|
||||||
|
"114.114.115.119",
|
||||||
|
"114.114.114.110",
|
||||||
|
"114.114.115.110",
|
||||||
|
"180.184.1.1",
|
||||||
|
"180.184.2.2",
|
||||||
|
"101.226.4.6",
|
||||||
|
"218.30.118.6",
|
||||||
|
"123.125.81.6",
|
||||||
|
"140.207.198.6",
|
||||||
|
"1.2.4.8",
|
||||||
|
"210.2.4.8",
|
||||||
|
"52.80.66.66",
|
||||||
|
"117.50.22.22",
|
||||||
|
"2400:7fc0:849e:200::4",
|
||||||
|
"2404:c2c0:85d8:901::4",
|
||||||
|
"117.50.10.10",
|
||||||
|
"52.80.52.52",
|
||||||
|
"2400:7fc0:849e:200::8",
|
||||||
|
"2404:c2c0:85d8:901::8",
|
||||||
|
"117.50.60.30",
|
||||||
|
"52.80.60.30"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国公共DNS域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"domain:alidns.com",
|
||||||
|
"domain:doh.pub",
|
||||||
|
"domain:dot.pub",
|
||||||
|
"domain:360.cn",
|
||||||
|
"domain:onedns.net"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"remarks": "绕过中国IP",
|
"remarks": "绕过中国IP",
|
||||||
"outboundTag": "direct",
|
"outboundTag": "direct",
|
||||||
"ip": [
|
"ip": [
|
||||||
"223.5.5.5/32",
|
|
||||||
"223.6.6.6/32",
|
|
||||||
"2400:3200::1/128",
|
|
||||||
"2400:3200:baba::1/128",
|
|
||||||
"119.29.29.29/32",
|
|
||||||
"1.12.12.12/32",
|
|
||||||
"120.53.53.53/32",
|
|
||||||
"2402:4e00::/128",
|
|
||||||
"2402:4e00:1::/128",
|
|
||||||
"180.76.76.76/32",
|
|
||||||
"2400:da00::6666/128",
|
|
||||||
"114.114.114.114/32",
|
|
||||||
"114.114.115.115/32",
|
|
||||||
"180.184.1.1/32",
|
|
||||||
"180.184.2.2/32",
|
|
||||||
"101.226.4.6/32",
|
|
||||||
"218.30.118.6/32",
|
|
||||||
"123.125.81.6/32",
|
|
||||||
"140.207.198.6/32",
|
|
||||||
"geoip:cn"
|
"geoip:cn"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -64,18 +97,7 @@
|
|||||||
"remarks": "绕过中国域名",
|
"remarks": "绕过中国域名",
|
||||||
"outboundTag": "direct",
|
"outboundTag": "direct",
|
||||||
"domain": [
|
"domain": [
|
||||||
"domain:dns.alidns.com",
|
"geosite:cn"
|
||||||
"domain:doh.pub",
|
|
||||||
"domain:dot.pub",
|
|
||||||
"domain:doh.360.cn",
|
|
||||||
"domain:dot.360.cn",
|
|
||||||
"geosite:cn",
|
|
||||||
"geosite:geolocation-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
@@ -7,6 +7,7 @@ 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.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ class AngApplication : MultiDexApplication() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val workManagerConfiguration: Configuration = Configuration.Builder()
|
private val workManagerConfiguration: Configuration = Configuration.Builder()
|
||||||
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
.setDefaultProcessName("${ANG_PACKAGE}:bg")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -37,16 +38,11 @@ class AngApplication : MultiDexApplication() {
|
|||||||
|
|
||||||
MMKV.initialize(this)
|
MMKV.initialize(this)
|
||||||
|
|
||||||
Utils.setNightMode(application)
|
Utils.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(
|
|
||||||
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
|
||||||
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
|
|
||||||
)!!
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -22,8 +22,10 @@ object AppConfig {
|
|||||||
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
||||||
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
||||||
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
||||||
|
const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy"
|
||||||
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
const val PREF_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"
|
||||||
@@ -47,9 +49,9 @@ object AppConfig {
|
|||||||
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
||||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||||
const val PREF_HTTP_PORT = "pref_http_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"
|
||||||
@@ -111,7 +113,6 @@ object AppConfig {
|
|||||||
/** Ports and addresses for various services. */
|
/** Ports and addresses for various services. */
|
||||||
const val PORT_LOCAL_DNS = "10853"
|
const val PORT_LOCAL_DNS = "10853"
|
||||||
const val PORT_SOCKS = "10808"
|
const val PORT_SOCKS = "10808"
|
||||||
const val PORT_HTTP = "10809"
|
|
||||||
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
||||||
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
||||||
const val WIREGUARD_LOCAL_MTU = "1420"
|
const val WIREGUARD_LOCAL_MTU = "1420"
|
||||||
@@ -161,15 +162,20 @@ object AppConfig {
|
|||||||
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
||||||
|
|
||||||
// Android Private DNS constants
|
// Android Private DNS constants
|
||||||
const val DNS_PUB_DOMAIN = "dns.pub"
|
const val DNS_DNSPOD_DOMAIN = "dot.pub"
|
||||||
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
||||||
const val DNS_ONE_ONE_DOMAIN = "one.one.one.one"
|
const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
|
||||||
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
||||||
|
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
||||||
|
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
||||||
|
|
||||||
val DNS_PUB_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
|
||||||
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_ONE_ONE_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_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
||||||
|
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
|
||||||
|
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
||||||
|
|
||||||
const val DEFAULT_PORT = 443
|
const val DEFAULT_PORT = 443
|
||||||
const val DEFAULT_SECURITY = "auto"
|
const val DEFAULT_SECURITY = "auto"
|
||||||
@@ -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,7 +8,8 @@ enum class Language(val code: String) {
|
|||||||
VIETNAMESE("vi"),
|
VIETNAMESE("vi"),
|
||||||
RUSSIAN("ru"),
|
RUSSIAN("ru"),
|
||||||
PERSIAN("fa"),
|
PERSIAN("fa"),
|
||||||
BANGLA("bn");
|
BANGLA("bn"),
|
||||||
|
BAKHTIARI("bqi-rIR");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromCode(code: String): Language {
|
fun fromCode(code: String): Language {
|
||||||
17
V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt
Normal file
17
V2rayNG/app/src/main/java/com/v2ray/ang/dto/NetworkType.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
enum class NetworkType(val type: String) {
|
||||||
|
TCP("tcp"),
|
||||||
|
KCP("kcp"),
|
||||||
|
WS("ws"),
|
||||||
|
HTTP_UPGRADE("httpupgrade"),
|
||||||
|
XHTTP("xhttp"),
|
||||||
|
HTTP("http"),
|
||||||
|
H2("h2"),
|
||||||
|
//QUIC("quic"),
|
||||||
|
GRPC("grpc");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
||||||
|
}
|
||||||
|
}
|
||||||
120
V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt
Normal file
120
V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileItem.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
|
import com.v2ray.ang.AppConfig.PORT_SOCKS
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
data class ProfileItem(
|
||||||
|
val configVersion: Int = 4,
|
||||||
|
val configType: EConfigType,
|
||||||
|
var subscriptionId: String = "",
|
||||||
|
var addedTime: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
|
var remarks: String = "",
|
||||||
|
var server: String? = null,
|
||||||
|
var serverPort: String? = null,
|
||||||
|
|
||||||
|
var password: String? = null,
|
||||||
|
var method: String? = null,
|
||||||
|
var flow: String? = null,
|
||||||
|
var username: String? = null,
|
||||||
|
|
||||||
|
var network: String? = null,
|
||||||
|
var headerType: String? = null,
|
||||||
|
var host: String? = null,
|
||||||
|
var path: String? = null,
|
||||||
|
var seed: String? = null,
|
||||||
|
var quicSecurity: String? = null,
|
||||||
|
var quicKey: String? = null,
|
||||||
|
var mode: String? = null,
|
||||||
|
var serviceName: String? = null,
|
||||||
|
var authority: String? = null,
|
||||||
|
var xhttpMode: String? = null,
|
||||||
|
var xhttpExtra: String? = null,
|
||||||
|
|
||||||
|
var security: String? = null,
|
||||||
|
var sni: String? = null,
|
||||||
|
var alpn: String? = null,
|
||||||
|
var fingerPrint: String? = null,
|
||||||
|
var insecure: Boolean? = null,
|
||||||
|
|
||||||
|
var publicKey: String? = null,
|
||||||
|
var shortId: String? = null,
|
||||||
|
var spiderX: String? = null,
|
||||||
|
|
||||||
|
var secretKey: String? = null,
|
||||||
|
var preSharedKey: String? = null,
|
||||||
|
var localAddress: String? = null,
|
||||||
|
var reserved: String? = null,
|
||||||
|
var mtu: Int? = null,
|
||||||
|
|
||||||
|
var obfsPassword: String? = null,
|
||||||
|
var portHopping: String? = null,
|
||||||
|
var portHoppingInterval: String? = null,
|
||||||
|
var pinSHA256: String? = null,
|
||||||
|
var bandwidthDown: String? = null,
|
||||||
|
var bandwidthUp: String? = null,
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(configType: EConfigType): ProfileItem {
|
||||||
|
return ProfileItem(configType = configType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllOutboundTags(): MutableList<String> {
|
||||||
|
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerAddressAndPort(): String {
|
||||||
|
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
|
||||||
|
return "$LOOPBACK:$PORT_SOCKS"
|
||||||
|
}
|
||||||
|
return Utils.getIpv6Address(server) + ":" + serverPort
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null) return false
|
||||||
|
val obj = other as ProfileItem
|
||||||
|
|
||||||
|
return (this.server == obj.server
|
||||||
|
&& this.serverPort == obj.serverPort
|
||||||
|
&& this.password == obj.password
|
||||||
|
&& this.method == obj.method
|
||||||
|
&& this.flow == obj.flow
|
||||||
|
&& this.username == obj.username
|
||||||
|
|
||||||
|
&& this.network == obj.network
|
||||||
|
&& this.headerType == obj.headerType
|
||||||
|
&& this.host == obj.host
|
||||||
|
&& this.path == obj.path
|
||||||
|
&& this.seed == obj.seed
|
||||||
|
&& this.quicSecurity == obj.quicSecurity
|
||||||
|
&& this.quicKey == obj.quicKey
|
||||||
|
&& this.mode == obj.mode
|
||||||
|
&& this.serviceName == obj.serviceName
|
||||||
|
&& this.authority == obj.authority
|
||||||
|
&& this.xhttpMode == obj.xhttpMode
|
||||||
|
|
||||||
|
&& this.security == obj.security
|
||||||
|
&& this.sni == obj.sni
|
||||||
|
&& this.alpn == obj.alpn
|
||||||
|
&& this.fingerPrint == obj.fingerPrint
|
||||||
|
&& this.publicKey == obj.publicKey
|
||||||
|
&& this.shortId == obj.shortId
|
||||||
|
|
||||||
|
&& this.secretKey == obj.secretKey
|
||||||
|
&& this.localAddress == obj.localAddress
|
||||||
|
&& this.reserved == obj.reserved
|
||||||
|
&& this.mtu == obj.mtu
|
||||||
|
|
||||||
|
&& this.obfsPassword == obj.obfsPassword
|
||||||
|
&& this.portHopping == obj.portHopping
|
||||||
|
&& this.portHoppingInterval == obj.portHoppingInterval
|
||||||
|
&& this.pinSHA256 == obj.pinSHA256
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,5 +9,5 @@ data class RulesetItem(
|
|||||||
var network: String? = null,
|
var network: String? = null,
|
||||||
var protocol: List<String>? = null,
|
var protocol: List<String>? = null,
|
||||||
var enabled: Boolean = true,
|
var enabled: Boolean = true,
|
||||||
var looked: Boolean? = false,
|
var locked: Boolean? = false,
|
||||||
)
|
)
|
||||||
@@ -195,18 +195,19 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class WireGuardBean(
|
data class WireGuardBean(
|
||||||
var publicKey: String = "",
|
var publicKey: String = "",
|
||||||
|
var preSharedKey: String = "",
|
||||||
var endpoint: String = ""
|
var endpoint: String = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class StreamSettingsBean(
|
data class StreamSettingsBean(
|
||||||
var network: String = AppConfig.DEFAULT_NETWORK,
|
var network: String = AppConfig.DEFAULT_NETWORK,
|
||||||
var security: String = "",
|
var security: String? = null,
|
||||||
var tcpSettings: TcpSettingsBean? = null,
|
var tcpSettings: TcpSettingsBean? = null,
|
||||||
var kcpSettings: KcpSettingsBean? = null,
|
var kcpSettings: KcpSettingsBean? = null,
|
||||||
var wsSettings: WsSettingsBean? = null,
|
var wsSettings: WsSettingsBean? = null,
|
||||||
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
||||||
var splithttpSettings: SplithttpSettingsBean? = null,
|
var xhttpSettings: XhttpSettingsBean? = null,
|
||||||
var httpSettings: HttpSettingsBean? = null,
|
var httpSettings: HttpSettingsBean? = null,
|
||||||
var tlsSettings: TlsSettingsBean? = null,
|
var tlsSettings: TlsSettingsBean? = null,
|
||||||
var quicSettings: QuicSettingBean? = null,
|
var quicSettings: QuicSettingBean? = null,
|
||||||
@@ -275,11 +276,11 @@ data class V2rayConfig(
|
|||||||
val acceptProxyProtocol: Boolean? = null
|
val acceptProxyProtocol: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SplithttpSettingsBean(
|
data class XhttpSettingsBean(
|
||||||
var path: String? = null,
|
var path: String? = null,
|
||||||
var host: String? = null,
|
var host: String? = null,
|
||||||
val maxUploadSize: Int? = null,
|
var mode: String? = null,
|
||||||
val maxConcurrentUploads: Int? = null
|
var extra: Any? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HttpSettingsBean(
|
data class HttpSettingsBean(
|
||||||
@@ -344,14 +345,21 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun populateTransportSettings(
|
fun populateTransportSettings(
|
||||||
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
transport: String,
|
||||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
headerType: String?,
|
||||||
|
host: String?,
|
||||||
|
path: String?,
|
||||||
|
seed: String?,
|
||||||
|
quicSecurity: String?,
|
||||||
|
key: String?,
|
||||||
|
mode: String?,
|
||||||
|
serviceName: String?,
|
||||||
authority: String?
|
authority: String?
|
||||||
): String? {
|
): String? {
|
||||||
var sni: String? = null
|
var sni: String? = null
|
||||||
network = transport
|
network = if (transport.isEmpty()) NetworkType.TCP.type else transport
|
||||||
when (network) {
|
when (network) {
|
||||||
"tcp" -> {
|
NetworkType.TCP.type -> {
|
||||||
val tcpSetting = TcpSettingsBean()
|
val tcpSetting = TcpSettingsBean()
|
||||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
||||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
||||||
@@ -360,16 +368,16 @@ data class V2rayConfig(
|
|||||||
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
tcpSetting.header.request = requestObj
|
tcpSetting.header.request = requestObj
|
||||||
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
|
sni = requestObj.headers.Host?.getOrNull(0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tcpSetting.header.type = "none"
|
tcpSetting.header.type = "none"
|
||||||
sni = host.orEmpty()
|
sni = host
|
||||||
}
|
}
|
||||||
tcpSettings = tcpSetting
|
tcpSettings = tcpSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"kcp" -> {
|
NetworkType.KCP.type -> {
|
||||||
val kcpsetting = KcpSettingsBean()
|
val kcpsetting = KcpSettingsBean()
|
||||||
kcpsetting.header.type = headerType ?: "none"
|
kcpsetting.header.type = headerType ?: "none"
|
||||||
if (seed.isNullOrEmpty()) {
|
if (seed.isNullOrEmpty()) {
|
||||||
@@ -380,55 +388,55 @@ data class V2rayConfig(
|
|||||||
kcpSettings = kcpsetting
|
kcpSettings = kcpsetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"ws" -> {
|
NetworkType.WS.type -> {
|
||||||
val wssetting = WsSettingsBean()
|
val wssetting = WsSettingsBean()
|
||||||
wssetting.headers.Host = host.orEmpty()
|
wssetting.headers.Host = host.orEmpty()
|
||||||
sni = wssetting.headers.Host
|
sni = host
|
||||||
wssetting.path = path ?: "/"
|
wssetting.path = path ?: "/"
|
||||||
wsSettings = wssetting
|
wsSettings = wssetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"httpupgrade" -> {
|
NetworkType.HTTP_UPGRADE.type -> {
|
||||||
val httpupgradeSetting = HttpupgradeSettingsBean()
|
val httpupgradeSetting = HttpupgradeSettingsBean()
|
||||||
httpupgradeSetting.host = host.orEmpty()
|
httpupgradeSetting.host = host.orEmpty()
|
||||||
sni = httpupgradeSetting.host
|
sni = host
|
||||||
httpupgradeSetting.path = path ?: "/"
|
httpupgradeSetting.path = path ?: "/"
|
||||||
httpupgradeSettings = httpupgradeSetting
|
httpupgradeSettings = httpupgradeSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"splithttp" -> {
|
NetworkType.XHTTP.type -> {
|
||||||
val splithttpSetting = SplithttpSettingsBean()
|
val xhttpSetting = XhttpSettingsBean()
|
||||||
splithttpSetting.host = host.orEmpty()
|
xhttpSetting.host = host.orEmpty()
|
||||||
sni = splithttpSetting.host
|
sni = host
|
||||||
splithttpSetting.path = path ?: "/"
|
xhttpSetting.path = path ?: "/"
|
||||||
splithttpSettings = splithttpSetting
|
xhttpSettings = xhttpSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"h2", "http" -> {
|
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
||||||
network = "h2"
|
network = NetworkType.H2.type
|
||||||
val h2Setting = HttpSettingsBean()
|
val h2Setting = HttpSettingsBean()
|
||||||
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
sni = h2Setting.host.getOrNull(0) ?: sni
|
sni = h2Setting.host.getOrNull(0)
|
||||||
h2Setting.path = path ?: "/"
|
h2Setting.path = path ?: "/"
|
||||||
httpSettings = h2Setting
|
httpSettings = h2Setting
|
||||||
}
|
}
|
||||||
|
|
||||||
"quic" -> {
|
// "quic" -> {
|
||||||
val quicsetting = QuicSettingBean()
|
// val quicsetting = QuicSettingBean()
|
||||||
quicsetting.security = quicSecurity ?: "none"
|
// quicsetting.security = quicSecurity ?: "none"
|
||||||
quicsetting.key = key.orEmpty()
|
// quicsetting.key = key.orEmpty()
|
||||||
quicsetting.header.type = headerType ?: "none"
|
// quicsetting.header.type = headerType ?: "none"
|
||||||
quicSettings = quicsetting
|
// quicSettings = quicsetting
|
||||||
}
|
// }
|
||||||
|
|
||||||
"grpc" -> {
|
NetworkType.GRPC.type -> {
|
||||||
val grpcSetting = GrpcSettingsBean()
|
val grpcSetting = GrpcSettingsBean()
|
||||||
grpcSetting.multiMode = mode == "multi"
|
grpcSetting.multiMode = mode == "multi"
|
||||||
grpcSetting.serviceName = serviceName.orEmpty()
|
grpcSetting.serviceName = serviceName.orEmpty()
|
||||||
grpcSetting.authority = authority.orEmpty()
|
grpcSetting.authority = authority.orEmpty()
|
||||||
grpcSetting.idle_timeout = 60
|
grpcSetting.idle_timeout = 60
|
||||||
grpcSetting.health_check_timeout = 20
|
grpcSetting.health_check_timeout = 20
|
||||||
sni = authority.orEmpty()
|
sni = authority
|
||||||
grpcSettings = grpcSetting
|
grpcSettings = grpcSetting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,18 +444,25 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun populateTlsSettings(
|
fun populateTlsSettings(
|
||||||
streamSecurity: String, allowInsecure: Boolean, sni: String?, fingerprint: String?, alpns: String?,
|
streamSecurity: String,
|
||||||
publicKey: String?, shortId: String?, spiderX: String?
|
allowInsecure: Boolean,
|
||||||
|
sni: String?,
|
||||||
|
fingerprint: String?,
|
||||||
|
alpns: String?,
|
||||||
|
publicKey: String?,
|
||||||
|
shortId: String?,
|
||||||
|
spiderX: String?
|
||||||
) {
|
) {
|
||||||
security = streamSecurity
|
security = if (streamSecurity.isEmpty()) null else streamSecurity
|
||||||
|
if (security == null) return
|
||||||
val tlsSetting = TlsSettingsBean(
|
val tlsSetting = TlsSettingsBean(
|
||||||
allowInsecure = allowInsecure,
|
allowInsecure = allowInsecure,
|
||||||
serverName = sni,
|
serverName = if (sni.isNullOrEmpty()) null else sni,
|
||||||
fingerprint = fingerprint,
|
fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
|
||||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
||||||
publicKey = publicKey,
|
publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
|
||||||
shortId = shortId,
|
shortId = if (shortId.isNullOrEmpty()) null else shortId,
|
||||||
spiderX = spiderX
|
spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
|
||||||
)
|
)
|
||||||
if (security == AppConfig.TLS) {
|
if (security == AppConfig.TLS) {
|
||||||
tlsSettings = tlsSetting
|
tlsSettings = tlsSetting
|
||||||
@@ -461,25 +476,25 @@ 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? {
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.vnext?.get(0)?.address
|
return settings?.vnext?.first()?.address
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.get(0)?.address
|
return settings?.servers?.first()?.address
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
|
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -488,16 +503,16 @@ data class V2rayConfig(
|
|||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.vnext?.get(0)?.port
|
return settings?.vnext?.first()?.port
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.get(0)?.port
|
return settings?.servers?.first()?.port
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
|
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -512,16 +527,16 @@ data class V2rayConfig(
|
|||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.vnext?.get(0)?.users?.get(0)?.id
|
return settings?.vnext?.first()?.users?.first()?.id
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.get(0)?.password
|
return settings?.servers?.first()?.password
|
||||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.HTTP.name, true)
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
return settings?.servers?.first()?.users?.first()?.pass
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.secretKey
|
return settings?.secretKey
|
||||||
}
|
}
|
||||||
@@ -530,9 +545,9 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
fun getSecurityEncryption(): String? {
|
fun getSecurityEncryption(): String? {
|
||||||
return when {
|
return when {
|
||||||
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security
|
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security
|
||||||
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption
|
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption
|
||||||
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method
|
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,7 +560,7 @@ data class V2rayConfig(
|
|||||||
) {
|
) {
|
||||||
val transport = streamSettings?.network ?: return null
|
val transport = streamSettings?.network ?: return null
|
||||||
return when (transport) {
|
return when (transport) {
|
||||||
"tcp" -> {
|
NetworkType.TCP.type -> {
|
||||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
tcpSetting.header.type,
|
tcpSetting.header.type,
|
||||||
@@ -554,7 +569,7 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"kcp" -> {
|
NetworkType.KCP.type -> {
|
||||||
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
kcpSetting.header.type,
|
kcpSetting.header.type,
|
||||||
@@ -563,7 +578,7 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"ws" -> {
|
NetworkType.WS.type -> {
|
||||||
val wsSetting = streamSettings?.wsSettings ?: return null
|
val wsSetting = streamSettings?.wsSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
"",
|
"",
|
||||||
@@ -572,7 +587,7 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"httpupgrade" -> {
|
NetworkType.HTTP_UPGRADE.type -> {
|
||||||
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
"",
|
"",
|
||||||
@@ -581,16 +596,16 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"splithttp" -> {
|
NetworkType.XHTTP.type -> {
|
||||||
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
|
val xhttpSettings = streamSettings?.xhttpSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
"",
|
"",
|
||||||
splithttpSetting.host,
|
xhttpSettings.host,
|
||||||
splithttpSetting.path
|
xhttpSettings.path
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"h2" -> {
|
NetworkType.H2.type -> {
|
||||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
val h2Setting = streamSettings?.httpSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
"",
|
"",
|
||||||
@@ -599,16 +614,16 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"quic" -> {
|
// "quic" -> {
|
||||||
val quicSetting = streamSettings?.quicSettings ?: return null
|
// val quicSetting = streamSettings?.quicSettings ?: return null
|
||||||
listOf(
|
// listOf(
|
||||||
quicSetting.header.type,
|
// quicSetting.header.type,
|
||||||
quicSetting.security,
|
// quicSetting.security,
|
||||||
quicSetting.key
|
// quicSetting.key
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
"grpc" -> {
|
NetworkType.GRPC.type -> {
|
||||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
if (grpcSetting.multiMode == true) "multi" else "gun",
|
if (grpcSetting.multiMode == true) "multi" else "gun",
|
||||||
@@ -95,4 +95,4 @@ inline fun <reified T : Serializable> Intent.serializable(key: String): T? = whe
|
|||||||
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
|
fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.v2ray.ang.fmt
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
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.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
@@ -29,9 +30,41 @@ open class FmtBase {
|
|||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
|
||||||
|
config.network = queryParam["type"] ?: NetworkType.TCP.type
|
||||||
|
config.headerType = queryParam["headerType"]
|
||||||
|
config.host = queryParam["host"]
|
||||||
|
config.path = queryParam["path"]
|
||||||
|
|
||||||
|
config.seed = queryParam["seed"]
|
||||||
|
config.quicSecurity = queryParam["quicSecurity"]
|
||||||
|
config.quicKey = queryParam["key"]
|
||||||
|
config.mode = queryParam["mode"]
|
||||||
|
config.serviceName = queryParam["serviceName"]
|
||||||
|
config.authority = queryParam["authority"]
|
||||||
|
config.xhttpMode = queryParam["mode"]
|
||||||
|
config.xhttpExtra = queryParam["extra"]
|
||||||
|
|
||||||
|
config.security = queryParam["security"]
|
||||||
|
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
|
||||||
|
config.security = null
|
||||||
|
}
|
||||||
|
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
||||||
|
allowInsecure
|
||||||
|
} else {
|
||||||
|
queryParam["allowInsecure"].orEmpty() == "1"
|
||||||
|
}
|
||||||
|
config.sni = queryParam["sni"]
|
||||||
|
config.fingerPrint = queryParam["fp"]
|
||||||
|
config.alpn = queryParam["alpn"]
|
||||||
|
config.publicKey = queryParam["pbk"]
|
||||||
|
config.shortId = queryParam["sid"]
|
||||||
|
config.spiderX = queryParam["spx"]
|
||||||
|
config.flow = queryParam["flow"]
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
||||||
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
||||||
@@ -41,37 +74,45 @@ open class FmtBase {
|
|||||||
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
|
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
|
||||||
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
|
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
|
||||||
|
|
||||||
dicQuery["type"] = config.network?.ifEmpty { AppConfig.DEFAULT_NETWORK }.orEmpty()
|
val networkType = NetworkType.fromString(config.network)
|
||||||
|
dicQuery["type"] = networkType.type
|
||||||
|
|
||||||
when (config.network) {
|
when (networkType) {
|
||||||
"tcp" -> {
|
NetworkType.TCP -> {
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
"kcp" -> {
|
NetworkType.KCP -> {
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||||
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
|
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
"ws", "httpupgrade", "splithttp" -> {
|
NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
|
||||||
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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
"http", "h2" -> {
|
NetworkType.XHTTP -> {
|
||||||
|
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||||
|
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||||
|
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
||||||
|
config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.HTTP, NetworkType.H2 -> {
|
||||||
dicQuery["type"] = "http"
|
dicQuery["type"] = "http"
|
||||||
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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
"quic" -> {
|
// NetworkType.QUIC -> {
|
||||||
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||||
config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
|
// config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
|
||||||
config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
|
// config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
|
||||||
}
|
// }
|
||||||
|
|
||||||
"grpc" -> {
|
NetworkType.GRPC -> {
|
||||||
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
||||||
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
|
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
|
||||||
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
|
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
|
||||||
@@ -10,7 +10,7 @@ object HttpFmt : FmtBase() {
|
|||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = profileItem.server.orEmpty()
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
@@ -85,6 +85,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 +102,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}",
|
||||||
),
|
),
|
||||||
139
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
Normal file
139
V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.NetworkType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object ShadowsocksFmt : FmtBase() {
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
return parseSip002(str) ?: parseLegacy(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseSip002(str: String): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.idnHost.isEmpty()) return null
|
||||||
|
if (uri.port <= 0) return null
|
||||||
|
if (uri.userInfo.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
config.server = uri.idnHost
|
||||||
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
val result = if (uri.userInfo.contains(":")) {
|
||||||
|
uri.userInfo.split(":", limit = 2)
|
||||||
|
} else {
|
||||||
|
Utils.decode(uri.userInfo).split(":", limit = 2)
|
||||||
|
}
|
||||||
|
if (result.count() == 2) {
|
||||||
|
config.method = result.first()
|
||||||
|
config.password = result.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uri.rawQuery.isNullOrEmpty()) {
|
||||||
|
val queryParam = getQueryParam(uri)
|
||||||
|
if (queryParam["plugin"]?.contains("obfs=http") == true) {
|
||||||
|
val queryPairs = HashMap<String, String>()
|
||||||
|
for (pair in queryParam["plugin"]?.split(";") ?: listOf()) {
|
||||||
|
val idx = pair.split("=")
|
||||||
|
if (idx.count() == 2) {
|
||||||
|
queryPairs.put(idx.first(), idx.last())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.network = NetworkType.TCP.type
|
||||||
|
config.headerType = "http"
|
||||||
|
config.host = queryPairs["obfs-host"]
|
||||||
|
config.path = queryPairs["path"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseLegacy(str: String): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||||
|
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||||
|
val indexSplit = result.indexOf("#")
|
||||||
|
if (indexSplit > 0) {
|
||||||
|
try {
|
||||||
|
config.remarks =
|
||||||
|
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.substring(0, indexSplit)
|
||||||
|
}
|
||||||
|
|
||||||
|
//part decode
|
||||||
|
val indexS = result.indexOf("@")
|
||||||
|
result = if (indexS > 0) {
|
||||||
|
Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||||
|
indexS,
|
||||||
|
result.length
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Utils.decode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
|
||||||
|
val match = legacyPattern.matchEntire(result) ?: return null
|
||||||
|
|
||||||
|
config.server = match.groupValues[3].removeSurrounding("[", "]")
|
||||||
|
config.serverPort = match.groupValues[4]
|
||||||
|
config.password = match.groupValues[2]
|
||||||
|
config.method = match.groupValues[1].lowercase()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ProfileItem): String {
|
||||||
|
val pw = "${config.method}:${config.password}"
|
||||||
|
|
||||||
|
return toUri(config, Utils.encode(pw), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = profileItem.server.orEmpty()
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
server.password = profileItem.password
|
||||||
|
server.method = profileItem.method
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
|
profileItem.network.orEmpty(),
|
||||||
|
profileItem.headerType,
|
||||||
|
profileItem.host,
|
||||||
|
profileItem.path,
|
||||||
|
profileItem.seed,
|
||||||
|
profileItem.quicSecurity,
|
||||||
|
profileItem.quicKey,
|
||||||
|
profileItem.mode,
|
||||||
|
profileItem.serviceName,
|
||||||
|
profileItem.authority,
|
||||||
|
)
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
|
profileItem.security.orEmpty(),
|
||||||
|
profileItem.insecure == true,
|
||||||
|
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
||||||
|
profileItem.fingerPrint,
|
||||||
|
profileItem.alpn,
|
||||||
|
profileItem.publicKey,
|
||||||
|
profileItem.shortId,
|
||||||
|
profileItem.spiderX,
|
||||||
|
)
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -15,13 +15,14 @@ object SocksFmt : FmtBase() {
|
|||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
if (uri.idnHost.isEmpty()) return null
|
if (uri.idnHost.isEmpty()) return null
|
||||||
|
if (uri.port <= 0) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
if (uri.userInfo?.isEmpty() == false) {
|
if (uri.userInfo?.isEmpty() == false) {
|
||||||
val result = Utils.decode(uri.userInfo).split(":")
|
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
|
||||||
if (result.count() == 2) {
|
if (result.count() == 2) {
|
||||||
config.username = result.first()
|
config.username = result.first()
|
||||||
config.password = result.last()
|
config.password = result.last()
|
||||||
@@ -38,13 +39,13 @@ object SocksFmt : FmtBase() {
|
|||||||
else
|
else
|
||||||
":"
|
":"
|
||||||
|
|
||||||
return toUri(config, pw, null)
|
return toUri(config, Utils.encode(pw), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = profileItem.server.orEmpty()
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
@@ -2,6 +2,7 @@ package com.v2ray.ang.fmt
|
|||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.NetworkType
|
||||||
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
|
||||||
@@ -22,37 +23,14 @@ object TrojanFmt : FmtBase() {
|
|||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) {
|
if (uri.rawQuery.isNullOrEmpty()) {
|
||||||
|
config.network = NetworkType.TCP.type
|
||||||
config.security = AppConfig.TLS
|
config.security = AppConfig.TLS
|
||||||
config.insecure = allowInsecure
|
config.insecure = allowInsecure
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.network = queryParam["type"] ?: "tcp"
|
getItemFormQuery(config, queryParam, allowInsecure)
|
||||||
config.headerType = queryParam["headerType"]
|
config.security = queryParam["security"] ?: AppConfig.TLS
|
||||||
config.host = queryParam["host"]
|
|
||||||
config.path = queryParam["path"]
|
|
||||||
|
|
||||||
config.seed = queryParam["seed"]
|
|
||||||
config.quicSecurity = queryParam["quicSecurity"]
|
|
||||||
config.quicKey = queryParam["key"]
|
|
||||||
config.mode = queryParam["mode"]
|
|
||||||
config.serviceName = queryParam["serviceName"]
|
|
||||||
config.authority = queryParam["authority"]
|
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
|
||||||
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
|
||||||
allowInsecure
|
|
||||||
} else {
|
|
||||||
queryParam["allowInsecure"].orEmpty() == "1"
|
|
||||||
}
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.fingerPrint = queryParam["fp"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
config.publicKey = queryParam["pbk"]
|
|
||||||
config.shortId = queryParam["sid"]
|
|
||||||
config.spiderX = queryParam["spx"]
|
|
||||||
config.flow = queryParam["flow"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@@ -67,14 +45,14 @@ object TrojanFmt : FmtBase() {
|
|||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.get(0)?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = profileItem.server.orEmpty()
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
server.password = profileItem.password
|
server.password = profileItem.password
|
||||||
server.flow = profileItem.flow
|
server.flow = profileItem.flow
|
||||||
}
|
}
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
profileItem.network.orEmpty(),
|
profileItem.network.orEmpty(),
|
||||||
profileItem.headerType,
|
profileItem.headerType,
|
||||||
profileItem.host,
|
profileItem.host,
|
||||||
@@ -90,7 +68,7 @@ object TrojanFmt : FmtBase() {
|
|||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
profileItem.security.orEmpty(),
|
profileItem.security.orEmpty(),
|
||||||
profileItem.insecure == true,
|
profileItem.insecure == true,
|
||||||
profileItem.sni,
|
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
||||||
profileItem.fingerPrint,
|
profileItem.fingerPrint,
|
||||||
profileItem.alpn,
|
profileItem.alpn,
|
||||||
profileItem.publicKey,
|
profileItem.publicKey,
|
||||||
@@ -6,6 +6,7 @@ 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.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
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
|
||||||
|
|
||||||
@@ -25,31 +26,7 @@ object VlessFmt : FmtBase() {
|
|||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
config.method = queryParam["encryption"] ?: "none"
|
config.method = queryParam["encryption"] ?: "none"
|
||||||
|
|
||||||
config.network = queryParam["type"] ?: "tcp"
|
getItemFormQuery(config, queryParam, allowInsecure)
|
||||||
config.headerType = queryParam["headerType"]
|
|
||||||
config.host = queryParam["host"]
|
|
||||||
config.path = queryParam["path"]
|
|
||||||
|
|
||||||
config.seed = queryParam["seed"]
|
|
||||||
config.quicSecurity = queryParam["quicSecurity"]
|
|
||||||
config.quicKey = queryParam["key"]
|
|
||||||
config.mode = queryParam["mode"]
|
|
||||||
config.serviceName = queryParam["serviceName"]
|
|
||||||
config.authority = queryParam["authority"]
|
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
|
||||||
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
|
||||||
allowInsecure
|
|
||||||
} else {
|
|
||||||
queryParam["allowInsecure"].orEmpty() == "1"
|
|
||||||
}
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.fingerPrint = queryParam["fp"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
config.publicKey = queryParam["pbk"]
|
|
||||||
config.shortId = queryParam["sid"]
|
|
||||||
config.spiderX = queryParam["spx"]
|
|
||||||
config.flow = queryParam["flow"]
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@@ -65,7 +42,7 @@ object VlessFmt : FmtBase() {
|
|||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
vnext.address = profileItem.server.orEmpty()
|
vnext.address = profileItem.server.orEmpty()
|
||||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
vnext.users[0].id = profileItem.password.orEmpty()
|
vnext.users[0].id = profileItem.password.orEmpty()
|
||||||
@@ -73,7 +50,7 @@ object VlessFmt : FmtBase() {
|
|||||||
vnext.users[0].flow = profileItem.flow
|
vnext.users[0].flow = profileItem.flow
|
||||||
}
|
}
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
profileItem.network.orEmpty(),
|
profileItem.network.orEmpty(),
|
||||||
profileItem.headerType,
|
profileItem.headerType,
|
||||||
profileItem.host,
|
profileItem.host,
|
||||||
@@ -85,11 +62,13 @@ object VlessFmt : FmtBase() {
|
|||||||
profileItem.serviceName,
|
profileItem.serviceName,
|
||||||
profileItem.authority,
|
profileItem.authority,
|
||||||
)
|
)
|
||||||
|
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
|
||||||
|
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
profileItem.security.orEmpty(),
|
profileItem.security.orEmpty(),
|
||||||
profileItem.insecure == true,
|
profileItem.insecure == true,
|
||||||
profileItem.sni,
|
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
||||||
profileItem.fingerPrint,
|
profileItem.fingerPrint,
|
||||||
profileItem.alpn,
|
profileItem.alpn,
|
||||||
profileItem.publicKey,
|
profileItem.publicKey,
|
||||||
@@ -4,6 +4,7 @@ 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.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.NetworkType
|
||||||
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.dto.VmessQRCode
|
import com.v2ray.ang.dto.VmessQRCode
|
||||||
@@ -47,27 +48,29 @@ object VmessFmt : FmtBase() {
|
|||||||
config.password = vmessQRCode.id
|
config.password = vmessQRCode.id
|
||||||
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
||||||
|
|
||||||
config.network = vmessQRCode.net ?: "tcp"
|
config.network = vmessQRCode.net ?: NetworkType.TCP.type
|
||||||
config.headerType = vmessQRCode.type
|
config.headerType = vmessQRCode.type
|
||||||
config.host = vmessQRCode.host
|
config.host = vmessQRCode.host
|
||||||
config.path = vmessQRCode.path
|
config.path = vmessQRCode.path
|
||||||
|
|
||||||
when (config.network) {
|
when (NetworkType.fromString(config.network)) {
|
||||||
"kcp" -> {
|
NetworkType.KCP -> {
|
||||||
config.seed = vmessQRCode.path
|
config.seed = vmessQRCode.path
|
||||||
}
|
}
|
||||||
|
|
||||||
"quic" -> {
|
// NetworkType.QUIC -> {
|
||||||
config.quicSecurity = vmessQRCode.host
|
// config.quicSecurity = vmessQRCode.host
|
||||||
config.quicKey = vmessQRCode.path
|
// config.quicKey = vmessQRCode.path
|
||||||
}
|
// }
|
||||||
|
|
||||||
"grpc" -> {
|
NetworkType.GRPC -> {
|
||||||
config.mode = vmessQRCode.type
|
config.mode = vmessQRCode.type
|
||||||
config.serviceName = vmessQRCode.path
|
config.serviceName = vmessQRCode.path
|
||||||
config.authority = vmessQRCode.host
|
config.authority = vmessQRCode.host
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.security = vmessQRCode.tls
|
config.security = vmessQRCode.tls
|
||||||
config.insecure = allowInsecure
|
config.insecure = allowInsecure
|
||||||
config.sni = vmessQRCode.sni
|
config.sni = vmessQRCode.sni
|
||||||
@@ -90,22 +93,24 @@ object VmessFmt : FmtBase() {
|
|||||||
|
|
||||||
vmessQRCode.net = config.network.orEmpty()
|
vmessQRCode.net = config.network.orEmpty()
|
||||||
vmessQRCode.type = config.headerType.orEmpty()
|
vmessQRCode.type = config.headerType.orEmpty()
|
||||||
when (config.network) {
|
when (NetworkType.fromString(config.network)) {
|
||||||
"kcp" -> {
|
NetworkType.KCP -> {
|
||||||
vmessQRCode.path = config.seed.orEmpty()
|
vmessQRCode.path = config.seed.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
"quic" -> {
|
// NetworkType.QUIC -> {
|
||||||
vmessQRCode.host = config.quicSecurity.orEmpty()
|
// vmessQRCode.host = config.quicSecurity.orEmpty()
|
||||||
vmessQRCode.path = config.quicKey.orEmpty()
|
// vmessQRCode.path = config.quicKey.orEmpty()
|
||||||
}
|
// }
|
||||||
|
|
||||||
"grpc" -> {
|
NetworkType.GRPC -> {
|
||||||
vmessQRCode.type = config.mode.orEmpty()
|
vmessQRCode.type = config.mode.orEmpty()
|
||||||
vmessQRCode.path = config.serviceName.orEmpty()
|
vmessQRCode.path = config.serviceName.orEmpty()
|
||||||
vmessQRCode.host = config.authority.orEmpty()
|
vmessQRCode.host = config.authority.orEmpty()
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
|
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
|
||||||
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
|
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
|
||||||
|
|
||||||
@@ -119,7 +124,7 @@ object VmessFmt : FmtBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun parseVmessStd(str: String): ProfileItem? {
|
fun parseVmessStd(str: String): ProfileItem? {
|
||||||
var 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)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
@@ -132,23 +137,7 @@ object VmessFmt : FmtBase() {
|
|||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
config.method = AppConfig.DEFAULT_SECURITY
|
config.method = AppConfig.DEFAULT_SECURITY
|
||||||
|
|
||||||
config.network = queryParam["type"] ?: "tcp"
|
getItemFormQuery(config, queryParam, allowInsecure)
|
||||||
config.headerType = queryParam["headerType"]
|
|
||||||
config.host = queryParam["host"]
|
|
||||||
config.path = queryParam["path"]
|
|
||||||
|
|
||||||
config.seed = queryParam["seed"]
|
|
||||||
config.quicSecurity = queryParam["quicSecurity"]
|
|
||||||
config.quicKey = queryParam["key"]
|
|
||||||
config.mode = queryParam["mode"]
|
|
||||||
config.serviceName = queryParam["serviceName"]
|
|
||||||
config.authority = queryParam["authority"]
|
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
|
||||||
config.insecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
|
||||||
config.sni = queryParam["sni"]
|
|
||||||
config.fingerPrint = queryParam["fp"]
|
|
||||||
config.alpn = queryParam["alpn"]
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
@@ -157,14 +146,14 @@ object VmessFmt : FmtBase() {
|
|||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
vnext.address = profileItem.server.orEmpty()
|
vnext.address = profileItem.server.orEmpty()
|
||||||
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
vnext.users[0].id = profileItem.password.orEmpty()
|
vnext.users[0].id = profileItem.password.orEmpty()
|
||||||
vnext.users[0].security = profileItem.method
|
vnext.users[0].security = profileItem.method
|
||||||
}
|
}
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
profileItem.network.orEmpty(),
|
profileItem.network.orEmpty(),
|
||||||
profileItem.headerType,
|
profileItem.headerType,
|
||||||
profileItem.host,
|
profileItem.host,
|
||||||
@@ -180,7 +169,7 @@ object VmessFmt : FmtBase() {
|
|||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
profileItem.security.orEmpty(),
|
profileItem.security.orEmpty(),
|
||||||
profileItem.insecure == true,
|
profileItem.insecure == true,
|
||||||
profileItem.sni,
|
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
||||||
profileItem.fingerPrint,
|
profileItem.fingerPrint,
|
||||||
profileItem.alpn,
|
profileItem.alpn,
|
||||||
null,
|
null,
|
||||||
@@ -22,9 +22,10 @@ object WireguardFmt : FmtBase() {
|
|||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
config.secretKey = uri.userInfo
|
config.secretKey = uri.userInfo.orEmpty()
|
||||||
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
||||||
config.publicKey = queryParam["publickey"].orEmpty()
|
config.publicKey = queryParam["publickey"].orEmpty()
|
||||||
|
config.preSharedKey = queryParam["presharedkey"].orEmpty()
|
||||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
||||||
|
|
||||||
@@ -33,33 +34,75 @@ object WireguardFmt : FmtBase() {
|
|||||||
|
|
||||||
fun parseWireguardConfFile(str: String): ProfileItem? {
|
fun parseWireguardConfFile(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
val queryParam: MutableMap<String, String> = mutableMapOf()
|
|
||||||
|
val interfaceParams: MutableMap<String, String> = mutableMapOf()
|
||||||
|
val peerParams: MutableMap<String, String> = mutableMapOf()
|
||||||
|
|
||||||
var currentSection: String? = null
|
var currentSection: String? = null
|
||||||
|
|
||||||
str.lines().forEach { line ->
|
str.lines().forEach { line ->
|
||||||
val trimmedLine = line.trim()
|
val trimmedLine = line.trim()
|
||||||
|
|
||||||
|
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
|
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
|
||||||
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
|
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
|
||||||
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
|
else -> {
|
||||||
currentSection != null -> {
|
if (currentSection != null) {
|
||||||
val (key, value) = trimmedLine.split("=").map { it.trim() }
|
val parts = trimmedLine.split("=", limit = 2).map { it.trim() }
|
||||||
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
|
if (parts.size == 2) {
|
||||||
|
val key = parts[0].lowercase()
|
||||||
|
val value = parts[1]
|
||||||
|
when (currentSection) {
|
||||||
|
"Interface" -> interfaceParams[key] = value
|
||||||
|
"Peer" -> peerParams[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.secretKey = queryParam["privatekey"].orEmpty()
|
config.secretKey = interfaceParams["privatekey"].orEmpty()
|
||||||
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
config.remarks = System.currentTimeMillis().toString()
|
||||||
config.publicKey = queryParam["publickey"].orEmpty()
|
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
config.publicKey = peerParams["publickey"].orEmpty()
|
||||||
|
config.preSharedKey = peerParams["presharedkey"].orEmpty()
|
||||||
|
val endpoint = peerParams["endpoint"].orEmpty()
|
||||||
|
val endpointParts = endpoint.split(":", limit = 2)
|
||||||
|
if (endpointParts.size == 2) {
|
||||||
|
config.server = endpointParts[0]
|
||||||
|
config.serverPort = endpointParts[1]
|
||||||
|
} else {
|
||||||
|
config.server = endpoint
|
||||||
|
config.serverPort = ""
|
||||||
|
}
|
||||||
|
config.reserved = peerParams["reserved"] ?: "0,0,0"
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
|
outboundBean?.settings?.let { wireguard ->
|
||||||
|
wireguard.secretKey = profileItem.secretKey
|
||||||
|
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||||
|
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||||
|
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||||
|
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
|
||||||
|
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||||
|
}
|
||||||
|
wireguard.mtu = profileItem.mtu
|
||||||
|
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
fun toUri(config: ProfileItem): String {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = HashMap<String, String>()
|
val dicQuery = HashMap<String, String>()
|
||||||
@@ -72,24 +115,10 @@ object WireguardFmt : FmtBase() {
|
|||||||
if (config.mtu != null) {
|
if (config.mtu != null) {
|
||||||
dicQuery["mtu"] = config.mtu.toString()
|
dicQuery["mtu"] = config.mtu.toString()
|
||||||
}
|
}
|
||||||
|
if (config.preSharedKey != null) {
|
||||||
|
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
return toUri(config, config.secretKey, dicQuery)
|
return toUri(config, config.secretKey, dicQuery)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
|
||||||
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
|
||||||
|
|
||||||
outboundBean?.settings?.let { wireguard ->
|
|
||||||
wireguard.secretKey = profileItem.secretKey
|
|
||||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
|
||||||
wireguard.peers?.get(0)?.publicKey = profileItem.publicKey.orEmpty()
|
|
||||||
wireguard.peers?.get(0)?.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
|
||||||
wireguard.mtu = profileItem.mtu?.toInt()
|
|
||||||
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
|
|
||||||
}
|
|
||||||
|
|
||||||
return outboundBean
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -138,11 +138,11 @@ object AngConfigManager {
|
|||||||
if (sb.count() > 0) {
|
if (sb.count() > 0) {
|
||||||
Utils.setClipboard(context, sb.toString())
|
Utils.setClipboard(context, sb.toString())
|
||||||
}
|
}
|
||||||
|
return sb.lines().count()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,6 +170,13 @@ object AngConfigManager {
|
|||||||
if (guid == null) return -1
|
if (guid == null) return -1
|
||||||
val result = V2rayConfigManager.getV2rayConfig(context, guid)
|
val result = V2rayConfigManager.getV2rayConfig(context, guid)
|
||||||
if (result.status) {
|
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)
|
Utils.setClipboard(context, result.content)
|
||||||
} else {
|
} else {
|
||||||
return -1
|
return -1
|
||||||
@@ -277,10 +284,10 @@ object AngConfigManager {
|
|||||||
if (serverList.isNotEmpty()) {
|
if (serverList.isNotEmpty()) {
|
||||||
var count = 0
|
var count = 0
|
||||||
for (srv in serverList.reversed()) {
|
for (srv in serverList.reversed()) {
|
||||||
val config = CustomFmt.parse(server) ?: continue
|
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||||
config.subscriptionId = subid
|
config.subscriptionId = subid
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv))
|
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
@@ -5,6 +5,7 @@ import com.tencent.mmkv.MMKV
|
|||||||
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.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
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.handler.MmkvManager.decodeServerConfig
|
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||||
@@ -19,7 +20,7 @@ object MigrateManager {
|
|||||||
if (serverStorage.count().toInt() == 0) {
|
if (serverStorage.count().toInt() == 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var serverList = serverStorage.allKeys() ?: return false
|
val serverList = serverStorage.allKeys() ?: return false
|
||||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
|
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
|
||||||
|
|
||||||
for (guid in serverList) {
|
for (guid in serverList) {
|
||||||
@@ -36,7 +37,7 @@ object MigrateManager {
|
|||||||
|
|
||||||
//check and remove old
|
//check and remove old
|
||||||
decodeServerConfig(guid) ?: continue
|
decodeServerConfig(guid) ?: continue
|
||||||
//serverStorage.remove(guid)
|
serverStorage.remove(guid)
|
||||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
|
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
|
||||||
}
|
}
|
||||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
|
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
|
||||||
@@ -70,10 +71,10 @@ object MigrateManager {
|
|||||||
config.serverPort = outbound.getServerPort().toString()
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
config.method = outbound.getSecurityEncryption()
|
config.method = outbound.getSecurityEncryption()
|
||||||
config.password = outbound.getPassword()
|
config.password = outbound.getPassword()
|
||||||
config.flow = outbound?.settings?.vnext?.get(0)?.users[0]?.flow ?: outbound?.settings?.servers?.get(0)?.flow
|
config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
|
||||||
|
|
||||||
|
config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type
|
||||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||||
config.network = "tcp"
|
|
||||||
config.headerType = transportDetails[0].orEmpty()
|
config.headerType = transportDetails[0].orEmpty()
|
||||||
config.host = transportDetails[1].orEmpty()
|
config.host = transportDetails[1].orEmpty()
|
||||||
config.path = transportDetails[2].orEmpty()
|
config.path = transportDetails[2].orEmpty()
|
||||||
@@ -107,7 +108,7 @@ object MigrateManager {
|
|||||||
config.remarks = configOld.remarks
|
config.remarks = configOld.remarks
|
||||||
config.server = outbound.getServerAddress()
|
config.server = outbound.getServerAddress()
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
config.username = outbound.settings?.servers?.get(0)?.users?.get(0)?.user
|
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
|
||||||
config.password = outbound.getPassword()
|
config.password = outbound.getPassword()
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@@ -120,7 +121,7 @@ object MigrateManager {
|
|||||||
config.remarks = configOld.remarks
|
config.remarks = configOld.remarks
|
||||||
config.server = outbound.getServerAddress()
|
config.server = outbound.getServerAddress()
|
||||||
config.serverPort = outbound.getServerPort().toString()
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
config.username = outbound.settings?.servers?.get(0)?.users?.get(0)?.user
|
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
|
||||||
config.password = outbound.getPassword()
|
config.password = outbound.getPassword()
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@@ -164,18 +164,22 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllServer() {
|
fun removeAllServer(): Int {
|
||||||
|
val count = profileFullStorage.allKeys()?.count() ?: 0
|
||||||
mainStorage.clearAll()
|
mainStorage.clearAll()
|
||||||
profileFullStorage.clearAll()
|
profileFullStorage.clearAll()
|
||||||
//profileStorage.clearAll()
|
//profileStorage.clearAll()
|
||||||
serverAffStorage.clearAll()
|
serverAffStorage.clearAll()
|
||||||
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeInvalidServer(guid: String) {
|
fun removeInvalidServer(guid: String): Int {
|
||||||
|
var count = 0
|
||||||
if (guid.isNotEmpty()) {
|
if (guid.isNotEmpty()) {
|
||||||
decodeServerAffiliationInfo(guid)?.let { aff ->
|
decodeServerAffiliationInfo(guid)?.let { aff ->
|
||||||
if (aff.testDelayMillis < 0L) {
|
if (aff.testDelayMillis < 0L) {
|
||||||
removeServer(guid)
|
removeServer(guid)
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -183,10 +187,12 @@ object MmkvManager {
|
|||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
if (aff.testDelayMillis < 0L) {
|
if (aff.testDelayMillis < 0L) {
|
||||||
removeServer(key)
|
removeServer(key)
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encodeServerRaw(guid: String, config: String) {
|
fun encodeServerRaw(guid: String, config: String) {
|
||||||
@@ -336,17 +342,13 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun decodeSettingsBool(key: String): Boolean {
|
fun decodeSettingsBool(key: String): Boolean {
|
||||||
return settingsStorage.decodeBool(key)
|
return settingsStorage.decodeBool(key, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
|
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
|
||||||
return settingsStorage.decodeStringSet(key)
|
return settingsStorage.decodeStringSet(key)
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,11 @@ 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.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
|
||||||
@@ -43,12 +45,12 @@ object SettingsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun resetRoutingRulesets(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetRoutingRulesetsFromClipboard(content: String?): Boolean {
|
fun resetRoutingRulesets(content: String?): Boolean {
|
||||||
if (content.isNullOrEmpty()) {
|
if (content.isNullOrEmpty()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -70,7 +72,7 @@ object SettingsManager {
|
|||||||
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 ->
|
||||||
if (key.looked == true) {
|
if (key.locked == true) {
|
||||||
rulesetNew.add(key)
|
rulesetNew.add(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,11 +93,13 @@ object SettingsManager {
|
|||||||
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(ruleset)
|
rulesetList.add(0, ruleset)
|
||||||
} else {
|
} else {
|
||||||
rulesetList[index] = ruleset
|
rulesetList[index] = ruleset
|
||||||
}
|
}
|
||||||
@@ -113,6 +117,25 @@ object SettingsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
//Follow config
|
||||||
|
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
|
||||||
@@ -155,7 +178,7 @@ object SettingsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getHttpPort(): Int {
|
fun getHttpPort(): Int {
|
||||||
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
return getSocksPort() + (if (Utils.isXray()) 0 else 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initAssets(context: Context, assets: AssetManager) {
|
fun initAssets(context: Context, assets: AssetManager) {
|
||||||
@@ -8,12 +8,16 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|||||||
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
|
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
|
||||||
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
|
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
|
||||||
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
|
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_ADDRESSES
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_DOMAIN
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_DNSPOD_ADDRESSES
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_DNSPOD_DOMAIN
|
||||||
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
|
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
|
||||||
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
|
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
|
||||||
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_ADDRESSES
|
import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES
|
||||||
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_DOMAIN
|
import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN
|
||||||
import com.v2ray.ang.AppConfig.DNS_PUB_ADDRESSES
|
import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES
|
||||||
import com.v2ray.ang.AppConfig.DNS_PUB_DOMAIN
|
import com.v2ray.ang.AppConfig.DNS_YANDEX_DOMAIN
|
||||||
import com.v2ray.ang.AppConfig.GEOIP_CN
|
import com.v2ray.ang.AppConfig.GEOIP_CN
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_CN
|
import com.v2ray.ang.AppConfig.GEOSITE_CN
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
@@ -30,10 +34,12 @@ import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
|||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
||||||
import com.v2ray.ang.dto.ConfigResult
|
import com.v2ray.ang.dto.ConfigResult
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
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
|
||||||
@@ -57,7 +63,7 @@ object V2rayConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val result = getV2rayNonCustomConfig(context, config)
|
val result = getV2rayNonCustomConfig(context, config)
|
||||||
Log.d(ANG_PACKAGE, result.content)
|
//Log.d(ANG_PACKAGE, result.content)
|
||||||
result.guid = guid
|
result.guid = guid
|
||||||
return result
|
return result
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -116,7 +122,6 @@ object V2rayConfigManager {
|
|||||||
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
val socksPort = SettingsManager.getSocksPort()
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
|
||||||
|
|
||||||
v2rayConfig.inbounds.forEach { curInbound ->
|
v2rayConfig.inbounds.forEach { curInbound ->
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
||||||
@@ -138,14 +143,13 @@ object V2rayConfigManager {
|
|||||||
v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
|
v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
|
||||||
}
|
}
|
||||||
|
|
||||||
v2rayConfig.inbounds[1].port = httpPort
|
if (Utils.isXray()) {
|
||||||
|
v2rayConfig.inbounds.removeAt(1)
|
||||||
|
} else {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
v2rayConfig.inbounds[1].port = httpPort
|
||||||
|
}
|
||||||
|
|
||||||
// if (httpPort > 0) {
|
|
||||||
// val httpCopy = v2rayConfig.inbounds[0].copy()
|
|
||||||
// httpCopy.port = httpPort
|
|
||||||
// httpCopy.protocol = "http"
|
|
||||||
// v2rayConfig.inbounds.add(httpCopy)
|
|
||||||
// }
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return false
|
return false
|
||||||
@@ -236,7 +240,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:"))
|
||||||
@@ -330,11 +334,11 @@ object V2rayConfigManager {
|
|||||||
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(),
|
||||||
domains = proxyDomain,
|
domains = proxyDomain,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -344,7 +348,7 @@ object V2rayConfigManager {
|
|||||||
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(),
|
||||||
@@ -366,9 +370,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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,10 +394,12 @@ object V2rayConfigManager {
|
|||||||
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
|
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
|
||||||
|
|
||||||
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
||||||
hosts[DNS_PUB_DOMAIN] = DNS_PUB_ADDRESSES
|
|
||||||
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
|
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
|
||||||
hosts[DNS_ONE_ONE_DOMAIN] = DNS_ONE_ONE_ADDRESSES
|
hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_ADDRESSES
|
||||||
|
hosts[DNS_DNSPOD_DOMAIN] = DNS_DNSPOD_ADDRESSES
|
||||||
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
|
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
|
||||||
|
hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES
|
||||||
|
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
|
||||||
|
|
||||||
|
|
||||||
// DNS dns对象
|
// DNS dns对象
|
||||||
@@ -418,19 +438,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)
|
} else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
|
||||||
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true
|
|
||||||
) {
|
|
||||||
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
|
||||||
@@ -455,7 +474,7 @@ object V2rayConfigManager {
|
|||||||
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
||||||
|
|
||||||
val requestString: String by lazy {
|
val requestString: String by lazy {
|
||||||
"""{"version":"1.1","method":"GET","headers":{"User-Agent":"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"Safari/537.36,"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
|
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
|
||||||
}
|
}
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
||||||
requestString,
|
requestString,
|
||||||
@@ -13,46 +13,41 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
package com.v2ray.ang.helper
|
||||||
package com.v2ray.ang.helper;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
* Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
|
||||||
*
|
*
|
||||||
* @author Paul Burke (ipaulpro)
|
* @author Paul Burke (ipaulpro)
|
||||||
*/
|
*/
|
||||||
public interface ItemTouchHelperAdapter {
|
interface ItemTouchHelperAdapter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
||||||
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
|
* an item is shifted, and **not** at the end of a "drop" event.<br></br>
|
||||||
* <br/>
|
* <br></br>
|
||||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
|
* Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
|
||||||
* adjusting the underlying data to reflect this move.
|
* adjusting the underlying data to reflect this move.
|
||||||
*
|
*
|
||||||
* @param fromPosition The start position of the moved item.
|
* @param fromPosition The start position of the moved item.
|
||||||
* @param toPosition Then resolved position of the moved item.
|
* @param toPosition Then resolved position of the moved item.
|
||||||
* @return True if the item was moved to the new adapter position.
|
* @return True if the item was moved to the new adapter position.
|
||||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
* @see RecyclerView.getAdapterPositionFor
|
||||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
* @see RecyclerView.ViewHolder.getAdapterPosition
|
||||||
*/
|
*/
|
||||||
boolean onItemMove(int fromPosition, int toPosition);
|
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
|
||||||
|
|
||||||
|
|
||||||
void onItemMoveCompleted();
|
fun onItemMoveCompleted()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an item has been dismissed by a swipe.<br/>
|
* Called when an item has been dismissed by a swipe.<br></br>
|
||||||
* <br/>
|
* <br></br>
|
||||||
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
|
* Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
|
||||||
* adjusting the underlying data to reflect this removal.
|
* adjusting the underlying data to reflect this removal.
|
||||||
*
|
*
|
||||||
* @param position The position of the item dismissed.
|
* @param position The position of the item dismissed.
|
||||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
* @see RecyclerView.getAdapterPositionFor
|
||||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
* @see RecyclerView.ViewHolder.getAdapterPosition
|
||||||
*/
|
*/
|
||||||
void onItemDismiss(int position);
|
fun onItemDismiss(position: Int)
|
||||||
}
|
}
|
||||||
@@ -13,29 +13,26 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
package com.v2ray.ang.helper
|
||||||
|
|
||||||
package com.v2ray.ang.helper;
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to notify an item ViewHolder of relevant callbacks from {@link
|
* Interface to notify an item ViewHolder of relevant callbacks from [ ].
|
||||||
* ItemTouchHelper.Callback}.
|
|
||||||
*
|
*
|
||||||
* @author Paul Burke (ipaulpro)
|
* @author Paul Burke (ipaulpro)
|
||||||
*/
|
*/
|
||||||
public interface ItemTouchHelperViewHolder {
|
interface ItemTouchHelperViewHolder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
|
* Called when the [ItemTouchHelper] first registers an item as being moved or swiped.
|
||||||
* Implementations should update the item view to indicate it's active state.
|
* Implementations should update the item view to indicate it's active state.
|
||||||
*/
|
*/
|
||||||
void onItemSelected();
|
fun onItemSelected()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
|
* Called when the [ItemTouchHelper] has completed the move or swipe, and the active item
|
||||||
* state should be cleared.
|
* state should be cleared.
|
||||||
*/
|
*/
|
||||||
void onItemClear();
|
fun onItemClear()
|
||||||
}
|
}
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Paul Burke
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.v2ray.ang.helper;
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.view.animation.DecelerateInterpolator;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
|
||||||
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
|
|
||||||
* </br/>
|
|
||||||
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
|
|
||||||
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement
|
|
||||||
* {@link ItemTouchHelperViewHolder}.
|
|
||||||
*
|
|
||||||
* @author Paul Burke (ipaulpro)
|
|
||||||
*/
|
|
||||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|
||||||
|
|
||||||
private static final float ALPHA_FULL = 1.0f;
|
|
||||||
private static final float SWIPE_THRESHOLD = 0.25f;
|
|
||||||
private static final long ANIMATION_DURATION = 200;
|
|
||||||
|
|
||||||
private final ItemTouchHelperAdapter mAdapter;
|
|
||||||
private ValueAnimator mReturnAnimator;
|
|
||||||
|
|
||||||
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
|
||||||
mAdapter = adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLongPressDragEnabled() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isItemViewSwipeEnabled() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
|
||||||
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
|
||||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
|
||||||
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
|
||||||
return makeMovementFlags(dragFlags, swipeFlags);
|
|
||||||
} else {
|
|
||||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
|
||||||
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
|
||||||
return makeMovementFlags(dragFlags, swipeFlags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) {
|
|
||||||
if (source.getItemViewType() != target.getItemViewType()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
|
||||||
// 不执行删除操作,仅返回项目到原位
|
|
||||||
returnViewToOriginalPosition(viewHolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
|
|
||||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
|
||||||
float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
|
||||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
|
||||||
float maxSwipeDistance = viewHolder.itemView.getWidth() * SWIPE_THRESHOLD;
|
|
||||||
float swipeAmount = Math.abs(dX);
|
|
||||||
float direction = Math.signum(dX);
|
|
||||||
|
|
||||||
// 限制最大滑动距离
|
|
||||||
float translationX = Math.min(swipeAmount, maxSwipeDistance) * direction;
|
|
||||||
float alpha = ALPHA_FULL - Math.min(swipeAmount, maxSwipeDistance) / maxSwipeDistance;
|
|
||||||
|
|
||||||
viewHolder.itemView.setTranslationX(translationX);
|
|
||||||
viewHolder.itemView.setAlpha(alpha);
|
|
||||||
|
|
||||||
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
|
|
||||||
returnViewToOriginalPosition(viewHolder);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void returnViewToOriginalPosition(RecyclerView.ViewHolder viewHolder) {
|
|
||||||
if (mReturnAnimator != null && mReturnAnimator.isRunning()) {
|
|
||||||
mReturnAnimator.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.getTranslationX(), 0f);
|
|
||||||
mReturnAnimator.addUpdateListener(animation -> {
|
|
||||||
float value = (float) animation.getAnimatedValue();
|
|
||||||
viewHolder.itemView.setTranslationX(value);
|
|
||||||
viewHolder.itemView.setAlpha(1f - Math.abs(value) / (viewHolder.itemView.getWidth() * SWIPE_THRESHOLD));
|
|
||||||
});
|
|
||||||
mReturnAnimator.setInterpolator(new DecelerateInterpolator());
|
|
||||||
mReturnAnimator.setDuration(ANIMATION_DURATION);
|
|
||||||
mReturnAnimator.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
|
|
||||||
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
|
|
||||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
|
||||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
|
||||||
itemViewHolder.onItemSelected();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onSelectedChanged(viewHolder, actionState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
|
||||||
super.clearView(recyclerView, viewHolder);
|
|
||||||
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
|
||||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
|
||||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
|
||||||
itemViewHolder.onItemClear();
|
|
||||||
}
|
|
||||||
mAdapter.onItemMoveCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
|
|
||||||
return 1.1f; // 设置一个大于1的值,确保不会触发默认的滑动删除操作
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getSwipeEscapeVelocity(float defaultValue) {
|
|
||||||
return defaultValue * 10; // 增加滑动逃逸速度,使得更难触发滑动
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Paul Burke
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.v2ray.ang.helper
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.animation.ValueAnimator.AnimatorUpdateListener
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.sign
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and
|
||||||
|
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br></br>
|
||||||
|
*
|
||||||
|
* Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement
|
||||||
|
* [ItemTouchHelperViewHolder].
|
||||||
|
*
|
||||||
|
* @author Paul Burke (ipaulpro)
|
||||||
|
*/
|
||||||
|
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
|
||||||
|
private var mReturnAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
|
override fun isLongPressDragEnabled(): Boolean = true
|
||||||
|
|
||||||
|
override fun isItemViewSwipeEnabled(): Boolean = true
|
||||||
|
|
||||||
|
override fun getMovementFlags(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder
|
||||||
|
): Int {
|
||||||
|
val dragFlags: Int
|
||||||
|
val swipeFlags: Int
|
||||||
|
if (recyclerView.layoutManager is GridLayoutManager) {
|
||||||
|
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
|
||||||
|
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
|
||||||
|
} else {
|
||||||
|
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||||
|
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
|
||||||
|
}
|
||||||
|
return makeMovementFlags(dragFlags, swipeFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
source: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
return if (source.itemViewType != target.itemViewType) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
mAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
// Do not delete; simply return item to original position
|
||||||
|
returnViewToOriginalPosition(viewHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChildDraw(
|
||||||
|
c: Canvas, recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
|
||||||
|
) {
|
||||||
|
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||||
|
val maxSwipeDistance = viewHolder.itemView.width * SWIPE_THRESHOLD
|
||||||
|
val swipeAmount = abs(dX)
|
||||||
|
val direction = sign(dX)
|
||||||
|
|
||||||
|
// Limit maximum swipe distance
|
||||||
|
val translationX = min(swipeAmount, maxSwipeDistance) * direction
|
||||||
|
val alpha = ALPHA_FULL - min(swipeAmount, maxSwipeDistance) / maxSwipeDistance
|
||||||
|
|
||||||
|
viewHolder.itemView.translationX = translationX
|
||||||
|
viewHolder.itemView.alpha = alpha
|
||||||
|
|
||||||
|
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
|
||||||
|
returnViewToOriginalPosition(viewHolder)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun returnViewToOriginalPosition(viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
mReturnAnimator?.takeIf { it.isRunning }?.cancel()
|
||||||
|
|
||||||
|
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.translationX, 0f).apply {
|
||||||
|
addUpdateListener { animation ->
|
||||||
|
val value = animation.animatedValue as Float
|
||||||
|
viewHolder.itemView.translationX = value
|
||||||
|
viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD))
|
||||||
|
}
|
||||||
|
interpolator = DecelerateInterpolator()
|
||||||
|
duration = ANIMATION_DURATION
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||||
|
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is ItemTouchHelperViewHolder) {
|
||||||
|
viewHolder.onItemSelected()
|
||||||
|
}
|
||||||
|
super.onSelectedChanged(viewHolder, actionState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
super.clearView(recyclerView, viewHolder)
|
||||||
|
viewHolder.itemView.alpha = ALPHA_FULL
|
||||||
|
if (viewHolder is ItemTouchHelperViewHolder) {
|
||||||
|
viewHolder.onItemClear()
|
||||||
|
}
|
||||||
|
mAdapter.onItemMoveCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
|
||||||
|
return 1.1f // Set a value greater than 1 to prevent default swipe delete
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
|
||||||
|
return defaultValue * 10 // Increase swipe escape velocity to make swipe harder to trigger
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ALPHA_FULL = 1.0f
|
||||||
|
private const val SWIPE_THRESHOLD = 0.25f
|
||||||
|
private const val ANIMATION_DURATION: Long = 200
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -222,7 +222,7 @@ 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)
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
package com.v2ray.ang.plugin
|
package com.v2ray.ang.plugin
|
||||||
|
|
||||||
import android.content.pm.ComponentInfo
|
import android.content.pm.ComponentInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.ResolveInfo
|
import android.content.pm.ResolveInfo
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -33,13 +34,18 @@ abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin()
|
|||||||
|
|
||||||
override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
|
override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
|
||||||
override val version by lazy {
|
override val version by lazy {
|
||||||
AngApplication.application.getPackageInfo(componentInfo.packageName).versionCode
|
getPackageInfo(componentInfo.packageName).versionCode
|
||||||
}
|
}
|
||||||
override val versionName: String by lazy {
|
override val versionName: String by lazy {
|
||||||
AngApplication.application.getPackageInfo(componentInfo.packageName).versionName!!
|
getPackageInfo(componentInfo.packageName).versionName!!
|
||||||
}
|
}
|
||||||
override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
|
override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
|
||||||
override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
|
override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
|
||||||
override val packageName: String get() = componentInfo.packageName
|
override val packageName: String get() = componentInfo.packageName
|
||||||
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
|
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
|
||||||
|
|
||||||
|
fun getPackageInfo(packageName: String) = AngApplication.application.packageManager.getPackageInfo(
|
||||||
|
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
|
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
|
||||||
|
)!!
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.v2ray.ang.receiver
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
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
|
||||||
|
//Check if flag is true and a server is selected
|
||||||
|
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
||||||
|
//Start v2ray
|
||||||
|
V2RayServiceManager.startV2Ray(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,9 @@ class TaskerReceiver : BroadcastReceiver() {
|
|||||||
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)
|
||||||
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
|
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty()
|
||||||
|
|
||||||
if (switch == null || guid == null || TextUtils.isEmpty(guid)) {
|
if (switch == null || TextUtils.isEmpty(guid)) {
|
||||||
return
|
return
|
||||||
} else if (switch) {
|
} else if (switch) {
|
||||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||||
@@ -8,11 +8,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ProcessService {
|
class ProcessService {
|
||||||
private val TAG = ANG_PACKAGE
|
|
||||||
private var process: Process? = null
|
private var process: Process? = null
|
||||||
|
|
||||||
fun runProcess(context: Context, cmd: MutableList<String>) {
|
fun runProcess(context: Context, cmd: MutableList<String>) {
|
||||||
Log.d(TAG, cmd.toString())
|
Log.d(ANG_PACKAGE, cmd.toString())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val proBuilder = ProcessBuilder(cmd)
|
val proBuilder = ProcessBuilder(cmd)
|
||||||
@@ -23,23 +22,23 @@ class ProcessService {
|
|||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
Thread.sleep(50L)
|
Thread.sleep(50L)
|
||||||
Log.d(TAG, "runProcess check")
|
Log.d(ANG_PACKAGE, "runProcess check")
|
||||||
process?.waitFor()
|
process?.waitFor()
|
||||||
Log.d(TAG, "runProcess exited")
|
Log.d(ANG_PACKAGE, "runProcess exited")
|
||||||
}
|
}
|
||||||
Log.d(TAG, process.toString())
|
Log.d(ANG_PACKAGE, process.toString())
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(TAG, e.toString())
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopProcess() {
|
fun stopProcess() {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "runProcess destroy")
|
Log.d(ANG_PACKAGE, "runProcess destroy")
|
||||||
process?.destroy()
|
process?.destroy()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(TAG, e.toString())
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,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.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.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
@@ -32,24 +33,31 @@ class QSTileService : TileService() {
|
|||||||
qsTile?.updateTile()
|
qsTile?.updateTile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
|
*/
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
|
|
||||||
setState(Tile.STATE_INACTIVE)
|
setState(Tile.STATE_INACTIVE)
|
||||||
mMsgReceive = ReceiveMessageHandler(this)
|
mMsgReceive = ReceiveMessageHandler(this)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), RECEIVER_EXPORTED)
|
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
} else {
|
|
||||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopListening() {
|
override fun onStopListening() {
|
||||||
super.onStopListening()
|
super.onStopListening()
|
||||||
|
|
||||||
unregisterReceiver(mMsgReceive)
|
try {
|
||||||
mMsgReceive = null
|
applicationContext.unregisterReceiver(mMsgReceive)
|
||||||
|
mMsgReceive = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
@@ -4,6 +4,7 @@ import android.app.Notification
|
|||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -13,11 +14,13 @@ import android.os.Build
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
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.TAG_DIRECT
|
||||||
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.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.toSpeedString
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
@@ -28,10 +31,11 @@ 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.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import libv2ray.V2RayPoint
|
import libv2ray.V2RayPoint
|
||||||
@@ -43,6 +47,7 @@ object V2RayServiceManager {
|
|||||||
private const val NOTIFICATION_ID = 1
|
private const val NOTIFICATION_ID = 1
|
||||||
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
||||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
||||||
|
private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
|
||||||
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
||||||
|
|
||||||
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
||||||
@@ -58,14 +63,17 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
private var lastQueryTime = 0L
|
private var lastQueryTime = 0L
|
||||||
private var mBuilder: NotificationCompat.Builder? = null
|
private var mBuilder: NotificationCompat.Builder? = null
|
||||||
private var mDisposable: Disposable? = null
|
private var speedNotificationJob: Job? = null
|
||||||
private var mNotificationManager: NotificationManager? = null
|
private var mNotificationManager: NotificationManager? = null
|
||||||
|
|
||||||
fun startV2Ray(context: Context) {
|
fun startV2Ray(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
|
||||||
|
|
||||||
@@ -126,6 +134,11 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
|
*/
|
||||||
|
|
||||||
fun startV2rayPoint() {
|
fun startV2rayPoint() {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
val service = serviceControl?.get()?.getService() ?: return
|
||||||
val guid = MmkvManager.getSelectServer() ?: return
|
val guid = MmkvManager.getSelectServer() ?: return
|
||||||
@@ -142,11 +155,7 @@ object V2RayServiceManager {
|
|||||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
|
|
||||||
} else {
|
|
||||||
service.registerReceiver(mMsgReceive, mFilter)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
@@ -156,7 +165,7 @@ object V2RayServiceManager {
|
|||||||
currentConfig = config
|
currentConfig = config
|
||||||
|
|
||||||
try {
|
try {
|
||||||
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true)
|
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
@@ -217,11 +226,15 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
AppConfig.MSG_STATE_STOP -> {
|
||||||
|
Log.d(ANG_PACKAGE, "Stop Service")
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
AppConfig.MSG_STATE_RESTART -> {
|
||||||
startV2rayPoint()
|
Log.d(ANG_PACKAGE, "Restart Service")
|
||||||
|
serviceControl.stopService()
|
||||||
|
Thread.sleep(500L)
|
||||||
|
startV2Ray(serviceControl.getService())
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY -> {
|
AppConfig.MSG_MEASURE_DELAY -> {
|
||||||
@@ -276,30 +289,24 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
private fun showNotification() {
|
private fun showNotification() {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
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 startMainIntent = Intent(service, MainActivity::class.java)
|
||||||
val contentPendingIntent = PendingIntent.getActivity(
|
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
|
||||||
service,
|
|
||||||
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
} else {
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||||
stopV2RayIntent.`package` = ANG_PACKAGE
|
stopV2RayIntent.`package` = ANG_PACKAGE
|
||||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
||||||
|
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
|
||||||
|
|
||||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(
|
val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||||
service,
|
restartV2RayIntent.`package` = ANG_PACKAGE
|
||||||
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
|
restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
} else {
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val channelId =
|
val channelId =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -323,6 +330,11 @@ object V2RayServiceManager {
|
|||||||
service.getString(R.string.notification_action_stop_v2ray),
|
service.getString(R.string.notification_action_stop_v2ray),
|
||||||
stopV2RayPendingIntent
|
stopV2RayPendingIntent
|
||||||
)
|
)
|
||||||
|
.addAction(
|
||||||
|
R.drawable.ic_delete_24dp,
|
||||||
|
service.getString(R.string.title_service_restart),
|
||||||
|
restartV2RayPendingIntent
|
||||||
|
)
|
||||||
//.build()
|
//.build()
|
||||||
|
|
||||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
||||||
@@ -347,10 +359,15 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
fun cancelNotification() {
|
fun cancelNotification() {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
val service = serviceControl?.get()?.getService() ?: return
|
||||||
service.stopForeground(true)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
|
} else {
|
||||||
|
service.stopForeground(true)
|
||||||
|
}
|
||||||
|
|
||||||
mBuilder = null
|
mBuilder = null
|
||||||
mDisposable?.dispose()
|
speedNotificationJob?.cancel()
|
||||||
mDisposable = null
|
speedNotificationJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
||||||
@@ -377,7 +394,7 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startSpeedNotification() {
|
private fun startSpeedNotification() {
|
||||||
if (mDisposable == null &&
|
if (speedNotificationJob == null &&
|
||||||
v2rayPoint.isRunning &&
|
v2rayPoint.isRunning &&
|
||||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
|
||||||
) {
|
) {
|
||||||
@@ -385,8 +402,8 @@ object V2RayServiceManager {
|
|||||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||||
outboundTags?.remove(TAG_DIRECT)
|
outboundTags?.remove(TAG_DIRECT)
|
||||||
|
|
||||||
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
.subscribe {
|
while (isActive) {
|
||||||
val queryTime = System.currentTimeMillis()
|
val queryTime = System.currentTimeMillis()
|
||||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
||||||
var proxyTotal = 0L
|
var proxyTotal = 0L
|
||||||
@@ -414,7 +431,9 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
lastZeroSpeed = zeroSpeed
|
lastZeroSpeed = zeroSpeed
|
||||||
lastQueryTime = queryTime
|
lastQueryTime = queryTime
|
||||||
|
delay(3000)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,11 +448,10 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopSpeedNotification() {
|
private fun stopSpeedNotification() {
|
||||||
mDisposable?.let {
|
speedNotificationJob?.let {
|
||||||
it.dispose() //stop queryStats
|
it.cancel()
|
||||||
mDisposable = null
|
speedNotificationJob = null
|
||||||
updateNotification(currentConfig?.remarks, 0, 0)
|
updateNotification(currentConfig?.remarks, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ import libv2ray.Libv2ray
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class V2RayTestService : Service() {
|
class V2RayTestService : Service() {
|
||||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) }
|
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -10,6 +10,7 @@ import android.net.LocalSocketAddress
|
|||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
|
import android.net.ProxyInfo
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
@@ -17,6 +18,7 @@ import android.os.StrictMode
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
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.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -33,10 +35,10 @@ 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 = "26.26.26.1"
|
private const val PRIVATE_VLAN4_CLIENT = "10.10.10.1"
|
||||||
private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2"
|
private const val PRIVATE_VLAN4_ROUTER = "10.10.10.2"
|
||||||
private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1"
|
private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:10:1"
|
||||||
private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2"
|
private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:10:2"
|
||||||
private const val TUN2SOCKS = "libtun2socks.so"
|
private const val TUN2SOCKS = "libtun2socks.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,9 +155,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
||||||
|
|
||||||
val selfPackageName = BuildConfig.APPLICATION_ID
|
val selfPackageName = BuildConfig.APPLICATION_ID
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
|
||||||
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS) == true
|
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
|
||||||
//process self package
|
//process self package
|
||||||
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
|
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
|
||||||
apps?.forEach {
|
apps?.forEach {
|
||||||
@@ -165,6 +167,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
else
|
else
|
||||||
builder.addAllowedApplication(it)
|
builder.addAllowedApplication(it)
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -188,6 +191,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
builder.setMetered(false)
|
builder.setMetered(false)
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY)) {
|
||||||
|
builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOOPBACK, SettingsManager.getHttpPort()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new interface using the builder and save the parameters.
|
// Create a new interface using the builder and save the parameters.
|
||||||
@@ -215,11 +221,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
"--loglevel", "notice"
|
"--loglevel", "notice"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
|
||||||
cmd.add("--netif-ip6addr")
|
cmd.add("--netif-ip6addr")
|
||||||
cmd.add(PRIVATE_VLAN6_ROUTER)
|
cmd.add(PRIVATE_VLAN6_ROUTER)
|
||||||
}
|
}
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
|
||||||
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
||||||
cmd.add("--dnsgw")
|
cmd.add("--dnsgw")
|
||||||
cmd.add("$LOOPBACK:${localDnsPort}")
|
cmd.add("$LOOPBACK:${localDnsPort}")
|
||||||
@@ -232,7 +238,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
process = proBuilder
|
process = proBuilder
|
||||||
.directory(applicationContext.filesDir)
|
.directory(applicationContext.filesDir)
|
||||||
.start()
|
.start()
|
||||||
Thread(Runnable {
|
Thread {
|
||||||
Log.d(packageName, "$TUN2SOCKS check")
|
Log.d(packageName, "$TUN2SOCKS check")
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
Log.d(packageName, "$TUN2SOCKS exited")
|
Log.d(packageName, "$TUN2SOCKS exited")
|
||||||
@@ -240,7 +246,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
Log.d(packageName, "$TUN2SOCKS restart")
|
Log.d(packageName, "$TUN2SOCKS restart")
|
||||||
runTun2socks()
|
runTun2socks()
|
||||||
}
|
}
|
||||||
}).start()
|
}.start()
|
||||||
Log.d(packageName, process.toString())
|
Log.d(packageName, process.toString())
|
||||||
|
|
||||||
sendFd()
|
sendFd()
|
||||||
@@ -5,15 +5,16 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
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.util.SpeedtestUtil
|
import com.v2ray.ang.util.SpeedtestUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.ZipUtil
|
import com.v2ray.ang.util.ZipUtil
|
||||||
@@ -22,9 +23,23 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
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)
|
||||||
@@ -32,6 +47,7 @@ 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) {
|
||||||
@@ -49,7 +65,8 @@ 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)
|
||||||
@@ -61,23 +78,22 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -88,6 +104,16 @@ class AboutActivity : BaseActivity() {
|
|||||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
Utils.openUri(this, AppConfig.TgChannelUrl)
|
Utils.openUri(this, AppConfig.TgChannelUrl)
|
||||||
}
|
}
|
||||||
@@ -148,9 +174,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")
|
||||||
@@ -171,4 +197,7 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toast(messageResId: Int) {
|
||||||
|
Toast.makeText(this, getString(messageResId), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
149
V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt
Normal file
149
V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
||||||
|
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
|
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
|
var logsetsAll: MutableList<String> = mutableListOf()
|
||||||
|
var logsets: MutableList<String> = mutableListOf()
|
||||||
|
private val adapter by lazy { LogcatRecyclerAdapter(this) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
title = getString(R.string.title_logcat)
|
||||||
|
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
binding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||||
|
|
||||||
|
binding.refreshLayout.setOnRefreshListener(this)
|
||||||
|
|
||||||
|
logsets.add(getString(R.string.pull_down_to_refresh))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLogcat() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
binding.refreshLayout.isRefreshing = true
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
val lst = LinkedHashSet<String>()
|
||||||
|
lst.add("logcat")
|
||||||
|
lst.add("-d")
|
||||||
|
lst.add("-v")
|
||||||
|
lst.add("time")
|
||||||
|
lst.add("-s")
|
||||||
|
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||||
|
val process = withContext(Dispatchers.IO) {
|
||||||
|
Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
logsetsAll = allText.toMutableList()
|
||||||
|
logsets = allText.toMutableList()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
binding.refreshLayout.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.menu_logcat, menu)
|
||||||
|
|
||||||
|
val searchItem = menu.findItem(R.id.search_view)
|
||||||
|
if (searchItem != null) {
|
||||||
|
val searchView = searchItem.actionView as SearchView
|
||||||
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
filterLogs(newText)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
searchView.setOnCloseListener {
|
||||||
|
filterLogs("")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
R.id.copy_all -> {
|
||||||
|
Utils.setClipboard(this, logsets.joinToString("\n"))
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.clear_all -> {
|
||||||
|
clearLogcat()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterLogs(content: String?): Boolean {
|
||||||
|
val key = content?.trim()
|
||||||
|
logsets = if (key.isNullOrEmpty()) {
|
||||||
|
logsetsAll.toMutableList()
|
||||||
|
} else {
|
||||||
|
logsetsAll.filter { it.contains(key) }.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRefresh() {
|
||||||
|
getLogcat()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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) {
|
||||||
|
val content = mActivity.logsets[position]
|
||||||
|
holder.itemSubSettingBinding.logContent.text = content
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
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
|
||||||
@@ -27,7 +28,6 @@ import androidx.recyclerview.widget.ItemTouchHelper
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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
|
||||||
@@ -41,8 +41,6 @@ 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
|
||||||
@@ -81,6 +79,60 @@ 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)
|
||||||
@@ -129,12 +181,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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
@@ -226,11 +276,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
Utils.stopVService(this)
|
Utils.stopVService(this)
|
||||||
}
|
}
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
lifecycleScope.launch {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
delay(500)
|
||||||
.subscribe {
|
startV2Ray()
|
||||||
startV2Ray()
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onResume() {
|
public override fun onResume() {
|
||||||
@@ -346,8 +395,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val ret = mainViewModel.exportAllServer()
|
val ret = mainViewModel.exportAllServer()
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (ret == 0)
|
if (ret > 0)
|
||||||
toast(R.string.toast_success)
|
toast(getString(R.string.title_export_config_count, ret))
|
||||||
else
|
else
|
||||||
toast(R.string.toast_failure)
|
toast(R.string.toast_failure)
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
@@ -358,13 +407,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.ping_all -> {
|
R.id.ping_all -> {
|
||||||
toast(R.string.connection_test_testing)
|
toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
|
||||||
mainViewModel.testAllTcping()
|
mainViewModel.testAllTcping()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.real_ping_all -> {
|
R.id.real_ping_all -> {
|
||||||
toast(R.string.connection_test_testing)
|
toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
|
||||||
mainViewModel.testAllRealPing()
|
mainViewModel.testAllRealPing()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -379,14 +428,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
binding.pbWaiting.show()
|
binding.pbWaiting.show()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
mainViewModel.removeAllServer()
|
val ret = mainViewModel.removeAllServer()
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
|
toast(getString(R.string.title_del_config_count, ret))
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do noting
|
//do noting
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -406,7 +456,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do noting
|
//do noting
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -418,14 +468,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
binding.pbWaiting.show()
|
binding.pbWaiting.show()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
mainViewModel.removeInvalidServer()
|
val ret = mainViewModel.removeInvalidServer()
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
|
toast(getString(R.string.title_del_config_count, ret))
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do noting
|
//do noting
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -460,38 +511,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 = 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
|
||||||
*/
|
*/
|
||||||
@@ -517,7 +550,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
when {
|
when {
|
||||||
count > 0 -> {
|
count > 0 -> {
|
||||||
toast(R.string.toast_success)
|
toast(getString(R.string.title_import_config_count, count))
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,10 +645,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
* 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) {
|
||||||
@@ -623,12 +652,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
delay(500L)
|
delay(500L)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
toast(R.string.toast_success)
|
toast(getString(R.string.title_update_config_count, count))
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
//dialog.dismiss()
|
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -643,17 +671,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,20 +694,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
} 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 {
|
try {
|
||||||
if (it) {
|
contentResolver.openInputStream(uri).use { input ->
|
||||||
try {
|
importCustomizeConfig(input?.bufferedReader()?.readText())
|
||||||
contentResolver.openInputStream(uri).use { input ->
|
}
|
||||||
importCustomizeConfig(input?.bufferedReader()?.readText())
|
} catch (e: Exception) {
|
||||||
}
|
e.printStackTrace()
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
requestPermissionLauncher.launch(permission)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -761,4 +787,4 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
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
|
||||||
@@ -23,9 +24,9 @@ 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 com.v2ray.ang.util.Utils
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.delay
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.*
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -143,7 +144,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
removeServer(guid, position)
|
removeServer(guid, position)
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do noting
|
//do noting
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -165,11 +166,14 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
Utils.stopVService(mActivity)
|
Utils.stopVService(mActivity)
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
mActivity.lifecycleScope.launch {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
try {
|
||||||
.subscribe {
|
delay(500)
|
||||||
V2RayServiceManager.startV2Ray(mActivity)
|
V2RayServiceManager.startV2Ray(mActivity)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,4 +250,4 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,10 +20,9 @@ import com.v2ray.ang.extension.v2RayApplication
|
|||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
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() {
|
||||||
@@ -43,93 +42,39 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
|
|
||||||
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
|
|
||||||
AppManagerUtil.rxLoadNetworkAppList(this)
|
lifecycleScope.launch {
|
||||||
.subscribeOn(Schedulers.io())
|
try {
|
||||||
.map {
|
binding.pbWaiting.visibility = View.VISIBLE
|
||||||
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(Comparator { 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.visibility = View.GONE
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.pbWaiting.visibility = View.GONE
|
||||||
|
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() {
|
||||||
@@ -37,7 +37,7 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
|
|
||||||
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
|
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
|
||||||
binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
|
binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
|
||||||
binding.chkLocked.isChecked = rulesetItem.looked == true
|
binding.chkLocked.isChecked = rulesetItem.locked == true
|
||||||
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
|
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
|
||||||
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
|
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
|
||||||
binding.etPort.text = Utils.getEditable(rulesetItem.port)
|
binding.etPort.text = Utils.getEditable(rulesetItem.port)
|
||||||
@@ -60,7 +60,7 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
|
|
||||||
rulesetItem.apply {
|
rulesetItem.apply {
|
||||||
remarks = binding.etRemarks.text.toString()
|
remarks = binding.etRemarks.text.toString()
|
||||||
looked = binding.chkLocked.isChecked
|
locked = binding.chkLocked.isChecked
|
||||||
domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() }
|
domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() }
|
||||||
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||||
ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() }
|
ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() }
|
||||||
@@ -95,7 +95,7 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
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 android.view.View
|
import android.view.View
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
@@ -36,6 +38,16 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
private val preset_rulesets: Array<out String> by lazy {
|
private val preset_rulesets: Array<out String> by lazy {
|
||||||
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)
|
||||||
@@ -83,13 +95,13 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.import_rulesets -> {
|
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 ->
|
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
||||||
try {
|
try {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
SettingsManager.resetRoutingRulesets(this@RoutingSettingActivity, i)
|
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
refreshData()
|
refreshData()
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
@@ -99,11 +111,9 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do noting
|
//do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
@@ -120,7 +130,7 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
return@setPositiveButton
|
return@setPositiveButton
|
||||||
}
|
}
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val result = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
|
val result = SettingsManager.resetRoutingRulesets(clipboard)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (result) {
|
if (result) {
|
||||||
refreshData()
|
refreshData()
|
||||||
@@ -131,13 +141,17 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do nothing
|
//do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.import_rulesets_from_qrcode -> {
|
||||||
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.export_rulesets_to_clipboard -> {
|
R.id.export_rulesets_to_clipboard -> {
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
@@ -153,10 +167,37 @@ class RoutingSettingActivity : BaseActivity() {
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == RESULT_OK) {
|
||||||
|
importRulesetsFromQRcode(it.data?.getStringExtra("SCAN_RESULT"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importRulesetsFromQRcode(qrcode: String?): Boolean {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val result = SettingsManager.resetRoutingRulesets(qrcode)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (result) {
|
||||||
|
refreshData()
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do nothing
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
fun refreshData() {
|
fun refreshData() {
|
||||||
rulesets.clear()
|
rulesets.clear()
|
||||||
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@ class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : Recy
|
|||||||
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString()
|
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString()
|
||||||
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
|
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
|
||||||
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
|
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
|
||||||
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.looked == true
|
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
||||||
@@ -4,13 +4,23 @@ 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.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)
|
||||||
@@ -18,19 +28,10 @@ class ScScannerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun importQRcode(): Boolean {
|
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()
|
||||||
@@ -46,5 +47,4 @@ class ScScannerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -2,13 +2,15 @@ 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.app.ActivityCompat
|
||||||
|
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 +23,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 +105,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 +130,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,9 @@ import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
|||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
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.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
@@ -79,6 +81,10 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val alpns: Array<out String> by lazy {
|
private val alpns: Array<out String> by lazy {
|
||||||
resources.getStringArray(R.array.streamsecurity_alpn)
|
resources.getStringArray(R.array.streamsecurity_alpn)
|
||||||
}
|
}
|
||||||
|
private val xhttpMode: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.xhttp_mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
|
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
|
||||||
// We don't use AndroidViewBinding because, it is better to share similar logics for different
|
// We don't use AndroidViewBinding because, it is better to share similar logics for different
|
||||||
@@ -107,6 +113,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
||||||
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
|
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
|
||||||
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
|
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
|
||||||
|
private val et_preshared_key: EditText? by lazy { findViewById(R.id.et_preshared_key) }
|
||||||
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
|
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
|
||||||
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
|
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
|
||||||
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
|
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
|
||||||
@@ -119,6 +126,10 @@ 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 layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -142,7 +153,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
parent: AdapterView<*>?,
|
parent: AdapterView<*>?,
|
||||||
view: View?,
|
view: View?,
|
||||||
position: Int,
|
position: Int,
|
||||||
id: Long
|
id: Long,
|
||||||
) {
|
) {
|
||||||
val types = transportTypes(networks[position])
|
val types = transportTypes(networks[position])
|
||||||
sp_header_type?.isEnabled = types.size > 1
|
sp_header_type?.isEnabled = types.size > 1
|
||||||
@@ -150,28 +161,49 @@ class ServerActivity : BaseActivity() {
|
|||||||
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
|
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
sp_header_type?.adapter = adapter
|
sp_header_type?.adapter = adapter
|
||||||
sp_header_type_title?.text = if (networks[position] == "grpc")
|
sp_header_type_title?.text =
|
||||||
getString(R.string.server_lab_mode_type) else
|
when (networks[position]) {
|
||||||
getString(R.string.server_lab_head_type)
|
NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type)
|
||||||
config?.headerType?.let { it ->
|
NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode)
|
||||||
sp_header_type?.setSelection(Utils.arrayFind(types, it))
|
else -> getString(R.string.server_lab_head_type)
|
||||||
}
|
}.orEmpty()
|
||||||
config?.host?.let { it ->
|
sp_header_type?.setSelection(
|
||||||
et_request_host?.text = Utils.getEditable(it)
|
Utils.arrayFind(
|
||||||
}
|
types,
|
||||||
config?.path?.let { it ->
|
when (networks[position]) {
|
||||||
et_path?.text = Utils.getEditable(it)
|
NetworkType.GRPC.type -> config?.mode
|
||||||
}
|
NetworkType.XHTTP.type -> config?.xhttpMode
|
||||||
|
else -> config?.headerType
|
||||||
|
}.orEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
et_request_host?.text = Utils.getEditable(
|
||||||
|
when (networks[position]) {
|
||||||
|
//"quic" -> config?.quicSecurity
|
||||||
|
NetworkType.GRPC.type -> config?.authority
|
||||||
|
else -> config?.host
|
||||||
|
}.orEmpty()
|
||||||
|
)
|
||||||
|
et_path?.text = Utils.getEditable(
|
||||||
|
when (networks[position]) {
|
||||||
|
NetworkType.KCP.type -> config?.seed
|
||||||
|
//"quic" -> config?.quicKey
|
||||||
|
NetworkType.GRPC.type -> config?.serviceName
|
||||||
|
else -> config?.path
|
||||||
|
}.orEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
tv_request_host?.text = Utils.getEditable(
|
tv_request_host?.text = Utils.getEditable(
|
||||||
getString(
|
getString(
|
||||||
when (networks[position]) {
|
when (networks[position]) {
|
||||||
"tcp" -> R.string.server_lab_request_host_http
|
NetworkType.TCP.type -> R.string.server_lab_request_host_http
|
||||||
"ws" -> R.string.server_lab_request_host_ws
|
NetworkType.WS.type -> R.string.server_lab_request_host_ws
|
||||||
"httpupgrade" -> R.string.server_lab_request_host_httpupgrade
|
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade
|
||||||
"splithttp" -> R.string.server_lab_request_host_splithttp
|
NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp
|
||||||
"h2" -> R.string.server_lab_request_host_h2
|
NetworkType.H2.type -> R.string.server_lab_request_host_h2
|
||||||
"quic" -> R.string.server_lab_request_host_quic
|
//"quic" -> R.string.server_lab_request_host_quic
|
||||||
"grpc" -> R.string.server_lab_request_host_grpc
|
NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc
|
||||||
else -> R.string.server_lab_request_host
|
else -> R.string.server_lab_request_host
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -180,17 +212,29 @@ class ServerActivity : BaseActivity() {
|
|||||||
tv_path?.text = Utils.getEditable(
|
tv_path?.text = Utils.getEditable(
|
||||||
getString(
|
getString(
|
||||||
when (networks[position]) {
|
when (networks[position]) {
|
||||||
"kcp" -> R.string.server_lab_path_kcp
|
NetworkType.KCP.type -> R.string.server_lab_path_kcp
|
||||||
"ws" -> R.string.server_lab_path_ws
|
NetworkType.WS.type -> R.string.server_lab_path_ws
|
||||||
"httpupgrade" -> R.string.server_lab_path_httpupgrade
|
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
|
||||||
"splithttp" -> R.string.server_lab_path_splithttp
|
NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
|
||||||
"h2" -> R.string.server_lab_path_h2
|
NetworkType.H2.type -> R.string.server_lab_path_h2
|
||||||
"quic" -> R.string.server_lab_path_quic
|
//"quic" -> R.string.server_lab_path_quic
|
||||||
"grpc" -> R.string.server_lab_path_grpc
|
NetworkType.GRPC.type -> R.string.server_lab_path_grpc
|
||||||
else -> R.string.server_lab_path
|
else -> R.string.server_lab_path
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
et_extra?.text = Utils.getEditable(
|
||||||
|
when (networks[position]) {
|
||||||
|
NetworkType.XHTTP.type -> config?.xhttpExtra
|
||||||
|
else -> null
|
||||||
|
}.orEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
|
layout_extra?.visibility =
|
||||||
|
when (networks[position]) {
|
||||||
|
NetworkType.XHTTP.type -> View.VISIBLE
|
||||||
|
else -> View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||||
@@ -202,7 +246,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
parent: AdapterView<*>?,
|
parent: AdapterView<*>?,
|
||||||
view: View?,
|
view: View?,
|
||||||
position: Int,
|
position: Int,
|
||||||
id: Long
|
id: Long,
|
||||||
) {
|
) {
|
||||||
val isBlank = streamSecuritys[position].isBlank()
|
val isBlank = streamSecuritys[position].isBlank()
|
||||||
val isTLS = streamSecuritys[position] == TLS
|
val isTLS = streamSecuritys[position] == TLS
|
||||||
@@ -280,29 +324,23 @@ class ServerActivity : BaseActivity() {
|
|||||||
} else if (config.configType == EConfigType.WIREGUARD) {
|
} else if (config.configType == EConfigType.WIREGUARD) {
|
||||||
et_id.text = Utils.getEditable(config.secretKey.orEmpty())
|
et_id.text = Utils.getEditable(config.secretKey.orEmpty())
|
||||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||||
if (config.reserved == null) {
|
et_preshared_key?.visibility = View.VISIBLE
|
||||||
et_reserved1?.text = Utils.getEditable("0,0,0")
|
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
|
||||||
} else {
|
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
|
||||||
et_reserved1?.text = Utils.getEditable(config.reserved?.toString())
|
et_local_address?.text = Utils.getEditable(
|
||||||
}
|
config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6"
|
||||||
if (config.localAddress == null) {
|
)
|
||||||
et_local_address?.text = Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
|
||||||
} else {
|
|
||||||
et_local_address?.text = Utils.getEditable(config.localAddress)
|
|
||||||
}
|
|
||||||
if (config.mtu == null) {
|
|
||||||
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
|
||||||
} else {
|
|
||||||
et_local_mtu?.text = Utils.getEditable(config.mtu.toString())
|
|
||||||
}
|
|
||||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
|
et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
|
||||||
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
|
||||||
val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
|
val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
|
||||||
if (security >= 0) {
|
if (security >= 0) {
|
||||||
sp_security?.setSelection(security)
|
sp_security?.setSelection(security)
|
||||||
@@ -318,11 +356,11 @@ class ServerActivity : BaseActivity() {
|
|||||||
et_sni?.text = Utils.getEditable(config.sni)
|
et_sni?.text = Utils.getEditable(config.sni)
|
||||||
config.fingerPrint?.let {
|
config.fingerPrint?.let {
|
||||||
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 {
|
||||||
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
|
||||||
@@ -333,7 +371,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
container_public_key?.visibility = View.GONE
|
container_public_key?.visibility = View.GONE
|
||||||
container_short_id?.visibility = View.GONE
|
container_short_id?.visibility = View.GONE
|
||||||
container_spider_x?.visibility = View.GONE
|
container_spider_x?.visibility = View.GONE
|
||||||
} else if (config.security == REALITY) { // reality settings
|
} else if (config.security == REALITY) {
|
||||||
container_public_key?.visibility = View.VISIBLE
|
container_public_key?.visibility = View.VISIBLE
|
||||||
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||||
container_short_id?.visibility = View.VISIBLE
|
container_short_id?.visibility = View.VISIBLE
|
||||||
@@ -353,7 +391,6 @@ class ServerActivity : BaseActivity() {
|
|||||||
container_short_id?.visibility = View.GONE
|
container_short_id?.visibility = View.GONE
|
||||||
container_spider_x?.visibility = View.GONE
|
container_spider_x?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
val network = Utils.arrayFind(networks, config.network.orEmpty())
|
val network = Utils.arrayFind(networks, config.network.orEmpty())
|
||||||
if (network >= 0) {
|
if (network >= 0) {
|
||||||
sp_network?.setSelection(network)
|
sp_network?.setSelection(network)
|
||||||
@@ -407,7 +444,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
|
val config =
|
||||||
|
MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
|
||||||
if (config.configType != EConfigType.SOCKS
|
if (config.configType != EConfigType.SOCKS
|
||||||
&& config.configType != EConfigType.HTTP
|
&& config.configType != EConfigType.HTTP
|
||||||
&& TextUtils.isEmpty(et_id.text.toString())
|
&& TextUtils.isEmpty(et_id.text.toString())
|
||||||
@@ -428,6 +466,12 @@ class ServerActivity : BaseActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (et_extra?.text?.toString().isNotNullEmpty()) {
|
||||||
|
if (JsonUtil.parseString(et_extra?.text?.toString()) == null) {
|
||||||
|
toast(R.string.server_lab_xhttp_extra)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveCommon(config)
|
saveCommon(config)
|
||||||
saveStreamSettings(config)
|
saveStreamSettings(config)
|
||||||
@@ -436,7 +480,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||||
config.subscriptionId = subscriptionId.orEmpty()
|
config.subscriptionId = subscriptionId.orEmpty()
|
||||||
}
|
}
|
||||||
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)
|
toast(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
@@ -464,6 +508,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
} else if (config.configType == EConfigType.WIREGUARD) {
|
} else if (config.configType == EConfigType.WIREGUARD) {
|
||||||
config.secretKey = et_id.text.toString().trim()
|
config.secretKey = et_id.text.toString().trim()
|
||||||
config.publicKey = et_public_key?.text.toString().trim()
|
config.publicKey = et_public_key?.text.toString().trim()
|
||||||
|
config.preSharedKey = et_preshared_key?.text.toString().trim()
|
||||||
config.reserved = et_reserved1?.text.toString().trim()
|
config.reserved = et_reserved1?.text.toString().trim()
|
||||||
config.localAddress = et_local_address?.text.toString().trim()
|
config.localAddress = et_local_address?.text.toString().trim()
|
||||||
config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
||||||
@@ -472,6 +517,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +539,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
profileItem.mode = transportTypes(networks[network])[type]
|
profileItem.mode = transportTypes(networks[network])[type]
|
||||||
profileItem.serviceName = path
|
profileItem.serviceName = path
|
||||||
profileItem.authority = requestHost
|
profileItem.authority = requestHost
|
||||||
|
profileItem.xhttpMode = transportTypes(networks[network])[type]
|
||||||
|
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTls(config: ProfileItem) {
|
private fun saveTls(config: ProfileItem) {
|
||||||
@@ -506,7 +555,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
|
|
||||||
val allowInsecure =
|
val allowInsecure =
|
||||||
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
|
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
|
||||||
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE) == true
|
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE)
|
||||||
} else {
|
} else {
|
||||||
allowinsecures[allowInsecureField].toBoolean()
|
allowinsecures[allowInsecureField].toBoolean()
|
||||||
}
|
}
|
||||||
@@ -523,18 +572,22 @@ class ServerActivity : BaseActivity() {
|
|||||||
|
|
||||||
private fun transportTypes(network: String?): Array<out String> {
|
private fun transportTypes(network: String?): Array<out String> {
|
||||||
return when (network) {
|
return when (network) {
|
||||||
"tcp" -> {
|
NetworkType.TCP.type -> {
|
||||||
tcpTypes
|
tcpTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
"kcp", "quic" -> {
|
NetworkType.KCP.type -> {
|
||||||
kcpAndQuicTypes
|
kcpAndQuicTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
"grpc" -> {
|
NetworkType.GRPC.type -> {
|
||||||
grpcModes
|
grpcModes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NetworkType.XHTTP.type -> {
|
||||||
|
xhttpMode
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
arrayOf("---")
|
arrayOf("---")
|
||||||
}
|
}
|
||||||
@@ -553,7 +606,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()
|
||||||
@@ -12,10 +12,9 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
|
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.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.JsonUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import me.drakeet.support.toast.ToastCompat
|
import me.drakeet.support.toast.ToastCompat
|
||||||
|
|
||||||
@@ -75,8 +74,8 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val v2rayConfig = try {
|
val profileItem = try {
|
||||||
JsonUtil.fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
|
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()
|
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
||||||
@@ -84,7 +83,11 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM)
|
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM)
|
||||||
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString()
|
binding.etRemarks.text.let {
|
||||||
|
config.remarks = if (it.isNullOrEmpty()) profileItem?.remarks.orEmpty() else it.toString()
|
||||||
|
}
|
||||||
|
config.server = profileItem?.server
|
||||||
|
config.serverPort = profileItem?.serverPort
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
|
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
|
||||||
@@ -103,7 +106,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()
|
||||||
@@ -40,8 +40,10 @@ class SettingsActivity : BaseActivity() {
|
|||||||
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
||||||
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
||||||
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
|
||||||
|
private val appendHttpProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_APPEND_HTTP_PROXY) }
|
||||||
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
|
private val 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) }
|
||||||
@@ -57,9 +59,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
|
||||||
|
|
||||||
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
|
||||||
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_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) }
|
||||||
|
|
||||||
@@ -141,11 +143,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
|
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
httpPort?.setOnPreferenceChangeListener { _, any ->
|
|
||||||
val nval = any as String
|
|
||||||
httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval
|
|
||||||
true
|
|
||||||
}
|
|
||||||
remoteDns?.setOnPreferenceChangeListener { _, any ->
|
remoteDns?.setOnPreferenceChangeListener { _, any ->
|
||||||
val nval = any as String
|
val nval = any as String
|
||||||
remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
|
remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
|
||||||
@@ -156,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
|
||||||
@@ -175,6 +178,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
||||||
localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||||
fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
||||||
|
appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false)
|
||||||
localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
||||||
vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
||||||
|
|
||||||
@@ -195,9 +199,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
|
|
||||||
socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||||
httpPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
|
|
||||||
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()
|
||||||
@@ -213,7 +217,6 @@ class SettingsActivity : BaseActivity() {
|
|||||||
fragmentInterval,
|
fragmentInterval,
|
||||||
autoUpdateInterval,
|
autoUpdateInterval,
|
||||||
socksPort,
|
socksPort,
|
||||||
httpPort,
|
|
||||||
remoteDns,
|
remoteDns,
|
||||||
domesticDns,
|
domesticDns,
|
||||||
delayTestUrl
|
delayTestUrl
|
||||||
@@ -244,6 +247,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,
|
||||||
@@ -264,8 +268,11 @@ class SettingsActivity : BaseActivity() {
|
|||||||
perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
|
perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
|
||||||
localDns?.isEnabled = vpn
|
localDns?.isEnabled = vpn
|
||||||
fakeDns?.isEnabled = vpn
|
fakeDns?.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(
|
||||||
@@ -113,7 +113,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -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.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
class UrlSchemeActivity : BaseActivity() {
|
class UrlSchemeActivity : BaseActivity() {
|
||||||
@@ -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,7 +19,6 @@ 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.AppConfig.LOOPBACK
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -50,6 +49,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)
|
||||||
@@ -75,6 +106,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
R.id.add_file -> showFileChooser().let { true }
|
R.id.add_file -> showFileChooser().let { true }
|
||||||
R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
|
R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
|
||||||
|
R.id.add_qrcode -> importAssetFromQRcode().let { true }
|
||||||
R.id.download_file -> downloadGeoFiles().let { true }
|
R.id.download_file -> downloadGeoFiles().let { true }
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
@@ -85,27 +117,7 @@ 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 ->
|
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
@@ -156,6 +168,33 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun importAssetFromQRcode(): Boolean {
|
||||||
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scanQRCodeForAssetURL = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == RESULT_OK) {
|
||||||
|
importAsset(it.data?.getStringExtra("SCAN_RESULT"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importAsset(url: String?): Boolean {
|
||||||
|
try {
|
||||||
|
if (!Utils.isValidUrl(url)) {
|
||||||
|
toast(R.string.toast_invalid_url)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Send URL to UserAssetUrlActivity for Processing
|
||||||
|
startActivity(Intent(this, UserAssetUrlActivity::class.java)
|
||||||
|
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun downloadGeoFiles() {
|
private fun downloadGeoFiles() {
|
||||||
val dialog = AlertDialog.Builder(this)
|
val dialog = AlertDialog.Builder(this)
|
||||||
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
||||||
@@ -298,7 +337,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()
|
||||||
@@ -314,4 +353,4 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
|
|
||||||
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
||||||
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,11 @@ import com.v2ray.ang.util.Utils
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class UserAssetUrlActivity : BaseActivity() {
|
class UserAssetUrlActivity : BaseActivity() {
|
||||||
|
// Receive QRcode URL from UserAssetActivity
|
||||||
|
companion object {
|
||||||
|
const val ASSET_URL_QRCODE = "ASSET_URL_QRCODE"
|
||||||
|
}
|
||||||
|
|
||||||
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
var del_config: MenuItem? = null
|
||||||
@@ -28,10 +33,15 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
title = getString(R.string.title_user_asset_add_url)
|
title = getString(R.string.title_user_asset_add_url)
|
||||||
|
|
||||||
val assetItem = MmkvManager.decodeAsset(editAssetId)
|
val assetItem = MmkvManager.decodeAsset(editAssetId)
|
||||||
if (assetItem != null) {
|
val assetUrlQrcode = intent.getStringExtra(ASSET_URL_QRCODE)
|
||||||
bindingAsset(assetItem)
|
val assetNameQrcode = File(assetUrlQrcode.toString()).name
|
||||||
} else {
|
when {
|
||||||
clearAsset()
|
assetItem != null -> bindingAsset(assetItem)
|
||||||
|
assetUrlQrcode != null -> {
|
||||||
|
binding.etRemarks.setText(assetNameQrcode)
|
||||||
|
binding.etUrl.setText(assetUrlQrcode)
|
||||||
|
}
|
||||||
|
else -> clearAsset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +116,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()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import com.v2ray.ang.dto.AppInfo
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
object AppManagerUtil {
|
||||||
|
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) {
|
||||||
|
val applicationInfo = pkg.applicationInfo ?: continue
|
||||||
|
|
||||||
|
val appName = applicationInfo.loadLabel(packageManager).toString()
|
||||||
|
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
|
||||||
|
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
|
||||||
|
|
||||||
|
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
||||||
|
apps.add(appInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext apps
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.v2ray.ang.util
|
|||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
import com.google.gson.JsonPrimitive
|
import com.google.gson.JsonPrimitive
|
||||||
import com.google.gson.JsonSerializationContext
|
import com.google.gson.JsonSerializationContext
|
||||||
import com.google.gson.JsonSerializer
|
import com.google.gson.JsonSerializer
|
||||||
@@ -15,11 +17,13 @@ object JsonUtil {
|
|||||||
return gson.toJson(src)
|
return gson.toJson(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> fromJson(json: String, cls: Class<T>): T {
|
fun <T> fromJson(src: String, cls: Class<T>): T {
|
||||||
return gson.fromJson(json, cls)
|
return gson.fromJson(src, cls)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toJsonPretty(src: Any?): String {
|
fun toJsonPretty(src: Any?): String? {
|
||||||
|
if (src == null)
|
||||||
|
return null
|
||||||
val gsonPre = GsonBuilder()
|
val gsonPre = GsonBuilder()
|
||||||
.setPrettyPrinting()
|
.setPrettyPrinting()
|
||||||
.disableHtmlEscaping()
|
.disableHtmlEscaping()
|
||||||
@@ -34,4 +38,15 @@ object JsonUtil {
|
|||||||
.create()
|
.create()
|
||||||
return gsonPre.toJson(src)
|
return gsonPre.toJson(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parseString(src: String?): JsonObject? {
|
||||||
|
if (src == null)
|
||||||
|
return null
|
||||||
|
try {
|
||||||
|
return JsonParser.parseString(src).getAsJsonObject()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user