mirror of
https://github.com/2dust/v2rayNG.git
synced 2025-06-30 13:19:51 +00:00
Compare commits
228 commits
Author | SHA1 | Date | |
---|---|---|---|
|
3bf911da9c | ||
|
3f778a1ea2 | ||
|
8e03de8055 | ||
|
1f42d7fc07 | ||
|
0700e834f1 | ||
|
777190e861 | ||
|
33572477fc | ||
|
2fb6e62e13 | ||
|
94cc72d2b9 | ||
|
f68c353715 | ||
|
e077c18108 | ||
|
1a5e105212 | ||
|
e0881caab4 | ||
|
7219425258 | ||
|
51eabe5440 | ||
|
6f0b3ce990 | ||
|
69e27ed3bb | ||
|
fff6ab30e6 | ||
|
fdb67a86f4 | ||
|
ea088376ac | ||
|
52332d960e | ||
|
3ead542e2b | ||
|
9d1f98ff34 | ||
|
f305e26a39 | ||
|
aa47fba20d | ||
|
69c5bbfd3d | ||
|
90ed02804c | ||
|
822c1de79c | ||
|
d910b93525 | ||
|
7e6b1c247b | ||
|
f3f2b7fab5 | ||
|
e6f260da76 | ||
|
55bc2bf934 | ||
|
f22454da5d | ||
|
4a87549fa7 | ||
|
d447adc97f | ||
|
3773962b64 | ||
|
be0a2506ce | ||
|
7f9cb8dfdd | ||
|
71a5b6e480 | ||
|
02e53ced50 | ||
|
42c27a5e7e | ||
|
af04bbcf87 | ||
|
9bedfe8a7b | ||
|
2fdf684ee7 | ||
|
5b79951da7 | ||
|
06aa680d45 | ||
|
cdb9b1811c | ||
|
0fc1f2f5d3 | ||
|
ef1bb3dd34 | ||
|
1bca321d3f | ||
|
247e2b3ba3 | ||
|
41fd2b0cfb | ||
|
72da42ee40 | ||
|
c130d55e8f | ||
|
5ae84f7eac | ||
|
df5ea251e1 | ||
|
8890d9f004 | ||
|
4fcb3f9d06 | ||
|
5bf7c98cd3 | ||
|
46bc1a49df | ||
|
21175f41ec | ||
|
864c63987e | ||
|
4ac0547e22 | ||
|
12a9ee262c | ||
|
cfa9c19c94 | ||
|
56e33e6cdd | ||
|
02421072c1 | ||
|
b862a0dc65 | ||
|
1f25d6a000 | ||
|
e1def0616a | ||
|
83fd6efc17 | ||
|
f0c0e2e83a | ||
|
6ca3eb769e | ||
|
963d24ab66 | ||
|
cfd81441fa | ||
|
4084ae2938 | ||
|
3f9bc098ec | ||
|
9cb28ed969 | ||
|
773ddc5373 | ||
|
38193b5621 | ||
|
358713a2a3 | ||
|
5b9f24c1f0 | ||
|
c47c2c3666 | ||
|
49f7c3e7d7 | ||
|
423e5de2c6 | ||
|
3e3387e63e | ||
|
debddace8b | ||
|
160b412e0a | ||
|
0f3e0a0ea2 | ||
|
c4cf90e807 | ||
|
5db46e81b7 | ||
|
1ef80a3a96 | ||
|
a46d9d0d2a | ||
|
7b80536e1e | ||
|
5733ecf20e | ||
|
eae33b61cf | ||
|
9e55b525f1 | ||
|
678b3cb505 | ||
|
b4c833b039 | ||
|
597bd021b8 | ||
|
ba03118a43 | ||
|
82148408b0 | ||
|
042900e065 | ||
|
874fccc351 | ||
|
14f36872e7 | ||
|
3b6ad3052a | ||
|
194fc6b6ed | ||
|
0275ad54ac | ||
|
7ca4044467 | ||
|
1672494ee9 | ||
|
bbbbc72d22 | ||
|
1e7f49b756 | ||
|
ac4c0f7ee1 | ||
|
6cc91b1a89 | ||
|
45facff41d | ||
|
ee703e6c95 | ||
|
87213c34a6 | ||
|
73a7c76183 | ||
|
ed5282f2b3 | ||
|
390c657047 | ||
|
7071072862 | ||
|
d111328541 | ||
|
76cb2aaf46 | ||
|
7ff1397163 | ||
|
bcf5d49a3d | ||
|
4fffb17283 | ||
|
83b8bdfdf4 | ||
|
cc1538a24d | ||
|
eb19199d18 | ||
|
441e5ef8d5 | ||
|
d768774aad | ||
|
c3d83907a5 | ||
|
b52a98ae5e | ||
|
a70e4089e3 | ||
|
397989769c | ||
|
796676abdc | ||
|
3c93ccb86c | ||
|
fc8c74184a | ||
|
1939e6b5cf | ||
|
1c2ac9385d | ||
|
7513f1fe07 | ||
|
63e2c02daa | ||
|
fec76385e1 | ||
|
bf21428746 | ||
|
01ab8dad17 | ||
|
8974f73841 | ||
|
d3a2a2413c | ||
|
9b52863270 | ||
|
733914a7a7 | ||
|
7b0ab1ea4d | ||
|
51ff95f071 | ||
|
fc7aca46ce | ||
|
78e34caff3 | ||
|
d2aecd4dee | ||
|
9be1083495 | ||
|
38eb9ee13f | ||
|
15b36dfc57 | ||
|
40a83f7cac | ||
|
fd9f912c18 | ||
|
eef6e60dbb | ||
|
f0de5275b9 | ||
|
4eb5c0263c | ||
|
7a0d997a81 | ||
|
17b7c6d357 | ||
|
206f2cb306 | ||
|
1bc433097b | ||
|
9bee5fbe99 | ||
|
e819798d80 | ||
|
14ff9eb527 | ||
|
a60f45ce31 | ||
|
0b9a96209f | ||
|
3747e58e4e | ||
|
28b1788dc1 | ||
|
12ab2954b0 | ||
|
d0e8937f03 | ||
|
1a7ab97a3a | ||
|
ef4145787b | ||
|
899e4c1b14 | ||
|
faa4385087 | ||
|
172d9fd093 | ||
|
093716baaa | ||
|
0165ad54b3 | ||
|
b52dd33102 | ||
|
00aed90f2f | ||
|
5f3d7c0213 | ||
|
401f051774 | ||
|
e14b48f3eb | ||
|
1972f83b86 | ||
|
55a11bbeee | ||
|
ca32ee3a0e | ||
|
159a370286 | ||
|
d1904d52d9 | ||
|
5e993fd91b | ||
|
bada7c93d7 | ||
|
38850597f3 | ||
|
da347492d3 | ||
|
2ec5d8db3c | ||
|
fd9c5040bf | ||
|
aa328f0add | ||
|
9743d7b87b | ||
|
98fb0c433e | ||
|
7c9fcd9f43 | ||
|
54c76d9968 | ||
|
40b3f0fedc | ||
|
dcfcf83430 | ||
|
e46b354643 | ||
|
f497e4e301 | ||
|
b65e4b3819 | ||
|
d166b036fc | ||
|
ddf5f22037 | ||
|
7d8a9f2b6d | ||
|
0a1695e3d7 | ||
|
4a653d4935 | ||
|
2bc31a10c5 | ||
|
e8d2c6214b | ||
|
3a0f2687e9 | ||
|
04c98326b2 | ||
|
eb22c7f303 | ||
|
d51a4d7a7e | ||
|
0fb705e1e2 | ||
|
10b849ef09 | ||
|
d7d3b23cea | ||
|
c3786d434e | ||
|
9e3b92014a | ||
|
f4e088131b | ||
|
e55e069fe3 | ||
|
d8d3767798 |
188 changed files with 7623 additions and 4081 deletions
89
.github/workflows/build.yml
vendored
89
.github/workflows/build.yml
vendored
|
@ -16,57 +16,50 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4.2.2
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3.2.0
|
||||||
|
with:
|
||||||
|
log-accepted-android-sdk-licenses: false
|
||||||
|
cmdline-tools-version: '12266719'
|
||||||
|
packages: 'platforms;android-35 build-tools;35.0.0 platform-tools'
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: |
|
||||||
|
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
|
||||||
|
--channel=3 \
|
||||||
|
--install "ndk;29.0.13113456"
|
||||||
|
echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.13113456" >> $GITHUB_ENV
|
||||||
|
sed -i '10i\
|
||||||
|
\
|
||||||
|
ndkVersion = "29.0.13113456"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
||||||
|
|
||||||
- name: Restore cached libtun2socks
|
- name: Restore cached libtun2socks
|
||||||
id: cache-libtun2socks-restore
|
id: cache-libtun2socks-restore
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/AndroidLibXrayLite/libs
|
path: ${{ github.workspace }}/libs
|
||||||
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/AndroidLibXrayLite/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/AndroidLibXrayLite/modules/libancillary/HEAD') }}
|
key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ 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
|
- name: Build libtun2socks
|
||||||
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
cd ${{ github.workspace }}/AndroidLibXrayLite
|
|
||||||
bash compile-tun2socks.sh
|
bash compile-tun2socks.sh
|
||||||
tar -xvzf libtun2socks.so.tgz
|
|
||||||
env:
|
|
||||||
NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
|
||||||
|
|
||||||
- name: Save libtun2socks
|
- name: Save libtun2socks
|
||||||
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/save@v4
|
uses: actions/cache/save@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/AndroidLibXrayLite/libs
|
path: ${{ github.workspace }}/libs
|
||||||
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/AndroidLibXrayLite/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/AndroidLibXrayLite/modules/libancillary/HEAD') }}
|
key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
|
||||||
|
|
||||||
- name: Copy libtun2socks
|
- name: Copy libtun2socks
|
||||||
run: |
|
run: |
|
||||||
cp -r ${{ github.workspace }}/AndroidLibXrayLite/libs ${{ github.workspace }}/V2rayNG/app
|
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
|
||||||
|
|
||||||
- name: Fetch AndroidLibXrayLite tag
|
- name: Fetch AndroidLibXrayLite tag
|
||||||
run: |
|
run: |
|
||||||
|
@ -77,7 +70,7 @@ jobs:
|
||||||
popd
|
popd
|
||||||
|
|
||||||
- name: Download libv2ray
|
- name: Download libv2ray
|
||||||
uses: robinraju/release-downloader@v1
|
uses: robinraju/release-downloader@v1.12
|
||||||
with:
|
with:
|
||||||
repository: '2dust/AndroidLibXrayLite'
|
repository: '2dust/AndroidLibXrayLite'
|
||||||
tag: ${{ env.CURRENT_TAG }}
|
tag: ${{ env.CURRENT_TAG }}
|
||||||
|
@ -89,50 +82,39 @@ jobs:
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/hysteria/libs
|
path: ${{ github.workspace }}/hysteria/libs
|
||||||
key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
||||||
|
|
||||||
- name: Setup Golang
|
- name: Setup Golang
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5.4.0
|
||||||
with:
|
with:
|
||||||
go-version-file: 'AndroidLibXrayLite/go.mod'
|
go-version-file: 'AndroidLibXrayLite/go.mod'
|
||||||
|
cache: false
|
||||||
- name: Patch Go use 600296
|
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
|
||||||
#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: Build libhysteria2
|
- name: Build libhysteria2
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
bash libhysteria2.sh
|
bash libhysteria2.sh
|
||||||
env:
|
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
|
||||||
|
|
||||||
- name: Save libhysteria2
|
- name: Save libhysteria2
|
||||||
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/save@v4
|
uses: actions/cache/save@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/hysteria/libs
|
path: ${{ github.workspace }}/hysteria/libs
|
||||||
key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
key: libhysteria2-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
|
||||||
|
|
||||||
- name: Copy libhysteria2
|
- name: Copy libhysteria2
|
||||||
run: |
|
run: |
|
||||||
cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app
|
cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4.7.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Setup Android environment
|
|
||||||
uses: android-actions/setup-android@v3
|
|
||||||
|
|
||||||
- name: Decode Keystore
|
- name: Decode Keystore
|
||||||
uses: timheuer/base64-to-file@v1
|
uses: timheuer/base64-to-file@v1.2.4
|
||||||
id: android_keystore
|
id: android_keystore
|
||||||
with:
|
with:
|
||||||
fileName: "android_keystore.jks"
|
fileName: "android_keystore.jks"
|
||||||
|
@ -141,28 +123,27 @@ jobs:
|
||||||
- name: Build APK
|
- name: Build APK
|
||||||
run: |
|
run: |
|
||||||
cd ${{ github.workspace }}/V2rayNG
|
cd ${{ github.workspace }}/V2rayNG
|
||||||
|
echo "sdk.dir=${ANDROID_HOME}" > local.properties
|
||||||
chmod 755 gradlew
|
chmod 755 gradlew
|
||||||
./gradlew licenseFdroidReleaseReport
|
./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 }}
|
./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.6.2
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: arm64-v8a
|
name: arm64-v8a
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
|
||||||
|
|
||||||
- name: Upload armeabi-v7a APK
|
- name: Upload armeabi-v7a APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4.6.2
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: armeabi-v7a
|
name: armeabi-v7a
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
|
||||||
|
|
||||||
- name: Upload x86 APK
|
- name: Upload x86 APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4.6.2
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: x86-apk
|
name: x86-apk
|
||||||
|
|
2
.github/workflows/fastlane.yml
vendored
2
.github/workflows/fastlane.yml
vendored
|
@ -13,4 +13,4 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Validate Fastlane Supply Metadata
|
- name: Validate Fastlane Supply Metadata
|
||||||
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0
|
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0
|
||||||
|
|
60
.gitignore
vendored
60
.gitignore
vendored
|
@ -1,6 +1,66 @@
|
||||||
|
# Ignore data and key store files
|
||||||
*.dat
|
*.dat
|
||||||
*.jks
|
*.jks
|
||||||
|
|
||||||
|
# Ignore output JSON file
|
||||||
V2rayNG/app/release/output.json
|
V2rayNG/app/release/output.json
|
||||||
|
|
||||||
|
# Ignore IDE and build system directories
|
||||||
.idea/
|
.idea/
|
||||||
.gradle/
|
.gradle/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Ignore local properties and DS_Store files
|
||||||
|
/local.properties
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Ignore build directories and captures
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
V2rayNG/app/build
|
||||||
|
V2rayNG/build
|
||||||
|
V2rayNG/local.properties
|
||||||
|
|
||||||
|
# Ignore APK and AAR files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
|
||||||
|
# Ignore signing properties
|
||||||
|
signing.properties
|
||||||
|
|
||||||
|
# Ignore shared object files
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
|
# Ignore Google services JSON
|
||||||
|
V2rayNG/app/google-services.json
|
||||||
|
|
||||||
|
# Additional common Android/Java ignores
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*.orig
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
|
||||||
|
# Ignore executable files
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.obj
|
||||||
|
*.o
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Ignore files from other IDEs
|
||||||
|
.vscode/
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# Ignore OS-specific files
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -4,3 +4,9 @@
|
||||||
[submodule "AndroidLibXrayLite"]
|
[submodule "AndroidLibXrayLite"]
|
||||||
path = AndroidLibXrayLite
|
path = AndroidLibXrayLite
|
||||||
url = https://github.com/2dust/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 +1 @@
|
||||||
Subproject commit 664c3892e22aab6501f9f5c5fb5473da605528a0
|
Subproject commit 8ad3e1ddf165d8d67e488346b2faa9153d3e33a4
|
|
@ -3,16 +3,12 @@
|
||||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||||
|
|
||||||
[](https://developer.android.com/about/versions/lollipop)
|
[](https://developer.android.com/about/versions/lollipop)
|
||||||
[](https://kotlinlang.org)
|
[](https://kotlinlang.org)
|
||||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||||
[](https://github.com/2dust/v2rayNG/releases)
|
[](https://github.com/2dust/v2rayNG/releases)
|
||||||
[](https://t.me/v2rayn)
|
[](https://t.me/v2rayn)
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
|
||||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
### Telegram Channel
|
### Telegram Channel
|
||||||
[github_2dust](https://t.me/github_2dust)
|
[github_2dust](https://t.me/github_2dust)
|
||||||
|
|
||||||
|
@ -21,7 +17,7 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
|
||||||
#### Geoip and Geosite
|
#### Geoip and Geosite
|
||||||
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
||||||
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
||||||
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually
|
- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually
|
||||||
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
|
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
|
||||||
|
|
||||||
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||||
|
|
10
V2rayNG/.gitignore
vendored
10
V2rayNG/.gitignore
vendored
|
@ -1,10 +0,0 @@
|
||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
*.apk
|
|
||||||
signing.properties
|
|
||||||
*.aar
|
|
2
V2rayNG/app/.gitignore
vendored
2
V2rayNG/app/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
/build
|
|
||||||
/google-services.json
|
|
|
@ -12,8 +12,8 @@ android {
|
||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 628
|
versionCode = 658
|
||||||
versionName = "1.9.32"
|
versionName = "1.10.8"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
@ -82,8 +82,9 @@ android {
|
||||||
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
|
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
|
||||||
if (isFdroid) {
|
if (isFdroid) {
|
||||||
val versionCodes =
|
val versionCodes =
|
||||||
mapOf("armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
|
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 }
|
||||||
|
@ -148,7 +149,7 @@ dependencies {
|
||||||
|
|
||||||
// UI Libraries
|
// UI Libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.toastcompat)
|
implementation(libs.toasty)
|
||||||
implementation(libs.editorkit)
|
implementation(libs.editorkit)
|
||||||
implementation(libs.flexbox)
|
implementation(libs.flexbox)
|
||||||
|
|
||||||
|
@ -157,9 +158,8 @@ dependencies {
|
||||||
implementation(libs.gson)
|
implementation(libs.gson)
|
||||||
|
|
||||||
// Reactive and Utility Libraries
|
// Reactive and Utility Libraries
|
||||||
implementation(libs.rxjava)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.rxandroid)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.rxpermissions)
|
|
||||||
|
|
||||||
// Language and Processing Libraries
|
// Language and Processing Libraries
|
||||||
implementation(libs.language.base)
|
implementation(libs.language.base)
|
||||||
|
|
|
@ -35,7 +35,6 @@
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
|
@ -52,6 +51,7 @@
|
||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppThemeDayNight"
|
android:theme="@style/AppThemeDayNight"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
@ -144,6 +144,9 @@
|
||||||
<data android:host="install-sub" />
|
<data android:host="install-sub" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.CheckUpdateActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.AboutActivity"
|
android:name=".ui.AboutActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
@ -212,7 +215,8 @@
|
||||||
android:icon="@drawable/ic_stat_name"
|
android:icon="@drawable/ic_stat_name"
|
||||||
android:label="@string/app_tile_name"
|
android:label="@string/app_tile_name"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
tools:targetApi="24">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -20,13 +20,6 @@
|
||||||
"port": "443",
|
"port": "443",
|
||||||
"network": "udp"
|
"network": "udp"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"remarks": "阻断广告",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ads-all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"remarks": "绕过局域网IP",
|
"remarks": "绕过局域网IP",
|
||||||
"outboundTag": "direct",
|
"outboundTag": "direct",
|
||||||
|
|
|
@ -5,13 +5,6 @@
|
||||||
"port": "443",
|
"port": "443",
|
||||||
"network": "udp"
|
"network": "udp"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"remarks": "阻断广告",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ads-all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"remarks": "绕过局域网IP",
|
"remarks": "绕过局域网IP",
|
||||||
"outboundTag": "direct",
|
"outboundTag": "direct",
|
||||||
|
|
|
@ -13,13 +13,6 @@
|
||||||
"port": "443",
|
"port": "443",
|
||||||
"network": "udp"
|
"network": "udp"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"remarks": "阻断广告",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ads-all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"remarks": "绕过局域网IP",
|
"remarks": "绕过局域网IP",
|
||||||
"outboundTag": "direct",
|
"outboundTag": "direct",
|
||||||
|
|
|
@ -5,13 +5,6 @@
|
||||||
"port": "443",
|
"port": "443",
|
||||||
"network": "udp"
|
"network": "udp"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"remarks": "Block ads and trackers",
|
|
||||||
"outboundTag": "block",
|
|
||||||
"domain": [
|
|
||||||
"geosite:category-ads-all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"remarks": "Direct LAN IP",
|
"remarks": "Direct LAN IP",
|
||||||
"outboundTag": "direct",
|
"outboundTag": "direct",
|
||||||
|
|
|
@ -4,6 +4,7 @@ au.com.shiftyjelly.pocketcasts
|
||||||
bbc.mobile.news.ww
|
bbc.mobile.news.ww
|
||||||
be.mygod.vpnhotspot
|
be.mygod.vpnhotspot
|
||||||
ch.protonmail.android
|
ch.protonmail.android
|
||||||
|
cm.aptoide.pt
|
||||||
co.wanqu.android
|
co.wanqu.android
|
||||||
com.alphainventor.filemanager
|
com.alphainventor.filemanager
|
||||||
com.amazon.kindle
|
com.amazon.kindle
|
||||||
|
@ -34,7 +35,9 @@ com.chrome.canary
|
||||||
com.chrome.dev
|
com.chrome.dev
|
||||||
com.cl.newt66y
|
com.cl.newt66y
|
||||||
com.cradle.iitc_mobile
|
com.cradle.iitc_mobile
|
||||||
|
org.exarhteam.iitc_mobile
|
||||||
com.cygames.shadowverse
|
com.cygames.shadowverse
|
||||||
|
com.dcard.freedom
|
||||||
com.devhd.feedly
|
com.devhd.feedly
|
||||||
com.devolver.reigns2
|
com.devolver.reigns2
|
||||||
com.discord
|
com.discord
|
||||||
|
@ -108,6 +111,7 @@ com.ifttt.ifttt
|
||||||
com.imgur.mobile
|
com.imgur.mobile
|
||||||
com.innologica.inoreader
|
com.innologica.inoreader
|
||||||
com.instagram.android
|
com.instagram.android
|
||||||
|
com.instagram.lite
|
||||||
com.instapaper.android
|
com.instapaper.android
|
||||||
com.jarvanh.vpntether
|
com.jarvanh.vpntether
|
||||||
com.kapp.youtube.final
|
com.kapp.youtube.final
|
||||||
|
@ -115,6 +119,7 @@ com.klinker.android.twitter_l
|
||||||
com.lastpass.lpandroid
|
com.lastpass.lpandroid
|
||||||
com.linecorp.linelite
|
com.linecorp.linelite
|
||||||
com.lingodeer
|
com.lingodeer
|
||||||
|
com.ltnnews.news
|
||||||
com.mediapods.tumbpods
|
com.mediapods.tumbpods
|
||||||
com.mgoogle.android.gms
|
com.mgoogle.android.gms
|
||||||
com.microsoft.emmx
|
com.microsoft.emmx
|
||||||
|
@ -159,6 +164,7 @@ com.slack
|
||||||
com.snaptube.premium
|
com.snaptube.premium
|
||||||
com.sololearn
|
com.sololearn
|
||||||
com.sonelli.juicessh
|
com.sonelli.juicessh
|
||||||
|
com.sparkslab.dcardreader
|
||||||
com.spotify.music
|
com.spotify.music
|
||||||
com.tencent.huatuo
|
com.tencent.huatuo
|
||||||
com.termux
|
com.termux
|
||||||
|
@ -173,10 +179,13 @@ com.twitter.android
|
||||||
com.u91porn
|
com.u91porn
|
||||||
com.u9porn
|
com.u9porn
|
||||||
com.ubisoft.dance.justdance2015companion
|
com.ubisoft.dance.justdance2015companion
|
||||||
|
com.udn.news
|
||||||
com.utopia.pxview
|
com.utopia.pxview
|
||||||
com.valvesoftware.android.steam.communimunity
|
|
||||||
com.valvesoftware.android.steam.community
|
com.valvesoftware.android.steam.community
|
||||||
|
com.vanced.manager
|
||||||
com.vanced.android.youtube
|
com.vanced.android.youtube
|
||||||
|
com.vanced.android.apps.youtube.music
|
||||||
|
com.mgoogle.android.gms
|
||||||
com.vimeo.android.videoapp
|
com.vimeo.android.videoapp
|
||||||
com.vivaldi.browser
|
com.vivaldi.browser
|
||||||
com.vivaldi.browser.snapshot
|
com.vivaldi.browser.snapshot
|
||||||
|
@ -186,10 +195,12 @@ com.wire
|
||||||
com.wuxiangai.refactor
|
com.wuxiangai.refactor
|
||||||
com.xda.labs
|
com.xda.labs
|
||||||
com.xvideos.app
|
com.xvideos.app
|
||||||
|
com.yahoo.mobile.client.android.superapp
|
||||||
com.yandex.browser
|
com.yandex.browser
|
||||||
com.yandex.browser.beta
|
com.yandex.browser.beta
|
||||||
com.yandex.browser.alpha
|
com.yandex.browser.alpha
|
||||||
com.z28j.feel
|
com.z28j.feel
|
||||||
|
com.zhiliaoapp.musically
|
||||||
con.medium.reader
|
con.medium.reader
|
||||||
de.apkgrabber
|
de.apkgrabber
|
||||||
de.robv.android.xposed.installer
|
de.robv.android.xposed.installer
|
||||||
|
@ -210,6 +221,7 @@ jp.bokete.app.android
|
||||||
jp.naver.line.android
|
jp.naver.line.android
|
||||||
jp.pxv.android
|
jp.pxv.android
|
||||||
luo.speedometergpspro
|
luo.speedometergpspro
|
||||||
|
m.cna.com.tw.App
|
||||||
mark.via.gp
|
mark.via.gp
|
||||||
me.tshine.easymark
|
me.tshine.easymark
|
||||||
net.teeha.android.url_shortener
|
net.teeha.android.url_shortener
|
||||||
|
@ -226,6 +238,7 @@ org.mozilla.firefox_beta
|
||||||
org.mozilla.focus
|
org.mozilla.focus
|
||||||
org.schabi.newpipe
|
org.schabi.newpipe
|
||||||
org.telegram.messenger
|
org.telegram.messenger
|
||||||
|
org.telegram.messenger.web
|
||||||
org.telegram.multi
|
org.telegram.multi
|
||||||
org.telegram.plus
|
org.telegram.plus
|
||||||
org.thunderdog.challegram
|
org.thunderdog.challegram
|
||||||
|
@ -239,3 +252,162 @@ tw.com.gamer.android.activecenter
|
||||||
videodownloader.downloadvideo.downloader
|
videodownloader.downloadvideo.downloader
|
||||||
uk.co.bbc.learningenglish
|
uk.co.bbc.learningenglish
|
||||||
com.ted.android
|
com.ted.android
|
||||||
|
de.danoeh.antennapod
|
||||||
|
com.kiwibrowser.browser
|
||||||
|
nekox.messenger
|
||||||
|
com.nextcloud.client
|
||||||
|
com.aurora.store
|
||||||
|
com.aurora.adroid
|
||||||
|
chat.simplex.app
|
||||||
|
im.vector.app
|
||||||
|
network.loki.messenger
|
||||||
|
eu.siacs.conversations
|
||||||
|
xyz.nextalone.nagram
|
||||||
|
net.programmierecke.radiodroid2
|
||||||
|
im.fdx.v2ex
|
||||||
|
ml.docilealligator.infinityforreddit
|
||||||
|
com.bytemyth.ama
|
||||||
|
app.vanadium.browser
|
||||||
|
com.cakewallet.cake_wallet
|
||||||
|
org.purplei2p.i2pd
|
||||||
|
dk.tacit.android.foldersync.lite
|
||||||
|
com.nononsenseapps.feeder
|
||||||
|
com.m2049r.xmrwallet
|
||||||
|
com.paypal.android.p2pmobile
|
||||||
|
com.google.android.apps.googlevoice
|
||||||
|
com.readdle.spark
|
||||||
|
org.torproject.torbrowser
|
||||||
|
com.deepl.mobiletranslator
|
||||||
|
com.microsoft.bing
|
||||||
|
com.keylesspalace.tusky
|
||||||
|
com.ottplay.ottplay
|
||||||
|
ru.iptvremote.android.iptv.pro
|
||||||
|
jp.naver.line.android
|
||||||
|
com.xmflsct.app.tooot
|
||||||
|
com.forem.android
|
||||||
|
app.revanced.android.youtube
|
||||||
|
com.mgoogle.android.gms
|
||||||
|
com.pionex.client
|
||||||
|
vip.mytokenpocket
|
||||||
|
im.token.app
|
||||||
|
com.linekong.mars24
|
||||||
|
com.feixiaohao
|
||||||
|
com.aicoin.appandroid
|
||||||
|
com.binance.dev
|
||||||
|
com.kraken.trade
|
||||||
|
com.okinc.okex.gp
|
||||||
|
com.authy.authy
|
||||||
|
air.com.rosettastone.mobile.CoursePlayer
|
||||||
|
com.blizzard.bma
|
||||||
|
com.amazon.kindle
|
||||||
|
com.google.android.apps.fitness
|
||||||
|
net.tsapps.appsales
|
||||||
|
com.wemesh.android
|
||||||
|
com.google.android.apps.googleassistant
|
||||||
|
allen.town.focus.reader
|
||||||
|
me.hyliu.fluent_reader_lite
|
||||||
|
com.aljazeera.mobile
|
||||||
|
com.ft.news
|
||||||
|
de.marmaro.krt.ffupdater
|
||||||
|
myradio.radio.fmradio.liveradio.radiostation
|
||||||
|
com.google.earth
|
||||||
|
eu.kanade.tachiyomi.j2k
|
||||||
|
com.audials
|
||||||
|
com.microsoft.skydrive
|
||||||
|
com.mb.android.tg
|
||||||
|
com.melodis.midomiMusicIdentifier.freemium
|
||||||
|
com.foxnews.android
|
||||||
|
ch.threema.app
|
||||||
|
com.briarproject.briar.android
|
||||||
|
foundation.e.apps
|
||||||
|
com.valvesoftware.android.steam.friendsui
|
||||||
|
com.imback.yeetalk
|
||||||
|
so.onekey.app.wallet
|
||||||
|
com.xc3fff0e.xmanager
|
||||||
|
meditofoundation.medito
|
||||||
|
com.picol.client
|
||||||
|
com.streetwriters.notesnook
|
||||||
|
shanghai.panewsApp.com
|
||||||
|
org.coursera.android
|
||||||
|
com.positron_it.zlib
|
||||||
|
com.blizzard.messenger
|
||||||
|
com.javdb.javrocket
|
||||||
|
com.picacomic.fregata
|
||||||
|
com.fxl.chacha
|
||||||
|
me.proton.android.drive
|
||||||
|
com.lastpass.lpandroid
|
||||||
|
com.tradingview.tradingviewapp
|
||||||
|
com.deviantart.android.damobile
|
||||||
|
com.fusionmedia.investing
|
||||||
|
com.ewa.ewaapp
|
||||||
|
com.duolingo
|
||||||
|
com.hellotalk
|
||||||
|
io.github.huskydg.magisk
|
||||||
|
com.jsy.xpgbox
|
||||||
|
com.hostloc.app.hostloc
|
||||||
|
com.dena.pokota
|
||||||
|
com.vitorpamplona.amethyst
|
||||||
|
com.zhiliaoapp.musically
|
||||||
|
us.spotco.fennec_dos
|
||||||
|
com.fongmi.android.tv
|
||||||
|
com.pocketprep.android.itcybersecurity
|
||||||
|
com.cloudtv
|
||||||
|
com.glassdoor.app
|
||||||
|
com.indeed.android.jobsearch
|
||||||
|
com.linkedin.android
|
||||||
|
com.github.tvbox.osc.bh
|
||||||
|
com.example.douban
|
||||||
|
com.sipnetic.app
|
||||||
|
com.microsoft.rdc.androidx
|
||||||
|
org.zwanoo.android.speedtest
|
||||||
|
com.sonelli.juicessh
|
||||||
|
com.scmp.newspulse
|
||||||
|
org.lsposed.manager
|
||||||
|
mnn.Android
|
||||||
|
com.thomsonretuers.reuters
|
||||||
|
com.guardian
|
||||||
|
com.ttxapps.onesyncv2
|
||||||
|
org.fcitx.fcitx5.android.updater
|
||||||
|
com.tailscale.ipn
|
||||||
|
tw.nekomimi.nekogram
|
||||||
|
com.nexon.kartdrift
|
||||||
|
io.syncapps.lemmy_sync
|
||||||
|
com.seazon.feedme
|
||||||
|
com.readwise
|
||||||
|
de.spiritcroc.riotx
|
||||||
|
com.openai.chatgpt
|
||||||
|
io.changenow.changenow
|
||||||
|
com.poe.android
|
||||||
|
com.twingate
|
||||||
|
com.blinkslabs.blinkist.android
|
||||||
|
com.ichi2.anki
|
||||||
|
md.obsidian
|
||||||
|
com.musixmatch.android.lyrify
|
||||||
|
com.cyber.turbo
|
||||||
|
com.offsec.nethunter
|
||||||
|
me.ghui.v2er
|
||||||
|
com.samruston.twitter
|
||||||
|
org.adaway
|
||||||
|
org.swiftapps.swiftbackup
|
||||||
|
com.zerotier.one
|
||||||
|
com.quietmobile
|
||||||
|
com.instagram.barcelona
|
||||||
|
im.molly.app
|
||||||
|
com.rvx.android.youtube
|
||||||
|
com.deepl.mobiletranslator
|
||||||
|
com.qingsong.yingmi
|
||||||
|
com.lemurbrowser.exts
|
||||||
|
com.silverdev.dnartdroid
|
||||||
|
me.ash.reader
|
||||||
|
de.tutao.tutanota
|
||||||
|
dev.imranr.obtainium
|
||||||
|
com.getsomeheadspace.android
|
||||||
|
org.cromite.cromite
|
||||||
|
com.nutomic.syncthingandroid
|
||||||
|
com.bumble.app
|
||||||
|
com.cnn.mobile.android.phone
|
||||||
|
com.google.android.apps.authenticator2
|
||||||
|
com.microsoft.copilot
|
||||||
|
com.netflix.NGP.Storyteller
|
||||||
|
com.Slack
|
||||||
|
com.server.auditor.ssh.client
|
|
@ -97,7 +97,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"routing": {
|
"routing": {
|
||||||
"domainStrategy": "IPIfNonMatch",
|
"domainStrategy": "AsIs",
|
||||||
"rules": []
|
"rules": []
|
||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
package com.v2ray.ang
|
package com.v2ray.ang
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication() {
|
class AngApplication : MultiDexApplication() {
|
||||||
companion object {
|
companion object {
|
||||||
//const val PREF_LAST_VERSION = "pref_last_version"
|
|
||||||
lateinit var application: AngApplication
|
lateinit var application: AngApplication
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the base context to the application.
|
||||||
|
* @param base The base context.
|
||||||
|
*/
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
application = this
|
application = this
|
||||||
|
@ -26,23 +26,22 @@ class AngApplication : MultiDexApplication() {
|
||||||
.setDefaultProcessName("${ANG_PACKAGE}:bg")
|
.setDefaultProcessName("${ANG_PACKAGE}:bg")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the application.
|
||||||
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// LeakCanary.install(this)
|
|
||||||
|
|
||||||
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
|
|
||||||
// if (firstRun)
|
|
||||||
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
|
||||||
|
|
||||||
MMKV.initialize(this)
|
MMKV.initialize(this)
|
||||||
|
|
||||||
Utils.setNightMode()
|
SettingsManager.setNightMode()
|
||||||
// Initialize WorkManager with the custom configuration
|
// Initialize WorkManager with the custom configuration
|
||||||
WorkManager.initialize(this, workManagerConfiguration)
|
WorkManager.initialize(this, workManagerConfiguration)
|
||||||
|
|
||||||
SettingsManager.initRoutingRulesets(this)
|
SettingsManager.initRoutingRulesets(this)
|
||||||
}
|
|
||||||
|
|
||||||
|
es.dmoral.toasty.Toasty.Config.getInstance()
|
||||||
|
.setGravity(android.view.Gravity.BOTTOM, 0, 200)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ object AppConfig {
|
||||||
|
|
||||||
/** The application's package name. */
|
/** The application's package name. */
|
||||||
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
|
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
|
||||||
|
const val TAG = BuildConfig.APPLICATION_ID
|
||||||
|
|
||||||
/** Directory names used in the app's file system. */
|
/** Directory names used in the app's file system. */
|
||||||
const val DIR_ASSETS = "assets"
|
const val DIR_ASSETS = "assets"
|
||||||
|
@ -12,7 +13,6 @@ object AppConfig {
|
||||||
|
|
||||||
/** Legacy configuration keys. */
|
/** Legacy configuration keys. */
|
||||||
const val ANG_CONFIG = "ang_config"
|
const val ANG_CONFIG = "ang_config"
|
||||||
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
|
|
||||||
|
|
||||||
/** Preferences mapped to MMKV storage. */
|
/** Preferences mapped to MMKV storage. */
|
||||||
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
||||||
|
@ -26,6 +26,7 @@ object AppConfig {
|
||||||
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_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
|
||||||
|
const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index"
|
||||||
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
const val PREF_ROUTING_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"
|
||||||
|
@ -43,6 +44,7 @@ object AppConfig {
|
||||||
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||||
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
||||||
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
|
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
|
||||||
|
const val PREF_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display"
|
||||||
const val PREF_LANGUAGE = "pref_language"
|
const val PREF_LANGUAGE = "pref_language"
|
||||||
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
||||||
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||||
|
@ -54,15 +56,18 @@ object AppConfig {
|
||||||
const val PREF_DNS_HOSTS = "pref_dns_hosts"
|
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_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method"
|
||||||
const val PREF_MODE = "pref_mode"
|
const val PREF_MODE = "pref_mode"
|
||||||
const val PREF_IS_BOOTED = "pref_is_booted"
|
const val PREF_IS_BOOTED = "pref_is_booted"
|
||||||
|
const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release"
|
||||||
|
const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources"
|
||||||
|
|
||||||
/** Cache keys. */
|
/** Cache keys. */
|
||||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||||
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
||||||
|
|
||||||
/** Protocol identifiers. */
|
/** Protocol identifiers. */
|
||||||
const val PROTOCOL_FREEDOM: String = "freedom"
|
const val PROTOCOL_FREEDOM = "freedom"
|
||||||
|
|
||||||
/** Broadcast actions. */
|
/** Broadcast actions. */
|
||||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||||
|
@ -87,19 +92,20 @@ object AppConfig {
|
||||||
const val DOWNLINK = "downlink"
|
const val DOWNLINK = "downlink"
|
||||||
|
|
||||||
/** URLs for various resources. */
|
/** URLs for various resources. */
|
||||||
const val androidpackagenamelistUrl =
|
const val GITHUB_URL = "https://github.com"
|
||||||
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
|
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
|
||||||
const val v2rayCustomRoutingListUrl =
|
const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
|
||||||
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
|
const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
|
||||||
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
|
const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
|
||||||
const val v2rayNGIssues = "$v2rayNGUrl/issues"
|
const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
|
||||||
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
|
const val APP_ISSUES_URL = "$APP_URL/issues"
|
||||||
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
|
const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
|
||||||
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
||||||
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
|
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||||
const val TgChannelUrl = "https://t.me/github_2dust"
|
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||||
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
|
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||||
const val DelayTestUrl2 = "https://www.google.com/generate_204"
|
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
||||||
|
const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||||
|
|
||||||
/** DNS server addresses. */
|
/** DNS server addresses. */
|
||||||
const val DNS_PROXY = "1.1.1.1"
|
const val DNS_PROXY = "1.1.1.1"
|
||||||
|
@ -164,19 +170,13 @@ object AppConfig {
|
||||||
// Android Private DNS constants
|
// Android Private DNS constants
|
||||||
const val DNS_DNSPOD_DOMAIN = "dot.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_CLOUDFLARE_DOMAIN = "one.one.one.one"
|
const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
|
||||||
|
const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
|
||||||
|
const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
|
||||||
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
||||||
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
||||||
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
||||||
|
|
||||||
|
|
||||||
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
|
||||||
val DNS_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_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"
|
||||||
const val DEFAULT_LEVEL = 8
|
const val DEFAULT_LEVEL = 8
|
||||||
|
@ -185,4 +185,64 @@ object AppConfig {
|
||||||
const val REALITY = "reality"
|
const val REALITY = "reality"
|
||||||
const val HEADER_TYPE_HTTP = "http"
|
const val HEADER_TYPE_HTTP = "http"
|
||||||
|
|
||||||
|
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||||
|
val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||||
|
val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
|
||||||
|
val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9")
|
||||||
|
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||||
|
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
||||||
|
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
|
||||||
|
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
||||||
|
|
||||||
|
//minimum list https://serverfault.com/a/304791
|
||||||
|
val ROUTED_IP_LIST = arrayListOf(
|
||||||
|
"0.0.0.0/5",
|
||||||
|
"8.0.0.0/7",
|
||||||
|
"11.0.0.0/8",
|
||||||
|
"12.0.0.0/6",
|
||||||
|
"16.0.0.0/4",
|
||||||
|
"32.0.0.0/3",
|
||||||
|
"64.0.0.0/2",
|
||||||
|
"128.0.0.0/3",
|
||||||
|
"160.0.0.0/5",
|
||||||
|
"168.0.0.0/6",
|
||||||
|
"172.0.0.0/12",
|
||||||
|
"172.32.0.0/11",
|
||||||
|
"172.64.0.0/10",
|
||||||
|
"172.128.0.0/9",
|
||||||
|
"173.0.0.0/8",
|
||||||
|
"174.0.0.0/7",
|
||||||
|
"176.0.0.0/4",
|
||||||
|
"192.0.0.0/9",
|
||||||
|
"192.128.0.0/11",
|
||||||
|
"192.160.0.0/13",
|
||||||
|
"192.169.0.0/16",
|
||||||
|
"192.170.0.0/15",
|
||||||
|
"192.172.0.0/14",
|
||||||
|
"192.176.0.0/12",
|
||||||
|
"192.192.0.0/10",
|
||||||
|
"193.0.0.0/8",
|
||||||
|
"194.0.0.0/7",
|
||||||
|
"196.0.0.0/6",
|
||||||
|
"200.0.0.0/5",
|
||||||
|
"208.0.0.0/4",
|
||||||
|
"240.0.0.0/4"
|
||||||
|
)
|
||||||
|
|
||||||
|
val PRIVATE_IP_LIST = arrayListOf(
|
||||||
|
"0.0.0.0/8",
|
||||||
|
"10.0.0.0/8",
|
||||||
|
"127.0.0.0/8",
|
||||||
|
"172.16.0.0/12",
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"169.254.0.0/16",
|
||||||
|
"224.0.0.0/4"
|
||||||
|
)
|
||||||
|
|
||||||
|
val GEO_FILES_SOURCES = arrayListOf(
|
||||||
|
"Loyalsoldier/v2ray-rules-dat",
|
||||||
|
"runetfreedom/russia-v2ray-rules-dat",
|
||||||
|
"Chocolate4U/Iran-v2ray-rules"
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,6 @@ data class AssetUrlItem(
|
||||||
var remarks: String = "",
|
var remarks: String = "",
|
||||||
var url: String = "",
|
var url: String = "",
|
||||||
val addedTime: Long = System.currentTimeMillis(),
|
val addedTime: Long = System.currentTimeMillis(),
|
||||||
var lastUpdated: Long = -1
|
var lastUpdated: Long = -1,
|
||||||
|
var locked: Boolean? = false,
|
||||||
)
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class CheckUpdateResult(
|
||||||
|
val hasUpdate: Boolean,
|
||||||
|
val latestVersion: String? = null,
|
||||||
|
val releaseNotes: String? = null,
|
||||||
|
val downloadUrl: String? = null,
|
||||||
|
val error: String? = null,
|
||||||
|
val isPreRelease: Boolean = false
|
||||||
|
)
|
|
@ -4,6 +4,6 @@ data class ConfigResult(
|
||||||
var status: Boolean,
|
var status: Boolean,
|
||||||
var guid: String? = null,
|
var guid: String? = null,
|
||||||
var content: String = "",
|
var content: String = "",
|
||||||
var domainPort: String? = null,
|
var socksPort: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,8 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
|
||||||
VLESS(5, AppConfig.VLESS),
|
VLESS(5, AppConfig.VLESS),
|
||||||
TROJAN(6, AppConfig.TROJAN),
|
TROJAN(6, AppConfig.TROJAN),
|
||||||
WIREGUARD(7, AppConfig.WIREGUARD),
|
WIREGUARD(7, AppConfig.WIREGUARD),
|
||||||
// TUIC(8, AppConfig.TUIC),
|
|
||||||
|
// TUIC(8, AppConfig.TUIC),
|
||||||
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
||||||
HTTP(10, AppConfig.HTTP);
|
HTTP(10, AppConfig.HTTP);
|
||||||
|
|
||||||
|
|
23
V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
Normal file
23
V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class GitHubRelease(
|
||||||
|
@SerializedName("tag_name")
|
||||||
|
val tagName: String,
|
||||||
|
@SerializedName("body")
|
||||||
|
val body: String,
|
||||||
|
@SerializedName("assets")
|
||||||
|
val assets: List<Asset>,
|
||||||
|
@SerializedName("prerelease")
|
||||||
|
val prerelease: Boolean = false,
|
||||||
|
@SerializedName("published_at")
|
||||||
|
val publishedAt: String = ""
|
||||||
|
) {
|
||||||
|
data class Asset(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("browser_download_url")
|
||||||
|
val browserDownloadUrl: String
|
||||||
|
)
|
||||||
|
}
|
|
@ -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?,
|
||||||
|
)
|
||||||
|
}
|
12
V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
Normal file
12
V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class IPAPIInfo(
|
||||||
|
var ip: String? = null,
|
||||||
|
var clientIp: String? = null,
|
||||||
|
var ip_addr: String? = null,
|
||||||
|
var query: String? = null,
|
||||||
|
var country: String? = null,
|
||||||
|
var country_name: String? = null,
|
||||||
|
var country_code: String? = null,
|
||||||
|
var countryCode: String? = null
|
||||||
|
)
|
|
@ -8,6 +8,7 @@ enum class Language(val code: String) {
|
||||||
VIETNAMESE("vi"),
|
VIETNAMESE("vi"),
|
||||||
RUSSIAN("ru"),
|
RUSSIAN("ru"),
|
||||||
PERSIAN("fa"),
|
PERSIAN("fa"),
|
||||||
|
ARABIC("ar"),
|
||||||
BANGLA("bn"),
|
BANGLA("bn"),
|
||||||
BAKHTIARI("bqi-rIR");
|
BAKHTIARI("bqi-rIR");
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ enum class NetworkType(val type: String) {
|
||||||
XHTTP("xhttp"),
|
XHTTP("xhttp"),
|
||||||
HTTP("http"),
|
HTTP("http"),
|
||||||
H2("h2"),
|
H2("h2"),
|
||||||
|
|
||||||
//QUIC("quic"),
|
//QUIC("quic"),
|
||||||
GRPC("grpc");
|
GRPC("grpc");
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,8 @@ data class ProfileItem(
|
||||||
var portHopping: String? = null,
|
var portHopping: String? = null,
|
||||||
var portHoppingInterval: String? = null,
|
var portHoppingInterval: String? = null,
|
||||||
var pinSHA256: String? = null,
|
var pinSHA256: String? = null,
|
||||||
|
var bandwidthDown: String? = null,
|
||||||
|
var bandwidthUp: String? = null,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
data class ProfileLiteItem(
|
|
||||||
val configType: EConfigType,
|
|
||||||
var subscriptionId: String = "",
|
|
||||||
var remarks: String = "",
|
|
||||||
var server: String?,
|
|
||||||
var serverPort: Int?,
|
|
||||||
)
|
|
|
@ -11,5 +11,6 @@ data class SubscriptionItem(
|
||||||
var prevProfile: String? = null,
|
var prevProfile: String? = null,
|
||||||
var nextProfile: String? = null,
|
var nextProfile: String? = null,
|
||||||
var filter: String? = null,
|
var filter: String? = null,
|
||||||
|
var allowInsecureUrl: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,17 @@
|
||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.*
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.*
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
data class V2rayConfig(
|
data class V2rayConfig(
|
||||||
var remarks: String? = null,
|
var remarks: String? = null,
|
||||||
var stats: Any? = null,
|
var stats: Any? = null,
|
||||||
val log: LogBean,
|
val log: LogBean,
|
||||||
var policy: PolicyBean?,
|
var policy: PolicyBean? = null,
|
||||||
val inbounds: ArrayList<InboundBean>,
|
val inbounds: ArrayList<InboundBean>,
|
||||||
var outbounds: ArrayList<OutboundBean>,
|
var outbounds: ArrayList<OutboundBean>,
|
||||||
var dns: DnsBean,
|
var dns: DnsBean? = null,
|
||||||
val routing: RoutingBean,
|
val routing: RoutingBean,
|
||||||
val api: Any? = null,
|
val api: Any? = null,
|
||||||
val transport: Any? = null,
|
val transport: Any? = null,
|
||||||
|
@ -32,9 +23,9 @@ data class V2rayConfig(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class LogBean(
|
data class LogBean(
|
||||||
val access: String,
|
val access: String? = null,
|
||||||
val error: String,
|
val error: String? = null,
|
||||||
var loglevel: String?,
|
var loglevel: String? = null,
|
||||||
val dnsLog: Boolean? = null
|
val dnsLog: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,7 +35,7 @@ data class V2rayConfig(
|
||||||
var protocol: String,
|
var protocol: String,
|
||||||
var listen: String? = null,
|
var listen: String? = null,
|
||||||
val settings: Any? = null,
|
val settings: Any? = null,
|
||||||
val sniffing: SniffingBean?,
|
val sniffing: SniffingBean? = null,
|
||||||
val streamSettings: Any? = null,
|
val streamSettings: Any? = null,
|
||||||
val allocate: Any? = null
|
val allocate: Any? = null
|
||||||
) {
|
) {
|
||||||
|
@ -75,50 +66,6 @@ data class V2rayConfig(
|
||||||
val sendThrough: String? = null,
|
val sendThrough: String? = null,
|
||||||
var mux: MuxBean? = MuxBean(false)
|
var mux: MuxBean? = MuxBean(false)
|
||||||
) {
|
) {
|
||||||
companion object {
|
|
||||||
fun create(configType: EConfigType): OutboundBean? {
|
|
||||||
return when (configType) {
|
|
||||||
EConfigType.VMESS,
|
|
||||||
EConfigType.VLESS ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
vnext = listOf(
|
|
||||||
VnextBean(
|
|
||||||
users = listOf(UsersBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
streamSettings = StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.SHADOWSOCKS,
|
|
||||||
EConfigType.SOCKS,
|
|
||||||
EConfigType.HTTP,
|
|
||||||
EConfigType.TROJAN,
|
|
||||||
EConfigType.HYSTERIA2 ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
servers = listOf(ServersBean())
|
|
||||||
),
|
|
||||||
streamSettings = StreamSettingsBean()
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.WIREGUARD ->
|
|
||||||
return OutboundBean(
|
|
||||||
protocol = configType.name.lowercase(),
|
|
||||||
settings = OutSettingsBean(
|
|
||||||
secretKey = "",
|
|
||||||
peers = listOf(WireGuardBean())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
EConfigType.CUSTOM -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class OutSettingsBean(
|
data class OutSettingsBean(
|
||||||
var vnext: List<VnextBean>? = null,
|
var vnext: List<VnextBean>? = null,
|
||||||
var fragment: FragmentBean? = null,
|
var fragment: FragmentBean? = null,
|
||||||
|
@ -195,7 +142,7 @@ data class V2rayConfig(
|
||||||
|
|
||||||
data class WireGuardBean(
|
data class WireGuardBean(
|
||||||
var publicKey: String = "",
|
var publicKey: String = "",
|
||||||
var preSharedKey: String = "",
|
var preSharedKey: String? = null,
|
||||||
var endpoint: String = ""
|
var endpoint: String = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -257,7 +204,10 @@ data class V2rayConfig(
|
||||||
var header: HeaderBean = HeaderBean(),
|
var header: HeaderBean = HeaderBean(),
|
||||||
var seed: String? = null
|
var seed: String? = null
|
||||||
) {
|
) {
|
||||||
data class HeaderBean(var type: String = "none")
|
data class HeaderBean(
|
||||||
|
var type: String = "none",
|
||||||
|
var domain: String? = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class WsSettingsBean(
|
data class WsSettingsBean(
|
||||||
|
@ -294,7 +244,8 @@ data class V2rayConfig(
|
||||||
var tcpFastOpen: Boolean? = null,
|
var tcpFastOpen: Boolean? = null,
|
||||||
var tproxy: String? = null,
|
var tproxy: String? = null,
|
||||||
var mark: Int? = null,
|
var mark: Int? = null,
|
||||||
var dialerProxy: String? = null
|
var dialerProxy: String? = null,
|
||||||
|
var domainStrategy: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TlsSettingsBean(
|
data class TlsSettingsBean(
|
||||||
|
@ -344,134 +295,6 @@ data class V2rayConfig(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateTransportSettings(
|
|
||||||
transport: String,
|
|
||||||
headerType: String?,
|
|
||||||
host: String?,
|
|
||||||
path: String?,
|
|
||||||
seed: String?,
|
|
||||||
quicSecurity: String?,
|
|
||||||
key: String?,
|
|
||||||
mode: String?,
|
|
||||||
serviceName: String?,
|
|
||||||
authority: String?
|
|
||||||
): String? {
|
|
||||||
var sni: String? = null
|
|
||||||
network = if (transport.isEmpty()) NetworkType.TCP.type else transport
|
|
||||||
when (network) {
|
|
||||||
NetworkType.TCP.type -> {
|
|
||||||
val tcpSetting = TcpSettingsBean()
|
|
||||||
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
|
||||||
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
|
||||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
|
||||||
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
|
|
||||||
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
tcpSetting.header.request = requestObj
|
|
||||||
sni = requestObj.headers.Host?.getOrNull(0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tcpSetting.header.type = "none"
|
|
||||||
sni = host
|
|
||||||
}
|
|
||||||
tcpSettings = tcpSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.KCP.type -> {
|
|
||||||
val kcpsetting = KcpSettingsBean()
|
|
||||||
kcpsetting.header.type = headerType ?: "none"
|
|
||||||
if (seed.isNullOrEmpty()) {
|
|
||||||
kcpsetting.seed = null
|
|
||||||
} else {
|
|
||||||
kcpsetting.seed = seed
|
|
||||||
}
|
|
||||||
kcpSettings = kcpsetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.WS.type -> {
|
|
||||||
val wssetting = WsSettingsBean()
|
|
||||||
wssetting.headers.Host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
wssetting.path = path ?: "/"
|
|
||||||
wsSettings = wssetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.HTTP_UPGRADE.type -> {
|
|
||||||
val httpupgradeSetting = HttpupgradeSettingsBean()
|
|
||||||
httpupgradeSetting.host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
httpupgradeSetting.path = path ?: "/"
|
|
||||||
httpupgradeSettings = httpupgradeSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.XHTTP.type -> {
|
|
||||||
val xhttpSetting = XhttpSettingsBean()
|
|
||||||
xhttpSetting.host = host.orEmpty()
|
|
||||||
sni = host
|
|
||||||
xhttpSetting.path = path ?: "/"
|
|
||||||
xhttpSettings = xhttpSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
|
||||||
network = NetworkType.H2.type
|
|
||||||
val h2Setting = HttpSettingsBean()
|
|
||||||
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
|
||||||
sni = h2Setting.host.getOrNull(0)
|
|
||||||
h2Setting.path = path ?: "/"
|
|
||||||
httpSettings = h2Setting
|
|
||||||
}
|
|
||||||
|
|
||||||
// "quic" -> {
|
|
||||||
// val quicsetting = QuicSettingBean()
|
|
||||||
// quicsetting.security = quicSecurity ?: "none"
|
|
||||||
// quicsetting.key = key.orEmpty()
|
|
||||||
// quicsetting.header.type = headerType ?: "none"
|
|
||||||
// quicSettings = quicsetting
|
|
||||||
// }
|
|
||||||
|
|
||||||
NetworkType.GRPC.type -> {
|
|
||||||
val grpcSetting = GrpcSettingsBean()
|
|
||||||
grpcSetting.multiMode = mode == "multi"
|
|
||||||
grpcSetting.serviceName = serviceName.orEmpty()
|
|
||||||
grpcSetting.authority = authority.orEmpty()
|
|
||||||
grpcSetting.idle_timeout = 60
|
|
||||||
grpcSetting.health_check_timeout = 20
|
|
||||||
sni = authority
|
|
||||||
grpcSettings = grpcSetting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sni
|
|
||||||
}
|
|
||||||
|
|
||||||
fun populateTlsSettings(
|
|
||||||
streamSecurity: String,
|
|
||||||
allowInsecure: Boolean,
|
|
||||||
sni: String?,
|
|
||||||
fingerprint: String?,
|
|
||||||
alpns: String?,
|
|
||||||
publicKey: String?,
|
|
||||||
shortId: String?,
|
|
||||||
spiderX: String?
|
|
||||||
) {
|
|
||||||
security = if (streamSecurity.isEmpty()) null else streamSecurity
|
|
||||||
if (security == null) return
|
|
||||||
val tlsSetting = TlsSettingsBean(
|
|
||||||
allowInsecure = allowInsecure,
|
|
||||||
serverName = if (sni.isNullOrEmpty()) null else sni,
|
|
||||||
fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
|
|
||||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
|
||||||
publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
|
|
||||||
shortId = if (shortId.isNullOrEmpty()) null else shortId,
|
|
||||||
spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
|
|
||||||
)
|
|
||||||
if (security == AppConfig.TLS) {
|
|
||||||
tlsSettings = tlsSetting
|
|
||||||
realitySettings = null
|
|
||||||
} else if (security == AppConfig.REALITY) {
|
|
||||||
tlsSettings = null
|
|
||||||
realitySettings = tlsSetting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MuxBean(
|
data class MuxBean(
|
||||||
|
@ -637,6 +460,18 @@ data class V2rayConfig(
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean {
|
||||||
|
val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also {
|
||||||
|
streamSettings = it
|
||||||
|
}
|
||||||
|
|
||||||
|
val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also {
|
||||||
|
stream.sockopt = it
|
||||||
|
}
|
||||||
|
|
||||||
|
return sockopt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DnsBean(
|
data class DnsBean(
|
||||||
|
@ -713,15 +548,9 @@ data class V2rayConfig(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toPrettyPrinting(): String {
|
fun getAllProxyOutbound(): List<OutboundBean> {
|
||||||
return GsonBuilder()
|
return outbounds.filter { outbound ->
|
||||||
.setPrettyPrinting()
|
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
|
||||||
.disableHtmlEscaping()
|
}
|
||||||
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
|
|
||||||
object : TypeToken<Double>() {}.type,
|
|
||||||
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
|
|
||||||
)
|
|
||||||
.create()
|
|
||||||
.toJson(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VPN interface address configuration enum class
|
||||||
|
* Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration.
|
||||||
|
* Each option provides client and router addresses to establish point-to-point VPN tunnels.
|
||||||
|
*/
|
||||||
|
enum class VpnInterfaceAddressConfig(
|
||||||
|
val displayName: String,
|
||||||
|
val ipv4Client: String,
|
||||||
|
val ipv4Router: String,
|
||||||
|
val ipv6Client: String,
|
||||||
|
val ipv6Router: String
|
||||||
|
) {
|
||||||
|
OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"),
|
||||||
|
OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"),
|
||||||
|
OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"),
|
||||||
|
OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"),
|
||||||
|
OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"),
|
||||||
|
OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"),
|
||||||
|
OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Retrieves the VPN interface address configuration based on the specified index.
|
||||||
|
*
|
||||||
|
* @param index The configuration index (0-based) corresponding to user selection
|
||||||
|
* @return The VpnInterfaceAddressConfig instance at the specified index,
|
||||||
|
* or OPTION_1 (default) if the index is out of bounds
|
||||||
|
*/
|
||||||
|
fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig {
|
||||||
|
return if (index in values().indices) {
|
||||||
|
values()[index]
|
||||||
|
} else {
|
||||||
|
OPTION_1 // Default to the first configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
import me.drakeet.support.toast.ToastCompat
|
import es.dmoral.toasty.Toasty
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
@ -17,18 +17,75 @@ import java.net.URLConnection
|
||||||
val Context.v2RayApplication: AngApplication?
|
val Context.v2RayApplication: AngApplication?
|
||||||
get() = applicationContext as? AngApplication
|
get() = applicationContext as? AngApplication
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given resource ID.
|
||||||
|
*
|
||||||
|
* @param message The resource ID of the message to show.
|
||||||
|
*/
|
||||||
fun Context.toast(message: Int) {
|
fun Context.toast(message: Int) {
|
||||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
Toasty.normal(this, message).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given text.
|
||||||
|
*
|
||||||
|
* @param message The text of the message to show.
|
||||||
|
*/
|
||||||
fun Context.toast(message: CharSequence) {
|
fun Context.toast(message: CharSequence) {
|
||||||
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show()
|
Toasty.normal(this, message).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given resource ID.
|
||||||
|
*
|
||||||
|
* @param message The resource ID of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toastSuccess(message: Int) {
|
||||||
|
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given text.
|
||||||
|
*
|
||||||
|
* @param message The text of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toastSuccess(message: CharSequence) {
|
||||||
|
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given resource ID.
|
||||||
|
*
|
||||||
|
* @param message The resource ID of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toastError(message: Int) {
|
||||||
|
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given text.
|
||||||
|
*
|
||||||
|
* @param message The text of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toastError(message: CharSequence) {
|
||||||
|
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts a key-value pair into the JSONObject.
|
||||||
|
*
|
||||||
|
* @param pair The key-value pair to put.
|
||||||
|
*/
|
||||||
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
|
fun JSONObject.putOpt(pair: Pair<String, Any?>) {
|
||||||
put(pair.first, pair.second)
|
put(pair.first, pair.second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts multiple key-value pairs into the JSONObject.
|
||||||
|
*
|
||||||
|
* @param pairs The map of key-value pairs to put.
|
||||||
|
*/
|
||||||
fun JSONObject.putOpt(pairs: Map<String, Any?>) {
|
fun JSONObject.putOpt(pairs: Map<String, Any?>) {
|
||||||
pairs.forEach { put(it.key, it.value) }
|
pairs.forEach { put(it.key, it.value) }
|
||||||
}
|
}
|
||||||
|
@ -36,8 +93,18 @@ fun JSONObject.putOpt(pairs: Map<String, Any?>) {
|
||||||
const val THRESHOLD = 1000L
|
const val THRESHOLD = 1000L
|
||||||
const val DIVISOR = 1024.0
|
const val DIVISOR = 1024.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Long value to a speed string.
|
||||||
|
*
|
||||||
|
* @return The speed string.
|
||||||
|
*/
|
||||||
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Long value to a traffic string.
|
||||||
|
*
|
||||||
|
* @return The traffic string.
|
||||||
|
*/
|
||||||
fun Long.toTrafficString(): String {
|
fun Long.toTrafficString(): String {
|
||||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
||||||
var size = this.toDouble()
|
var size = this.toDouble()
|
||||||
|
@ -59,10 +126,27 @@ val URLConnection.responseLength: Long
|
||||||
val URI.idnHost: String
|
val URI.idnHost: String
|
||||||
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
||||||
|
|
||||||
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
|
/**
|
||||||
|
* Removes all whitespace from the string.
|
||||||
|
*
|
||||||
|
* @return The string without whitespace.
|
||||||
|
*/
|
||||||
|
fun String?.removeWhiteSpace(): String? = this?.replace(" ", "")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the string to a Long value, or returns 0 if the conversion fails.
|
||||||
|
*
|
||||||
|
* @return The Long value.
|
||||||
|
*/
|
||||||
fun String.toLongEx(): Long = toLongOrNull() ?: 0
|
fun String.toLongEx(): Long = toLongOrNull() ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for package changes and executes a callback when a change occurs.
|
||||||
|
*
|
||||||
|
* @param onetime Whether to unregister the receiver after the first callback.
|
||||||
|
* @param callback The callback to execute when a package change occurs.
|
||||||
|
* @return The BroadcastReceiver that was registered.
|
||||||
|
*/
|
||||||
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
@ -85,14 +169,44 @@ fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Uni
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a serializable object from the Bundle.
|
||||||
|
*
|
||||||
|
* @param key The key of the serializable object.
|
||||||
|
* @return The serializable object, or null if not found.
|
||||||
|
*/
|
||||||
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
|
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
|
||||||
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
|
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a serializable object from the Intent.
|
||||||
|
*
|
||||||
|
* @param key The key of the serializable object.
|
||||||
|
* @return The serializable object, or null if not found.
|
||||||
|
*/
|
||||||
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
|
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
|
||||||
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
|
/**
|
||||||
|
* Checks if the CharSequence is not null and not empty.
|
||||||
|
*
|
||||||
|
* @return True if the CharSequence is not null and not empty, false otherwise.
|
||||||
|
*/
|
||||||
|
fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty()
|
||||||
|
|
||||||
|
fun String.concatUrl(vararg paths: String): String {
|
||||||
|
val builder = StringBuilder(this.trimEnd('/'))
|
||||||
|
|
||||||
|
paths.forEach { path ->
|
||||||
|
val trimmedPath = path.trim('/')
|
||||||
|
if (trimmedPath.isNotEmpty()) {
|
||||||
|
builder.append('/').append(trimmedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString()
|
||||||
|
}
|
|
@ -6,6 +6,12 @@ import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
|
||||||
object CustomFmt : FmtBase() {
|
object CustomFmt : FmtBase() {
|
||||||
|
/**
|
||||||
|
* Parses a JSON string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the JSON string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
val config = ProfileItem.create(EConfigType.CUSTOM)
|
||||||
|
|
||||||
|
|
|
@ -4,32 +4,55 @@ import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.NetworkType
|
import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
open class FmtBase {
|
open class FmtBase {
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a URI string.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @param userInfo the user information to include in the URI
|
||||||
|
* @param dicQuery the query parameters to include in the URI
|
||||||
|
* @return the converted URI string
|
||||||
|
*/
|
||||||
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
||||||
val query = if (dicQuery != null)
|
val query = if (dicQuery != null)
|
||||||
("?" + dicQuery.toList().joinToString(
|
"?" + dicQuery.toList().joinToString(
|
||||||
separator = "&",
|
separator = "&",
|
||||||
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
|
transform = { it.first + "=" + Utils.urlEncode(it.second) })
|
||||||
else ""
|
else ""
|
||||||
|
|
||||||
val url = String.format(
|
val url = String.format(
|
||||||
"%s@%s:%s",
|
"%s@%s:%s",
|
||||||
Utils.urlEncode(userInfo ?: ""),
|
Utils.urlEncode(userInfo ?: ""),
|
||||||
Utils.getIpv6Address(config.server),
|
Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
|
||||||
config.serverPort
|
config.serverPort
|
||||||
)
|
)
|
||||||
|
|
||||||
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
|
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts query parameters from a URI.
|
||||||
|
*
|
||||||
|
* @param uri the URI to extract query parameters from
|
||||||
|
* @return a map of query parameters
|
||||||
|
*/
|
||||||
fun getQueryParam(uri: URI): Map<String, String> {
|
fun getQueryParam(uri: URI): Map<String, String> {
|
||||||
return uri.rawQuery.split("&")
|
return uri.rawQuery.split("&")
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates a ProfileItem object with values from query parameters.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to populate
|
||||||
|
* @param queryParam the query parameters to use for populating the ProfileItem
|
||||||
|
* @param allowInsecure whether to allow insecure connections
|
||||||
|
*/
|
||||||
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
|
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
|
||||||
config.network = queryParam["type"] ?: NetworkType.TCP.type
|
config.network = queryParam["type"] ?: NetworkType.TCP.type
|
||||||
config.headerType = queryParam["headerType"]
|
config.headerType = queryParam["headerType"]
|
||||||
|
@ -63,6 +86,12 @@ open class FmtBase {
|
||||||
config.flow = queryParam["flow"]
|
config.flow = queryParam["flow"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a map of query parameters from a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to create query parameters from
|
||||||
|
* @return a map of query parameters
|
||||||
|
*/
|
||||||
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
|
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
|
||||||
val dicQuery = HashMap<String, String>()
|
val dicQuery = HashMap<String, String>()
|
||||||
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
|
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
|
||||||
|
@ -93,7 +122,7 @@ open class FmtBase {
|
||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkType.XHTTP -> {
|
NetworkType.XHTTP -> {
|
||||||
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||||
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||||
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
||||||
|
@ -122,4 +151,20 @@ open class FmtBase {
|
||||||
return dicQuery
|
return dicQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fun getServerAddress(profileItem: ProfileItem): String {
|
||||||
|
if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
|
||||||
|
return profileItem.server.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
|
||||||
|
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
//Resolve and replace domain
|
||||||
|
val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||||
|
if (resolvedIps.isNullOrEmpty()) {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
return resolvedIps.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,14 +4,20 @@ import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import kotlin.text.orEmpty
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
|
|
||||||
object HttpFmt : FmtBase() {
|
object HttpFmt : FmtBase() {
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
|
*
|
||||||
|
* @param profileItem the ProfileItem object to convert
|
||||||
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = getServerAddress(profileItem)
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
@ -23,6 +29,4 @@ object HttpFmt : FmtBase() {
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -9,16 +9,23 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object Hysteria2Fmt : FmtBase() {
|
object Hysteria2Fmt : FmtBase() {
|
||||||
|
/**
|
||||||
|
* Parses a Hysteria2 URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the Hysteria2 URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem? {
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
@ -45,6 +52,12 @@ object Hysteria2Fmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a URI string.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @return the converted URI string
|
||||||
|
*/
|
||||||
fun toUri(config: ProfileItem): String {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = HashMap<String, String>()
|
val dicQuery = HashMap<String, String>()
|
||||||
|
|
||||||
|
@ -67,6 +80,13 @@ object Hysteria2Fmt : FmtBase() {
|
||||||
return toUri(config, config.password, dicQuery)
|
return toUri(config, config.password, dicQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a Hysteria2Bean object.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @param socksPort the port number for the socks5 proxy
|
||||||
|
* @return the converted Hysteria2Bean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
|
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
|
||||||
|
|
||||||
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
|
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
|
||||||
|
@ -85,6 +105,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 +122,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||||
auth = config.password,
|
auth = config.password,
|
||||||
obfs = obfs,
|
obfs = obfs,
|
||||||
transport = transport,
|
transport = transport,
|
||||||
|
bandwidth = bandwidth,
|
||||||
socks5 = Hysteria2Bean.Socks5Bean(
|
socks5 = Hysteria2Bean.Socks5Bean(
|
||||||
listen = "$LOOPBACK:${socksPort}",
|
listen = "$LOOPBACK:${socksPort}",
|
||||||
),
|
),
|
||||||
|
@ -111,10 +138,14 @@ object Hysteria2Fmt : FmtBase() {
|
||||||
return bean
|
return bean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
|
*
|
||||||
|
* @param profileItem the ProfileItem object to convert
|
||||||
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,18 +1,33 @@
|
||||||
package com.v2ray.ang.fmt
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.NetworkType
|
import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.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.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object ShadowsocksFmt : FmtBase() {
|
object ShadowsocksFmt : FmtBase() {
|
||||||
|
/**
|
||||||
|
* Parses a Shadowsocks URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the Shadowsocks URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem? {
|
||||||
return parseSip002(str) ?: parseLegacy(str)
|
return parseSip002(str) ?: parseLegacy(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a SIP002 Shadowsocks URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the SIP002 Shadowsocks URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parseSip002(str: String): ProfileItem? {
|
fun parseSip002(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
|
@ -21,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
|
||||||
if (uri.port <= 0) return null
|
if (uri.port <= 0) return null
|
||||||
if (uri.userInfo.isNullOrEmpty()) return null
|
if (uri.userInfo.isNullOrEmpty()) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
@ -55,6 +70,12 @@ object ShadowsocksFmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a legacy Shadowsocks URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the legacy Shadowsocks URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parseLegacy(str: String): ProfileItem? {
|
fun parseLegacy(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||||
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||||
|
@ -64,7 +85,7 @@ object ShadowsocksFmt : FmtBase() {
|
||||||
config.remarks =
|
config.remarks =
|
||||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
result = result.substring(0, indexSplit)
|
result = result.substring(0, indexSplit)
|
||||||
|
@ -92,48 +113,42 @@ object ShadowsocksFmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a URI string.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @return the converted URI string
|
||||||
|
*/
|
||||||
fun toUri(config: ProfileItem): String {
|
fun toUri(config: ProfileItem): String {
|
||||||
val pw = "${config.method}:${config.password}"
|
val pw = "${config.method}:${config.password}"
|
||||||
|
|
||||||
return toUri(config, Utils.encode(pw), null)
|
return toUri(config, Utils.encode(pw), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
|
*
|
||||||
|
* @param profileItem the ProfileItem object to convert
|
||||||
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = getServerAddress(profileItem)
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
server.password = profileItem.password
|
server.password = profileItem.password
|
||||||
server.method = profileItem.method
|
server.method = profileItem.method
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
profileItem.network.orEmpty(),
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
profileItem.headerType,
|
}
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.let {
|
||||||
profileItem.security.orEmpty(),
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
profileItem.insecure == true,
|
}
|
||||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
profileItem.publicKey,
|
|
||||||
profileItem.shortId,
|
|
||||||
profileItem.spiderX,
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -5,11 +5,17 @@ import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object SocksFmt : FmtBase() {
|
object SocksFmt : FmtBase() {
|
||||||
|
/**
|
||||||
|
* Parses a Socks URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the Socks URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.SOCKS)
|
val config = ProfileItem.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
|
@ -17,7 +23,7 @@ object SocksFmt : FmtBase() {
|
||||||
if (uri.idnHost.isEmpty()) return null
|
if (uri.idnHost.isEmpty()) return null
|
||||||
if (uri.port <= 0) return null
|
if (uri.port <= 0) return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
@ -32,6 +38,12 @@ object SocksFmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a URI string.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @return the converted URI string
|
||||||
|
*/
|
||||||
fun toUri(config: ProfileItem): String {
|
fun toUri(config: ProfileItem): String {
|
||||||
val pw =
|
val pw =
|
||||||
if (config.username.isNotNullEmpty())
|
if (config.username.isNotNullEmpty())
|
||||||
|
@ -42,11 +54,17 @@ object SocksFmt : FmtBase() {
|
||||||
return toUri(config, Utils.encode(pw), null)
|
return toUri(config, Utils.encode(pw), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
|
*
|
||||||
|
* @param profileItem the ProfileItem object to convert
|
||||||
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = getServerAddress(profileItem)
|
||||||
server.port = profileItem.serverPort.orEmpty().toInt()
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
if (profileItem.username.isNotNullEmpty()) {
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
@ -58,5 +76,4 @@ object SocksFmt : FmtBase() {
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,17 +7,23 @@ 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.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object TrojanFmt : FmtBase() {
|
object TrojanFmt : FmtBase() {
|
||||||
|
/**
|
||||||
|
* Parses a Trojan URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the Trojan URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem? {
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.TROJAN)
|
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
@ -36,45 +42,41 @@ object TrojanFmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a URI string.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @return the converted URI string
|
||||||
|
*/
|
||||||
fun toUri(config: ProfileItem): String {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = getQueryDic(config)
|
val dicQuery = getQueryDic(config)
|
||||||
|
|
||||||
return toUri(config, config.password, dicQuery)
|
return toUri(config, config.password, dicQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
|
*
|
||||||
|
* @param profileItem the ProfileItem object to convert
|
||||||
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
|
||||||
|
|
||||||
outboundBean?.settings?.servers?.first()?.let { server ->
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
server.address = profileItem.server.orEmpty()
|
server.address = getServerAddress(profileItem)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
profileItem.network.orEmpty(),
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
profileItem.headerType,
|
}
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.let {
|
||||||
profileItem.security.orEmpty(),
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
profileItem.insecure == true,
|
}
|
||||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
profileItem.publicKey,
|
|
||||||
profileItem.shortId,
|
|
||||||
profileItem.spiderX,
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,18 @@ 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.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object VlessFmt : FmtBase() {
|
object VlessFmt : FmtBase() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a Vless URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the Vless URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem? {
|
||||||
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.VLESS)
|
val config = ProfileItem.create(EConfigType.VLESS)
|
||||||
|
@ -20,7 +26,7 @@ object VlessFmt : FmtBase() {
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
@ -31,6 +37,12 @@ object VlessFmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a URI string.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @return the converted URI string
|
||||||
|
*/
|
||||||
fun toUri(config: ProfileItem): String {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = getQueryDic(config)
|
val dicQuery = getQueryDic(config)
|
||||||
dicQuery["encryption"] = config.method ?: "none"
|
dicQuery["encryption"] = config.method ?: "none"
|
||||||
|
@ -38,46 +50,31 @@ object VlessFmt : FmtBase() {
|
||||||
return toUri(config, config.password, dicQuery)
|
return toUri(config, config.password, dicQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
|
*
|
||||||
|
* @param profileItem the ProfileItem object to convert
|
||||||
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
vnext.address = profileItem.server.orEmpty()
|
vnext.address = getServerAddress(profileItem)
|
||||||
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].encryption = profileItem.method
|
vnext.users[0].encryption = profileItem.method
|
||||||
vnext.users[0].flow = profileItem.flow
|
vnext.users[0].flow = profileItem.flow
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
profileItem.network.orEmpty(),
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
profileItem.headerType,
|
}
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
|
|
||||||
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.let {
|
||||||
profileItem.security.orEmpty(),
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
profileItem.insecure == true,
|
}
|
||||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
profileItem.publicKey,
|
|
||||||
profileItem.shortId,
|
|
||||||
profileItem.spiderX,
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -11,12 +11,18 @@ import com.v2ray.ang.dto.VmessQRCode
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object VmessFmt : FmtBase() {
|
object VmessFmt : FmtBase() {
|
||||||
|
/**
|
||||||
|
* Parses a Vmess string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the Vmess string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem? {
|
||||||
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
||||||
return parseVmessStd(str)
|
return parseVmessStd(str)
|
||||||
|
@ -28,7 +34,7 @@ object VmessFmt : FmtBase() {
|
||||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
||||||
result = Utils.decode(result)
|
result = Utils.decode(result)
|
||||||
if (TextUtils.isEmpty(result)) {
|
if (TextUtils.isEmpty(result)) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
Log.w(AppConfig.TAG, "Toast decoding failed")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
|
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
|
||||||
|
@ -38,7 +44,7 @@ object VmessFmt : FmtBase() {
|
||||||
|| TextUtils.isEmpty(vmessQRCode.id)
|
|| TextUtils.isEmpty(vmessQRCode.id)
|
||||||
|| TextUtils.isEmpty(vmessQRCode.net)
|
|| TextUtils.isEmpty(vmessQRCode.net)
|
||||||
) {
|
) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
|
Log.w(AppConfig.TAG, "Toast incorrect protocol")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +74,7 @@ object VmessFmt : FmtBase() {
|
||||||
config.serviceName = vmessQRCode.path
|
config.serviceName = vmessQRCode.path
|
||||||
config.authority = vmessQRCode.host
|
config.authority = vmessQRCode.host
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +87,12 @@ object VmessFmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a URI string.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @return the converted URI string
|
||||||
|
*/
|
||||||
fun toUri(config: ProfileItem): String {
|
fun toUri(config: ProfileItem): String {
|
||||||
val vmessQRCode = VmessQRCode()
|
val vmessQRCode = VmessQRCode()
|
||||||
|
|
||||||
|
@ -108,6 +121,7 @@ object VmessFmt : FmtBase() {
|
||||||
vmessQRCode.path = config.serviceName.orEmpty()
|
vmessQRCode.path = config.serviceName.orEmpty()
|
||||||
vmessQRCode.host = config.authority.orEmpty()
|
vmessQRCode.host = config.authority.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +137,12 @@ object VmessFmt : FmtBase() {
|
||||||
return Utils.encode(json)
|
return Utils.encode(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a standard Vmess URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the standard Vmess URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parseVmessStd(str: String): ProfileItem? {
|
fun parseVmessStd(str: String): ProfileItem? {
|
||||||
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
val config = ProfileItem.create(EConfigType.VMESS)
|
val config = ProfileItem.create(EConfigType.VMESS)
|
||||||
|
@ -131,7 +151,7 @@ object VmessFmt : FmtBase() {
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
config.password = uri.userInfo
|
config.password = uri.userInfo
|
||||||
|
@ -142,40 +162,29 @@ object VmessFmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
|
*
|
||||||
|
* @param profileItem the ProfileItem object to convert
|
||||||
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
|
||||||
|
|
||||||
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
vnext.address = profileItem.server.orEmpty()
|
vnext.address = getServerAddress(profileItem)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = outboundBean?.streamSettings?.populateTransportSettings(
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
profileItem.network.orEmpty(),
|
V2rayConfigManager.populateTransportSettings(it, profileItem)
|
||||||
profileItem.headerType,
|
}
|
||||||
profileItem.host,
|
|
||||||
profileItem.path,
|
|
||||||
profileItem.seed,
|
|
||||||
profileItem.quicSecurity,
|
|
||||||
profileItem.quicKey,
|
|
||||||
profileItem.mode,
|
|
||||||
profileItem.serviceName,
|
|
||||||
profileItem.authority,
|
|
||||||
)
|
|
||||||
|
|
||||||
outboundBean?.streamSettings?.populateTlsSettings(
|
outboundBean?.streamSettings?.let {
|
||||||
profileItem.security.orEmpty(),
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
profileItem.insecure == true,
|
}
|
||||||
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
|
|
||||||
profileItem.fingerPrint,
|
|
||||||
profileItem.alpn,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,18 @@ import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.extension.removeWhiteSpace
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.text.orEmpty
|
|
||||||
|
|
||||||
object WireguardFmt : FmtBase() {
|
object WireguardFmt : FmtBase() {
|
||||||
|
/**
|
||||||
|
* Parses a URI string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the URI string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parse(str: String): ProfileItem? {
|
fun parse(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
|
@ -18,20 +25,26 @@ object WireguardFmt : FmtBase() {
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
val queryParam = getQueryParam(uri)
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
|
||||||
config.server = uri.idnHost
|
config.server = uri.idnHost
|
||||||
config.serverPort = uri.port.toString()
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
config.secretKey = uri.userInfo.orEmpty()
|
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.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
|
||||||
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"
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a Wireguard configuration file string into a ProfileItem object.
|
||||||
|
*
|
||||||
|
* @param str the Wireguard configuration file string to parse
|
||||||
|
* @return the parsed ProfileItem object, or null if parsing fails
|
||||||
|
*/
|
||||||
fun parseWireguardConfFile(str: String): ProfileItem? {
|
fun parseWireguardConfFile(str: String): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
|
@ -71,7 +84,7 @@ object WireguardFmt : FmtBase() {
|
||||||
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
config.publicKey = peerParams["publickey"].orEmpty()
|
config.publicKey = peerParams["publickey"].orEmpty()
|
||||||
config.preSharedKey = peerParams["presharedkey"].orEmpty()
|
config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
|
||||||
val endpoint = peerParams["endpoint"].orEmpty()
|
val endpoint = peerParams["endpoint"].orEmpty()
|
||||||
val endpointParts = endpoint.split(":", limit = 2)
|
val endpointParts = endpoint.split(":", limit = 2)
|
||||||
if (endpointParts.size == 2) {
|
if (endpointParts.size == 2) {
|
||||||
|
@ -86,37 +99,49 @@ object WireguardFmt : FmtBase() {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to an OutboundBean object.
|
||||||
|
*
|
||||||
|
* @param profileItem the ProfileItem object to convert
|
||||||
|
* @return the converted OutboundBean object, or null if conversion fails
|
||||||
|
*/
|
||||||
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
outboundBean?.settings?.let { wireguard ->
|
outboundBean?.settings?.let { wireguard ->
|
||||||
wireguard.secretKey = profileItem.secretKey
|
wireguard.secretKey = profileItem.secretKey
|
||||||
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||||
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
|
peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() }
|
||||||
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||||
}
|
}
|
||||||
wireguard.mtu = profileItem.mtu
|
wireguard.mtu = profileItem.mtu
|
||||||
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
|
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
|
||||||
}
|
}
|
||||||
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a ProfileItem object to a URI string.
|
||||||
|
*
|
||||||
|
* @param config the ProfileItem object to convert
|
||||||
|
* @return the converted URI string
|
||||||
|
*/
|
||||||
fun toUri(config: ProfileItem): String {
|
fun toUri(config: ProfileItem): String {
|
||||||
val dicQuery = HashMap<String, String>()
|
val dicQuery = HashMap<String, String>()
|
||||||
|
|
||||||
dicQuery["publickey"] = config.publicKey.orEmpty()
|
dicQuery["publickey"] = config.publicKey.orEmpty()
|
||||||
if (config.reserved != null) {
|
if (config.reserved != null) {
|
||||||
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty()
|
dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty()
|
||||||
}
|
}
|
||||||
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty()
|
dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty()
|
||||||
if (config.mtu != null) {
|
if (config.mtu != null) {
|
||||||
dicQuery["mtu"] = config.mtu.toString()
|
dicQuery["mtu"] = config.mtu.toString()
|
||||||
}
|
}
|
||||||
if (config.preSharedKey != null) {
|
if (config.preSharedKey != null) {
|
||||||
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty()
|
dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
return toUri(config, config.secretKey, dicQuery)
|
return toUri(config, config.secretKey, dicQuery)
|
||||||
|
|
|
@ -7,7 +7,9 @@ import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.HY2
|
import com.v2ray.ang.AppConfig.HY2
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.*
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
import com.v2ray.ang.fmt.ShadowsocksFmt
|
import com.v2ray.ang.fmt.ShadowsocksFmt
|
||||||
|
@ -16,14 +18,314 @@ import com.v2ray.ang.fmt.TrojanFmt
|
||||||
import com.v2ray.ang.fmt.VlessFmt
|
import com.v2ray.ang.fmt.VlessFmt
|
||||||
import com.v2ray.ang.fmt.VmessFmt
|
import com.v2ray.ang.fmt.VmessFmt
|
||||||
import com.v2ray.ang.fmt.WireguardFmt
|
import com.v2ray.ang.fmt.WireguardFmt
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object AngConfigManager {
|
object AngConfigManager {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* parse config form qrcode or...
|
* Shares the configuration to the clipboard.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param guid The GUID of the configuration.
|
||||||
|
* @return The result code.
|
||||||
|
*/
|
||||||
|
fun share2Clipboard(context: Context, guid: String): Int {
|
||||||
|
try {
|
||||||
|
val conf = shareConfig(guid)
|
||||||
|
if (TextUtils.isEmpty(conf)) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.setClipboard(context, conf)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to share config to clipboard", e)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares non-custom configurations to the clipboard.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param serverList The list of server GUIDs.
|
||||||
|
* @return The number of configurations shared.
|
||||||
|
*/
|
||||||
|
fun shareNonCustomConfigsToClipboard(context: Context, serverList: List<String>): Int {
|
||||||
|
try {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (guid in serverList) {
|
||||||
|
val url = shareConfig(guid)
|
||||||
|
if (TextUtils.isEmpty(url)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sb.append(url)
|
||||||
|
sb.appendLine()
|
||||||
|
}
|
||||||
|
if (sb.count() > 0) {
|
||||||
|
Utils.setClipboard(context, sb.toString())
|
||||||
|
}
|
||||||
|
return sb.lines().count()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares the configuration as a QR code.
|
||||||
|
*
|
||||||
|
* @param guid The GUID of the configuration.
|
||||||
|
* @return The QR code bitmap.
|
||||||
|
*/
|
||||||
|
fun share2QRCode(guid: String): Bitmap? {
|
||||||
|
try {
|
||||||
|
val conf = shareConfig(guid)
|
||||||
|
if (TextUtils.isEmpty(conf)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return QRCodeDecoder.createQRCode(conf)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to share config as QR code", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares the full content of the configuration to the clipboard.
|
||||||
|
*
|
||||||
|
* @param context The context.
|
||||||
|
* @param guid The GUID of the configuration.
|
||||||
|
* @return The result code.
|
||||||
|
*/
|
||||||
|
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
|
||||||
|
try {
|
||||||
|
if (guid == null) return -1
|
||||||
|
val result = V2rayConfigManager.getV2rayConfig(context, guid)
|
||||||
|
if (result.status) {
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid)
|
||||||
|
if (config?.configType == EConfigType.HYSTERIA2) {
|
||||||
|
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
|
||||||
|
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort)
|
||||||
|
Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
Utils.setClipboard(context, result.content)
|
||||||
|
} else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares the configuration.
|
||||||
|
*
|
||||||
|
* @param guid The GUID of the configuration.
|
||||||
|
* @return The configuration string.
|
||||||
|
*/
|
||||||
|
private fun shareConfig(guid: String): String {
|
||||||
|
try {
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
|
||||||
|
|
||||||
|
return config.configType.protocolScheme + when (config.configType) {
|
||||||
|
EConfigType.VMESS -> VmessFmt.toUri(config)
|
||||||
|
EConfigType.CUSTOM -> ""
|
||||||
|
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
|
||||||
|
EConfigType.SOCKS -> SocksFmt.toUri(config)
|
||||||
|
EConfigType.HTTP -> ""
|
||||||
|
EConfigType.VLESS -> VlessFmt.toUri(config)
|
||||||
|
EConfigType.TROJAN -> TrojanFmt.toUri(config)
|
||||||
|
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
||||||
|
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports a batch of configurations.
|
||||||
|
*
|
||||||
|
* @param server The server string.
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
* @param append Whether to append the configurations.
|
||||||
|
* @return A pair containing the number of configurations and subscriptions imported.
|
||||||
|
*/
|
||||||
|
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
|
||||||
|
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
||||||
|
if (count <= 0) {
|
||||||
|
count = parseBatchConfig(server, subid, append)
|
||||||
|
}
|
||||||
|
if (count <= 0) {
|
||||||
|
count = parseCustomConfigServer(server, subid)
|
||||||
|
}
|
||||||
|
|
||||||
|
var countSub = parseBatchSubscription(server)
|
||||||
|
if (countSub <= 0) {
|
||||||
|
countSub = parseBatchSubscription(Utils.decode(server))
|
||||||
|
}
|
||||||
|
if (countSub > 0) {
|
||||||
|
updateConfigViaSubAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
return count to countSub
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a batch of subscriptions.
|
||||||
|
*
|
||||||
|
* @param servers The servers string.
|
||||||
|
* @return The number of subscriptions parsed.
|
||||||
|
*/
|
||||||
|
private fun parseBatchSubscription(servers: String?): Int {
|
||||||
|
try {
|
||||||
|
if (servers == null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
servers.lines()
|
||||||
|
.distinct()
|
||||||
|
.forEach { str ->
|
||||||
|
if (Utils.isValidSubUrl(str)) {
|
||||||
|
count += importUrlAsSubscription(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse batch subscription", e)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a batch of configurations.
|
||||||
|
*
|
||||||
|
* @param servers The servers string.
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
* @param append Whether to append the configurations.
|
||||||
|
* @return The number of configurations parsed.
|
||||||
|
*/
|
||||||
|
private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
|
||||||
|
try {
|
||||||
|
if (servers == null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
val removedSelectedServer =
|
||||||
|
if (!TextUtils.isEmpty(subid) && !append) {
|
||||||
|
MmkvManager.decodeServerConfig(
|
||||||
|
MmkvManager.getSelectServer().orEmpty()
|
||||||
|
)?.let {
|
||||||
|
if (it.subscriptionId == subid) {
|
||||||
|
return@let it
|
||||||
|
}
|
||||||
|
return@let null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
|
|
||||||
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
|
var count = 0
|
||||||
|
servers.lines()
|
||||||
|
.distinct()
|
||||||
|
.reversed()
|
||||||
|
.forEach {
|
||||||
|
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
|
||||||
|
if (resId == 0) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a custom configuration server.
|
||||||
|
*
|
||||||
|
* @param server The server string.
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
* @return The number of configurations parsed.
|
||||||
|
*/
|
||||||
|
private fun parseCustomConfigServer(server: String?, subid: String): Int {
|
||||||
|
if (server == null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (server.contains("inbounds")
|
||||||
|
&& server.contains("outbounds")
|
||||||
|
&& server.contains("routing")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val serverList: Array<Any> =
|
||||||
|
JsonUtil.fromJson(server, Array<Any>::class.java)
|
||||||
|
|
||||||
|
if (serverList.isNotEmpty()) {
|
||||||
|
var count = 0
|
||||||
|
for (srv in serverList.reversed()) {
|
||||||
|
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||||
|
config.subscriptionId = subid
|
||||||
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
|
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For compatibility
|
||||||
|
val config = CustomFmt.parse(server) ?: return 0
|
||||||
|
config.subscriptionId = subid
|
||||||
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
|
return 1
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
||||||
|
try {
|
||||||
|
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
||||||
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
|
return 1
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the configuration from a QR code or string.
|
||||||
|
*
|
||||||
|
* @param str The configuration string.
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
* @param subItem The subscription item.
|
||||||
|
* @param removedSelectedServer The removed selected server.
|
||||||
|
* @return The result code.
|
||||||
*/
|
*/
|
||||||
private fun parseConfig(
|
private fun parseConfig(
|
||||||
str: String?,
|
str: String?,
|
||||||
|
@ -72,256 +374,17 @@ object AngConfigManager {
|
||||||
MmkvManager.setSelectServer(guid)
|
MmkvManager.setSelectServer(guid)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to parse config", e)
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* share config
|
* Updates the configuration via all subscriptions.
|
||||||
|
*
|
||||||
|
* @return The number of configurations updated.
|
||||||
*/
|
*/
|
||||||
private fun shareConfig(guid: String): String {
|
|
||||||
try {
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
|
|
||||||
|
|
||||||
return config.configType.protocolScheme + when (config.configType) {
|
|
||||||
EConfigType.VMESS -> VmessFmt.toUri(config)
|
|
||||||
EConfigType.CUSTOM -> ""
|
|
||||||
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
|
|
||||||
EConfigType.SOCKS -> SocksFmt.toUri(config)
|
|
||||||
EConfigType.HTTP -> ""
|
|
||||||
EConfigType.VLESS -> VlessFmt.toUri(config)
|
|
||||||
EConfigType.TROJAN -> TrojanFmt.toUri(config)
|
|
||||||
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
|
||||||
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2Clipboard
|
|
||||||
*/
|
|
||||||
fun share2Clipboard(context: Context, guid: String): Int {
|
|
||||||
try {
|
|
||||||
val conf = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(conf)) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.setClipboard(context, conf)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2Clipboard
|
|
||||||
*/
|
|
||||||
fun shareNonCustomConfigsToClipboard(context: Context, serverList: List<String>): Int {
|
|
||||||
try {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (guid in serverList) {
|
|
||||||
val url = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(url)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sb.append(url)
|
|
||||||
sb.appendLine()
|
|
||||||
}
|
|
||||||
if (sb.count() > 0) {
|
|
||||||
Utils.setClipboard(context, sb.toString())
|
|
||||||
}
|
|
||||||
return sb.lines().count()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share2QRCode
|
|
||||||
*/
|
|
||||||
fun share2QRCode(guid: String): Bitmap? {
|
|
||||||
try {
|
|
||||||
val conf = shareConfig(guid)
|
|
||||||
if (TextUtils.isEmpty(conf)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return QRCodeDecoder.createQRCode(conf)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shareFullContent2Clipboard
|
|
||||||
*/
|
|
||||||
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
|
|
||||||
try {
|
|
||||||
if (guid == null) return -1
|
|
||||||
val result = V2rayConfigManager.getV2rayConfig(context, guid)
|
|
||||||
if (result.status) {
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid)
|
|
||||||
if (config?.configType == EConfigType.HYSTERIA2) {
|
|
||||||
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
|
|
||||||
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort)
|
|
||||||
Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
Utils.setClipboard(context, result.content)
|
|
||||||
} else {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
|
|
||||||
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseBatchConfig(server, subid, append)
|
|
||||||
}
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseCustomConfigServer(server, subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
var countSub = parseBatchSubscription(server)
|
|
||||||
if (countSub <= 0) {
|
|
||||||
countSub = parseBatchSubscription(Utils.decode(server))
|
|
||||||
}
|
|
||||||
if (countSub > 0) {
|
|
||||||
updateConfigViaSubAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
return count to countSub
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseBatchSubscription(servers: String?): Int {
|
|
||||||
try {
|
|
||||||
if (servers == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = 0
|
|
||||||
servers.lines()
|
|
||||||
.distinct()
|
|
||||||
.forEach { str ->
|
|
||||||
if (Utils.isValidSubUrl(str)) {
|
|
||||||
count += importUrlAsSubscription(str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
|
|
||||||
try {
|
|
||||||
if (servers == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
val removedSelectedServer =
|
|
||||||
if (!TextUtils.isEmpty(subid) && !append) {
|
|
||||||
MmkvManager.decodeServerConfig(
|
|
||||||
MmkvManager.getSelectServer().orEmpty()
|
|
||||||
)?.let {
|
|
||||||
if (it.subscriptionId == subid) {
|
|
||||||
return@let it
|
|
||||||
}
|
|
||||||
return@let null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
|
||||||
var count = 0
|
|
||||||
servers.lines()
|
|
||||||
.distinct()
|
|
||||||
.reversed()
|
|
||||||
.forEach {
|
|
||||||
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
|
|
||||||
if (resId == 0) {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseCustomConfigServer(server: String?, subid: String): Int {
|
|
||||||
if (server == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (server.contains("inbounds")
|
|
||||||
&& server.contains("outbounds")
|
|
||||||
&& server.contains("routing")
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
val serverList: Array<Any> =
|
|
||||||
JsonUtil.fromJson(server, Array<Any>::class.java)
|
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
|
||||||
var count = 0
|
|
||||||
for (srv in serverList.reversed()) {
|
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// For compatibility
|
|
||||||
val config = CustomFmt.parse(server) ?: return 0
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
|
||||||
try {
|
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
|
||||||
return 1
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConfigViaSubAll(): Int {
|
fun updateConfigViaSubAll(): Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
try {
|
try {
|
||||||
|
@ -329,12 +392,18 @@ object AngConfigManager {
|
||||||
count += updateConfigViaSub(it)
|
count += updateConfigViaSub(it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the configuration via a subscription.
|
||||||
|
*
|
||||||
|
* @param it The subscription item.
|
||||||
|
* @return The number of configurations updated.
|
||||||
|
*/
|
||||||
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
|
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
|
||||||
try {
|
try {
|
||||||
if (TextUtils.isEmpty(it.first)
|
if (TextUtils.isEmpty(it.first)
|
||||||
|
@ -346,25 +415,29 @@ object AngConfigManager {
|
||||||
if (!it.second.enabled) {
|
if (!it.second.enabled) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
val url = Utils.idnToASCII(it.second.url)
|
val url = HttpUtil.toIdnUrl(it.second.url)
|
||||||
if (!Utils.isValidUrl(url)) {
|
if (!Utils.isValidUrl(url)) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
Log.d(AppConfig.ANG_PACKAGE, url)
|
if (!it.second.allowInsecureUrl) {
|
||||||
|
if (!Utils.isValidSubUrl(url)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(AppConfig.TAG, url)
|
||||||
|
|
||||||
var configText = try {
|
var configText = try {
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
|
HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
|
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||||
//e.printStackTrace()
|
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
if (configText.isEmpty()) {
|
if (configText.isEmpty()) {
|
||||||
configText = try {
|
configText = try {
|
||||||
Utils.getUrlContentWithCustomUserAgent(url)
|
HttpUtil.getUrlContentWithUserAgent(url)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,11 +446,19 @@ object AngConfigManager {
|
||||||
}
|
}
|
||||||
return parseConfigViaSub(configText, it.first, false)
|
return parseConfigViaSub(configText, it.first, false)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the configuration via a subscription.
|
||||||
|
*
|
||||||
|
* @param server The server string.
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
* @param append Whether to append the configurations.
|
||||||
|
* @return The number of configurations parsed.
|
||||||
|
*/
|
||||||
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
|
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
|
||||||
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
|
@ -389,6 +470,12 @@ object AngConfigManager {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports a URL as a subscription.
|
||||||
|
*
|
||||||
|
* @param url The URL.
|
||||||
|
* @return The number of subscriptions imported.
|
||||||
|
*/
|
||||||
private fun importUrlAsSubscription(url: String): Int {
|
private fun importUrlAsSubscription(url: String): Int {
|
||||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
subscriptions.forEach {
|
subscriptions.forEach {
|
||||||
|
|
|
@ -3,25 +3,29 @@ package com.v2ray.ang.handler
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.tencent.mmkv.MMKV
|
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.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.NetworkType
|
import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.extension.removeWhiteSpace
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
object MigrateManager {
|
object MigrateManager {
|
||||||
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
||||||
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates server configurations to profile items.
|
||||||
|
*
|
||||||
|
* @return True if migration was successful, false otherwise.
|
||||||
|
*/
|
||||||
fun migrateServerConfig2Profile(): Boolean {
|
fun migrateServerConfig2Profile(): Boolean {
|
||||||
if (serverStorage.count().toInt() == 0) {
|
if (serverStorage.count().toInt() == 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val serverList = serverStorage.allKeys() ?: return false
|
val serverList = serverStorage.allKeys() ?: return false
|
||||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
|
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + serverList.count())
|
||||||
|
|
||||||
for (guid in serverList) {
|
for (guid in serverList) {
|
||||||
var configOld = decodeServerConfigOld(guid) ?: continue
|
var configOld = decodeServerConfigOld(guid) ?: continue
|
||||||
|
@ -38,12 +42,18 @@ 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.i(AppConfig.TAG, "migrateServerConfig2Profile-" + config.remarks)
|
||||||
}
|
}
|
||||||
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
|
Log.i(AppConfig.TAG, "migrateServerConfig2Profile-end")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates a server configuration to a profile item.
|
||||||
|
*
|
||||||
|
* @param configOld The old server configuration.
|
||||||
|
* @return The profile item.
|
||||||
|
*/
|
||||||
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
|
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
|
||||||
return when (configOld.getProxyOutbound()?.protocol) {
|
return when (configOld.getProxyOutbound()?.protocol) {
|
||||||
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
||||||
|
@ -62,6 +72,12 @@ object MigrateManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates a common server configuration to a profile item.
|
||||||
|
*
|
||||||
|
* @param configOld The old server configuration.
|
||||||
|
* @return The profile item.
|
||||||
|
*/
|
||||||
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
|
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(configOld.configType)
|
val config = ProfileItem.create(configOld.configType)
|
||||||
|
|
||||||
|
@ -92,7 +108,7 @@ object MigrateManager {
|
||||||
config.insecure = tlsSettings?.allowInsecure
|
config.insecure = tlsSettings?.allowInsecure
|
||||||
config.sni = tlsSettings?.serverName
|
config.sni = tlsSettings?.serverName
|
||||||
config.fingerPrint = tlsSettings?.fingerprint
|
config.fingerPrint = tlsSettings?.fingerprint
|
||||||
config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString()
|
config.alpn = tlsSettings?.alpn?.joinToString(",").removeWhiteSpace().toString()
|
||||||
|
|
||||||
config.publicKey = tlsSettings?.publicKey
|
config.publicKey = tlsSettings?.publicKey
|
||||||
config.shortId = tlsSettings?.shortId
|
config.shortId = tlsSettings?.shortId
|
||||||
|
@ -101,6 +117,12 @@ object MigrateManager {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates a SOCKS server configuration to a profile item.
|
||||||
|
*
|
||||||
|
* @param configOld The old server configuration.
|
||||||
|
* @return The profile item.
|
||||||
|
*/
|
||||||
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
|
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.SOCKS)
|
val config = ProfileItem.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
|
@ -114,6 +136,12 @@ object MigrateManager {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates an HTTP server configuration to a profile item.
|
||||||
|
*
|
||||||
|
* @param configOld The old server configuration.
|
||||||
|
* @return The profile item.
|
||||||
|
*/
|
||||||
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
|
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.HTTP)
|
val config = ProfileItem.create(EConfigType.HTTP)
|
||||||
|
|
||||||
|
@ -127,6 +155,12 @@ object MigrateManager {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates a WireGuard server configuration to a profile item.
|
||||||
|
*
|
||||||
|
* @param configOld The old server configuration.
|
||||||
|
* @return The profile item.
|
||||||
|
*/
|
||||||
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
|
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
|
@ -137,14 +171,20 @@ object MigrateManager {
|
||||||
|
|
||||||
outbound.settings?.let { wireguard ->
|
outbound.settings?.let { wireguard ->
|
||||||
config.secretKey = wireguard.secretKey
|
config.secretKey = wireguard.secretKey
|
||||||
config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString()
|
config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString()
|
||||||
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
|
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
|
||||||
config.mtu = wireguard.mtu
|
config.mtu = wireguard.mtu
|
||||||
config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString()
|
config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString()
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates a Hysteria2 server configuration to a profile item.
|
||||||
|
*
|
||||||
|
* @param configOld The old server configuration.
|
||||||
|
* @return The profile item.
|
||||||
|
*/
|
||||||
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
|
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
|
@ -158,7 +198,7 @@ object MigrateManager {
|
||||||
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
|
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
|
||||||
config.insecure = tlsSetting.allowInsecure
|
config.insecure = tlsSetting.allowInsecure
|
||||||
config.sni = tlsSetting.serverName
|
config.sni = tlsSetting.serverName
|
||||||
config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty()
|
config.alpn = tlsSetting.alpn?.joinToString(",").removeWhiteSpace().orEmpty()
|
||||||
|
|
||||||
}
|
}
|
||||||
config.obfsPassword = outbound.settings?.obfsPassword
|
config.obfsPassword = outbound.settings?.obfsPassword
|
||||||
|
@ -166,6 +206,12 @@ object MigrateManager {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates a custom server configuration to a profile item.
|
||||||
|
*
|
||||||
|
* @param configOld The old server configuration.
|
||||||
|
* @return The profile item.
|
||||||
|
*/
|
||||||
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
|
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
|
||||||
val config = ProfileItem.create(EConfigType.CUSTOM)
|
val config = ProfileItem.create(EConfigType.CUSTOM)
|
||||||
|
|
||||||
|
@ -177,7 +223,12 @@ object MigrateManager {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the old server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The old server configuration.
|
||||||
|
*/
|
||||||
private fun decodeServerConfigOld(guid: String): ServerConfig? {
|
private fun decodeServerConfigOld(guid: String): ServerConfig? {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.v2ray.ang.handler
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
|
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
|
||||||
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
|
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
|
||||||
|
@ -41,18 +40,38 @@ object MmkvManager {
|
||||||
|
|
||||||
//region Server
|
//region Server
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the selected server GUID.
|
||||||
|
*
|
||||||
|
* @return The selected server GUID.
|
||||||
|
*/
|
||||||
fun getSelectServer(): String? {
|
fun getSelectServer(): String? {
|
||||||
return mainStorage.decodeString(KEY_SELECTED_SERVER)
|
return mainStorage.decodeString(KEY_SELECTED_SERVER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected server GUID.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
*/
|
||||||
fun setSelectServer(guid: String) {
|
fun setSelectServer(guid: String) {
|
||||||
mainStorage.encode(KEY_SELECTED_SERVER, guid)
|
mainStorage.encode(KEY_SELECTED_SERVER, guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the server list.
|
||||||
|
*
|
||||||
|
* @param serverList The list of server GUIDs.
|
||||||
|
*/
|
||||||
fun encodeServerList(serverList: MutableList<String>) {
|
fun encodeServerList(serverList: MutableList<String>) {
|
||||||
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
|
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the server list.
|
||||||
|
*
|
||||||
|
* @return The list of server GUIDs.
|
||||||
|
*/
|
||||||
fun decodeServerList(): MutableList<String> {
|
fun decodeServerList(): MutableList<String> {
|
||||||
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
||||||
return if (json.isNullOrBlank()) {
|
return if (json.isNullOrBlank()) {
|
||||||
|
@ -62,7 +81,12 @@ object MmkvManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The server configuration.
|
||||||
|
*/
|
||||||
fun decodeServerConfig(guid: String): ProfileItem? {
|
fun decodeServerConfig(guid: String): ProfileItem? {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
|
@ -85,6 +109,13 @@ object MmkvManager {
|
||||||
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
|
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @param config The server configuration.
|
||||||
|
* @return The server GUID.
|
||||||
|
*/
|
||||||
fun encodeServerConfig(guid: String, config: ProfileItem): String {
|
fun encodeServerConfig(guid: String, config: ProfileItem): String {
|
||||||
val key = guid.ifBlank { Utils.getUuid() }
|
val key = guid.ifBlank { Utils.getUuid() }
|
||||||
profileFullStorage.encode(key, JsonUtil.toJson(config))
|
profileFullStorage.encode(key, JsonUtil.toJson(config))
|
||||||
|
@ -107,6 +138,11 @@ object MmkvManager {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
*/
|
||||||
fun removeServer(guid: String) {
|
fun removeServer(guid: String) {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return
|
return
|
||||||
|
@ -122,6 +158,11 @@ object MmkvManager {
|
||||||
serverAffStorage.remove(guid)
|
serverAffStorage.remove(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the server configurations via subscription ID.
|
||||||
|
*
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
*/
|
||||||
fun removeServerViaSubid(subid: String) {
|
fun removeServerViaSubid(subid: String) {
|
||||||
if (subid.isBlank()) {
|
if (subid.isBlank()) {
|
||||||
return
|
return
|
||||||
|
@ -135,6 +176,12 @@ object MmkvManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the server affiliation information.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The server affiliation information.
|
||||||
|
*/
|
||||||
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
|
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
|
@ -146,6 +193,12 @@ object MmkvManager {
|
||||||
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
|
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the server test delay in milliseconds.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @param testResult The test delay in milliseconds.
|
||||||
|
*/
|
||||||
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
|
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return
|
return
|
||||||
|
@ -155,6 +208,11 @@ object MmkvManager {
|
||||||
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
|
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all test delay results.
|
||||||
|
*
|
||||||
|
* @param keys The list of server GUIDs.
|
||||||
|
*/
|
||||||
fun clearAllTestDelayResults(keys: List<String>?) {
|
fun clearAllTestDelayResults(keys: List<String>?) {
|
||||||
keys?.forEach { key ->
|
keys?.forEach { key ->
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
|
@ -164,6 +222,11 @@ object MmkvManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all server configurations.
|
||||||
|
*
|
||||||
|
* @return The number of server configurations removed.
|
||||||
|
*/
|
||||||
fun removeAllServer(): Int {
|
fun removeAllServer(): Int {
|
||||||
val count = profileFullStorage.allKeys()?.count() ?: 0
|
val count = profileFullStorage.allKeys()?.count() ?: 0
|
||||||
mainStorage.clearAll()
|
mainStorage.clearAll()
|
||||||
|
@ -173,6 +236,12 @@ object MmkvManager {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes invalid server configurations.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The number of server configurations removed.
|
||||||
|
*/
|
||||||
fun removeInvalidServer(guid: String): Int {
|
fun removeInvalidServer(guid: String): Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
if (guid.isNotEmpty()) {
|
if (guid.isNotEmpty()) {
|
||||||
|
@ -195,10 +264,22 @@ object MmkvManager {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the raw server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @param config The raw server configuration.
|
||||||
|
*/
|
||||||
fun encodeServerRaw(guid: String, config: String) {
|
fun encodeServerRaw(guid: String, config: String) {
|
||||||
serverRawStorage.encode(guid, config)
|
serverRawStorage.encode(guid, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the raw server configuration.
|
||||||
|
*
|
||||||
|
* @param guid The server GUID.
|
||||||
|
* @return The raw server configuration.
|
||||||
|
*/
|
||||||
fun decodeServerRaw(guid: String): String? {
|
fun decodeServerRaw(guid: String): String? {
|
||||||
return serverRawStorage.decodeString(guid)
|
return serverRawStorage.decodeString(guid)
|
||||||
}
|
}
|
||||||
|
@ -207,6 +288,9 @@ object MmkvManager {
|
||||||
|
|
||||||
//region Subscriptions
|
//region Subscriptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the subscription list.
|
||||||
|
*/
|
||||||
private fun initSubsList() {
|
private fun initSubsList() {
|
||||||
val subsList = decodeSubsList()
|
val subsList = decodeSubsList()
|
||||||
if (subsList.isNotEmpty()) {
|
if (subsList.isNotEmpty()) {
|
||||||
|
@ -218,6 +302,11 @@ object MmkvManager {
|
||||||
encodeSubsList(subsList)
|
encodeSubsList(subsList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the subscriptions.
|
||||||
|
*
|
||||||
|
* @return The list of subscriptions.
|
||||||
|
*/
|
||||||
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
|
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
|
||||||
initSubsList()
|
initSubsList()
|
||||||
|
|
||||||
|
@ -231,6 +320,11 @@ object MmkvManager {
|
||||||
return subscriptions
|
return subscriptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the subscription.
|
||||||
|
*
|
||||||
|
* @param subid The subscription ID.
|
||||||
|
*/
|
||||||
fun removeSubscription(subid: String) {
|
fun removeSubscription(subid: String) {
|
||||||
subStorage.remove(subid)
|
subStorage.remove(subid)
|
||||||
val subsList = decodeSubsList()
|
val subsList = decodeSubsList()
|
||||||
|
@ -240,6 +334,12 @@ object MmkvManager {
|
||||||
removeServerViaSubid(subid)
|
removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the subscription.
|
||||||
|
*
|
||||||
|
* @param guid The subscription GUID.
|
||||||
|
* @param subItem The subscription item.
|
||||||
|
*/
|
||||||
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
|
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
|
||||||
val key = guid.ifBlank { Utils.getUuid() }
|
val key = guid.ifBlank { Utils.getUuid() }
|
||||||
subStorage.encode(key, JsonUtil.toJson(subItem))
|
subStorage.encode(key, JsonUtil.toJson(subItem))
|
||||||
|
@ -251,15 +351,31 @@ object MmkvManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the subscription.
|
||||||
|
*
|
||||||
|
* @param subscriptionId The subscription ID.
|
||||||
|
* @return The subscription item.
|
||||||
|
*/
|
||||||
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
|
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
|
||||||
val json = subStorage.decodeString(subscriptionId) ?: return null
|
val json = subStorage.decodeString(subscriptionId) ?: return null
|
||||||
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
|
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the subscription list.
|
||||||
|
*
|
||||||
|
* @param subsList The list of subscription IDs.
|
||||||
|
*/
|
||||||
fun encodeSubsList(subsList: MutableList<String>) {
|
fun encodeSubsList(subsList: MutableList<String>) {
|
||||||
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
|
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the subscription list.
|
||||||
|
*
|
||||||
|
* @return The list of subscription IDs.
|
||||||
|
*/
|
||||||
fun decodeSubsList(): MutableList<String> {
|
fun decodeSubsList(): MutableList<String> {
|
||||||
val json = mainStorage.decodeString(KEY_SUB_IDS)
|
val json = mainStorage.decodeString(KEY_SUB_IDS)
|
||||||
return if (json.isNullOrBlank()) {
|
return if (json.isNullOrBlank()) {
|
||||||
|
@ -273,6 +389,11 @@ object MmkvManager {
|
||||||
|
|
||||||
//region Asset
|
//region Asset
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the asset URLs.
|
||||||
|
*
|
||||||
|
* @return The list of asset URLs.
|
||||||
|
*/
|
||||||
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
||||||
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||||
assetStorage.allKeys()?.forEach { key ->
|
assetStorage.allKeys()?.forEach { key ->
|
||||||
|
@ -284,15 +405,32 @@ object MmkvManager {
|
||||||
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the asset URL.
|
||||||
|
*
|
||||||
|
* @param assetid The asset ID.
|
||||||
|
*/
|
||||||
fun removeAssetUrl(assetid: String) {
|
fun removeAssetUrl(assetid: String) {
|
||||||
assetStorage.remove(assetid)
|
assetStorage.remove(assetid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the asset.
|
||||||
|
*
|
||||||
|
* @param assetid The asset ID.
|
||||||
|
* @param assetItem The asset item.
|
||||||
|
*/
|
||||||
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
|
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
|
||||||
val key = assetid.ifBlank { Utils.getUuid() }
|
val key = assetid.ifBlank { Utils.getUuid() }
|
||||||
assetStorage.encode(key, JsonUtil.toJson(assetItem))
|
assetStorage.encode(key, JsonUtil.toJson(assetItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the asset.
|
||||||
|
*
|
||||||
|
* @param assetid The asset ID.
|
||||||
|
* @return The asset item.
|
||||||
|
*/
|
||||||
fun decodeAsset(assetid: String): AssetUrlItem? {
|
fun decodeAsset(assetid: String): AssetUrlItem? {
|
||||||
val json = assetStorage.decodeString(assetid) ?: return null
|
val json = assetStorage.decodeString(assetid) ?: return null
|
||||||
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
|
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
|
||||||
|
@ -302,12 +440,22 @@ object MmkvManager {
|
||||||
|
|
||||||
//region Routing
|
//region Routing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the routing rulesets.
|
||||||
|
*
|
||||||
|
* @return The list of routing rulesets.
|
||||||
|
*/
|
||||||
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
||||||
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
||||||
if (ruleset.isNullOrEmpty()) return null
|
if (ruleset.isNullOrEmpty()) return null
|
||||||
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
|
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the routing rulesets.
|
||||||
|
*
|
||||||
|
* @param rulesetList The list of routing rulesets.
|
||||||
|
*/
|
||||||
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
|
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
|
||||||
if (rulesetList.isNullOrEmpty())
|
if (rulesetList.isNullOrEmpty())
|
||||||
encodeSettings(PREF_ROUTING_RULESET, "")
|
encodeSettings(PREF_ROUTING_RULESET, "")
|
||||||
|
@ -316,39 +464,99 @@ object MmkvManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the settings.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @param value The settings value.
|
||||||
|
* @return Whether the encoding was successful.
|
||||||
|
*/
|
||||||
fun encodeSettings(key: String, value: String?): Boolean {
|
fun encodeSettings(key: String, value: String?): Boolean {
|
||||||
return settingsStorage.encode(key, value)
|
return settingsStorage.encode(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the settings.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @param value The settings value.
|
||||||
|
* @return Whether the encoding was successful.
|
||||||
|
*/
|
||||||
fun encodeSettings(key: String, value: Int): Boolean {
|
fun encodeSettings(key: String, value: Int): Boolean {
|
||||||
return settingsStorage.encode(key, value)
|
return settingsStorage.encode(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the settings.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @param value The settings value.
|
||||||
|
* @return Whether the encoding was successful.
|
||||||
|
*/
|
||||||
fun encodeSettings(key: String, value: Boolean): Boolean {
|
fun encodeSettings(key: String, value: Boolean): Boolean {
|
||||||
return settingsStorage.encode(key, value)
|
return settingsStorage.encode(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the settings.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @param value The settings value.
|
||||||
|
* @return Whether the encoding was successful.
|
||||||
|
*/
|
||||||
fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
|
fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
|
||||||
return settingsStorage.encode(key, value)
|
return settingsStorage.encode(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the settings string.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @return The settings value.
|
||||||
|
*/
|
||||||
fun decodeSettingsString(key: String): String? {
|
fun decodeSettingsString(key: String): String? {
|
||||||
return settingsStorage.decodeString(key)
|
return settingsStorage.decodeString(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the settings string.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @param defaultValue The default value.
|
||||||
|
* @return The settings value.
|
||||||
|
*/
|
||||||
fun decodeSettingsString(key: String, defaultValue: String?): String? {
|
fun decodeSettingsString(key: String, defaultValue: String?): String? {
|
||||||
return settingsStorage.decodeString(key, defaultValue)
|
return settingsStorage.decodeString(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the settings boolean.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @return The settings value.
|
||||||
|
*/
|
||||||
fun decodeSettingsBool(key: String): Boolean {
|
fun decodeSettingsBool(key: String): Boolean {
|
||||||
return settingsStorage.decodeBool(key, false)
|
return settingsStorage.decodeBool(key, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the settings boolean.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @param defaultValue The default value.
|
||||||
|
* @return The settings value.
|
||||||
|
*/
|
||||||
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
|
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
|
||||||
return settingsStorage.decodeBool(key, defaultValue)
|
return settingsStorage.decodeBool(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the settings string set.
|
||||||
|
*
|
||||||
|
* @param key The settings key.
|
||||||
|
* @return The settings value.
|
||||||
|
*/
|
||||||
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
|
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
|
||||||
return settingsStorage.decodeStringSet(key)
|
return settingsStorage.decodeStringSet(key)
|
||||||
}
|
}
|
||||||
|
@ -357,10 +565,20 @@ object MmkvManager {
|
||||||
|
|
||||||
//region Others
|
//region Others
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the start on boot setting.
|
||||||
|
*
|
||||||
|
* @param startOnBoot Whether to start on boot.
|
||||||
|
*/
|
||||||
fun encodeStartOnBoot(startOnBoot: Boolean) {
|
fun encodeStartOnBoot(startOnBoot: Boolean) {
|
||||||
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
|
encodeSettings(PREF_IS_BOOTED, startOnBoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the start on boot setting.
|
||||||
|
*
|
||||||
|
* @return Whether to start on boot.
|
||||||
|
*/
|
||||||
fun decodeStartOnBoot(): Boolean {
|
fun decodeStartOnBoot(): Boolean {
|
||||||
return decodeSettingsBool(PREF_IS_BOOTED, false)
|
return decodeSettingsBool(PREF_IS_BOOTED, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,28 +4,34 @@ import android.content.Context
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.Language
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.RoutingType
|
import com.v2ray.ang.dto.RoutingType
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.dto.VpnInterfaceAddressConfig
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||||
import com.v2ray.ang.handler.MmkvManager.decodeServerList
|
import com.v2ray.ang.handler.MmkvManager.decodeServerList
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.Utils.parseInt
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import kotlin.Int
|
import java.util.Locale
|
||||||
|
|
||||||
object SettingsManager {
|
object SettingsManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize routing rulesets.
|
||||||
|
* @param context The application context.
|
||||||
|
*/
|
||||||
fun initRoutingRulesets(context: Context) {
|
fun initRoutingRulesets(context: Context) {
|
||||||
val exist = MmkvManager.decodeRoutingRulesets()
|
val exist = MmkvManager.decodeRoutingRulesets()
|
||||||
if (exist.isNullOrEmpty()) {
|
if (exist.isNullOrEmpty()) {
|
||||||
|
@ -34,6 +40,12 @@ object SettingsManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get preset routing rulesets.
|
||||||
|
* @param context The application context.
|
||||||
|
* @param index The index of the routing type.
|
||||||
|
* @return A mutable list of RulesetItem.
|
||||||
|
*/
|
||||||
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
|
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
|
||||||
val fileName = RoutingType.fromIndex(index).fileName
|
val fileName = RoutingType.fromIndex(index).fileName
|
||||||
val assets = Utils.readTextFromAssets(context, fileName)
|
val assets = Utils.readTextFromAssets(context, fileName)
|
||||||
|
@ -44,12 +56,21 @@ object SettingsManager {
|
||||||
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
|
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset routing rulesets from presets.
|
||||||
|
* @param context The application context.
|
||||||
|
* @param index The index of the routing type.
|
||||||
|
*/
|
||||||
fun resetRoutingRulesetsFromPresets(context: Context, index: Int) {
|
fun resetRoutingRulesetsFromPresets(context: Context, index: Int) {
|
||||||
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
|
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
|
||||||
resetRoutingRulesetsCommon(rulesetList)
|
resetRoutingRulesetsCommon(rulesetList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset routing rulesets.
|
||||||
|
* @param content The content of the rulesets.
|
||||||
|
* @return True if successful, false otherwise.
|
||||||
|
*/
|
||||||
fun resetRoutingRulesets(content: String?): Boolean {
|
fun resetRoutingRulesets(content: String?): Boolean {
|
||||||
if (content.isNullOrEmpty()) {
|
if (content.isNullOrEmpty()) {
|
||||||
return false
|
return false
|
||||||
|
@ -64,11 +85,15 @@ object SettingsManager {
|
||||||
resetRoutingRulesetsCommon(rulesetList)
|
resetRoutingRulesetsCommon(rulesetList)
|
||||||
return true
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common method to reset routing rulesets.
|
||||||
|
* @param rulesetList The list of rulesets.
|
||||||
|
*/
|
||||||
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
|
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
|
||||||
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
|
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
|
||||||
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
|
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
|
||||||
|
@ -81,6 +106,11 @@ object SettingsManager {
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetNew)
|
MmkvManager.encodeRoutingRulesets(rulesetNew)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a routing ruleset by index.
|
||||||
|
* @param index The index of the ruleset.
|
||||||
|
* @return The RulesetItem.
|
||||||
|
*/
|
||||||
fun getRoutingRuleset(index: Int): RulesetItem? {
|
fun getRoutingRuleset(index: Int): RulesetItem? {
|
||||||
if (index < 0) return null
|
if (index < 0) return null
|
||||||
|
|
||||||
|
@ -90,6 +120,11 @@ object SettingsManager {
|
||||||
return rulesetList[index]
|
return rulesetList[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a routing ruleset.
|
||||||
|
* @param index The index of the ruleset.
|
||||||
|
* @param ruleset The RulesetItem to save.
|
||||||
|
*/
|
||||||
fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
|
fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
|
||||||
if (ruleset == null) return
|
if (ruleset == null) return
|
||||||
|
|
||||||
|
@ -106,6 +141,10 @@ object SettingsManager {
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a routing ruleset by index.
|
||||||
|
* @param index The index of the ruleset.
|
||||||
|
*/
|
||||||
fun removeRoutingRuleset(index: Int) {
|
fun removeRoutingRuleset(index: Int) {
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
|
|
||||||
|
@ -116,21 +155,24 @@ object SettingsManager {
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if routing rulesets bypass LAN.
|
||||||
|
* @return True if bypassing LAN, false otherwise.
|
||||||
|
*/
|
||||||
fun routingRulesetsBypassLan(): Boolean {
|
fun routingRulesetsBypassLan(): Boolean {
|
||||||
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "0"
|
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1"
|
||||||
if (vpnBypassLan == "1") {
|
if (vpnBypassLan == "1") {
|
||||||
return true
|
return true
|
||||||
} else if (vpnBypassLan == "2") {
|
} else if (vpnBypassLan == "2") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
//Follow config
|
|
||||||
val guid = MmkvManager.getSelectServer() ?: return false
|
val guid = MmkvManager.getSelectServer() ?: return false
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return false
|
val config = decodeServerConfig(guid) ?: return false
|
||||||
if (config.configType == EConfigType.CUSTOM) {
|
if (config.configType == EConfigType.CUSTOM) {
|
||||||
val raw = MmkvManager.decodeServerRaw(guid) ?: return false
|
val raw = MmkvManager.decodeServerRaw(guid) ?: return false
|
||||||
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
|
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
|
||||||
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }?.any {
|
val exist = v2rayConfig.routing.rules.filter { 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
|
||||||
}
|
}
|
||||||
return exist == true
|
return exist == true
|
||||||
|
@ -143,6 +185,11 @@ object SettingsManager {
|
||||||
return exist == true
|
return exist == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swap routing rulesets.
|
||||||
|
* @param fromPosition The position to swap from.
|
||||||
|
* @param toPosition The position to swap to.
|
||||||
|
*/
|
||||||
fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
|
fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
if (rulesetList.isNullOrEmpty()) return
|
if (rulesetList.isNullOrEmpty()) return
|
||||||
|
@ -151,6 +198,11 @@ object SettingsManager {
|
||||||
MmkvManager.encodeRoutingRulesets(rulesetList)
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swap subscriptions.
|
||||||
|
* @param fromPosition The position to swap from.
|
||||||
|
* @param toPosition The position to swap to.
|
||||||
|
*/
|
||||||
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
|
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
|
||||||
val subsList = MmkvManager.decodeSubsList()
|
val subsList = MmkvManager.decodeSubsList()
|
||||||
if (subsList.isNullOrEmpty()) return
|
if (subsList.isNullOrEmpty()) return
|
||||||
|
@ -159,8 +211,13 @@ object SettingsManager {
|
||||||
MmkvManager.encodeSubsList(subsList)
|
MmkvManager.encodeSubsList(subsList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server via remarks.
|
||||||
|
* @param remarks The remarks of the server.
|
||||||
|
* @return The ProfileItem.
|
||||||
|
*/
|
||||||
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
||||||
if (remarks == null) {
|
if (remarks.isNullOrEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val serverList = decodeServerList()
|
val serverList = decodeServerList()
|
||||||
|
@ -173,14 +230,27 @@ object SettingsManager {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the SOCKS port.
|
||||||
|
* @return The SOCKS port.
|
||||||
|
*/
|
||||||
fun getSocksPort(): Int {
|
fun getSocksPort(): Int {
|
||||||
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the HTTP port.
|
||||||
|
* @return The HTTP port.
|
||||||
|
*/
|
||||||
fun getHttpPort(): Int {
|
fun getHttpPort(): Int {
|
||||||
return getSocksPort() + (if (Utils.isXray()) 0 else 1)
|
return getSocksPort() + if (Utils.isXray()) 0 else 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize assets.
|
||||||
|
* @param context The application context.
|
||||||
|
* @param assets The AssetManager.
|
||||||
|
*/
|
||||||
fun initAssets(context: Context, assets: AssetManager) {
|
fun initAssets(context: Context, assets: AssetManager) {
|
||||||
val extFolder = Utils.userAssetPath(context)
|
val extFolder = Utils.userAssetPath(context)
|
||||||
|
|
||||||
|
@ -196,14 +266,108 @@ object SettingsManager {
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(
|
Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
|
||||||
ANG_PACKAGE,
|
|
||||||
"Copied from apk assets folder to ${target.absolutePath}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get domestic DNS servers from preference.
|
||||||
|
* @return A list of domestic DNS servers.
|
||||||
|
*/
|
||||||
|
fun getDomesticDnsServers(): List<String> {
|
||||||
|
val domesticDns =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
|
||||||
|
val ret = domesticDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) }
|
||||||
|
if (ret.isEmpty()) {
|
||||||
|
return listOf(AppConfig.DNS_DIRECT)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remote DNS servers from preference.
|
||||||
|
* @return A list of remote DNS servers.
|
||||||
|
*/
|
||||||
|
fun getRemoteDnsServers(): List<String> {
|
||||||
|
val remoteDns =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
||||||
|
val ret = remoteDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) }
|
||||||
|
if (ret.isEmpty()) {
|
||||||
|
return listOf(AppConfig.DNS_PROXY)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get VPN DNS servers from preference.
|
||||||
|
* @return A list of VPN DNS servers.
|
||||||
|
*/
|
||||||
|
fun getVpnDnsServers(): List<String> {
|
||||||
|
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
|
||||||
|
return vpnDns.split(",").filter { Utils.isPureIpAddress(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get delay test URL.
|
||||||
|
* @param second Whether to use the second URL.
|
||||||
|
* @return The delay test URL.
|
||||||
|
*/
|
||||||
|
fun getDelayTestUrl(second: Boolean = false): String {
|
||||||
|
return if (second) {
|
||||||
|
AppConfig.DELAY_TEST_URL2
|
||||||
|
} else {
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
||||||
|
?: AppConfig.DELAY_TEST_URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the locale.
|
||||||
|
* @return The locale.
|
||||||
|
*/
|
||||||
|
fun getLocale(): Locale {
|
||||||
|
val langCode =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
|
||||||
|
val language = Language.fromCode(langCode)
|
||||||
|
|
||||||
|
return when (language) {
|
||||||
|
Language.AUTO -> Utils.getSysLocale()
|
||||||
|
Language.ENGLISH -> Locale.ENGLISH
|
||||||
|
Language.CHINA -> Locale.CHINA
|
||||||
|
Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
|
||||||
|
Language.VIETNAMESE -> Locale("vi")
|
||||||
|
Language.RUSSIAN -> Locale("ru")
|
||||||
|
Language.PERSIAN -> Locale("fa")
|
||||||
|
Language.ARABIC -> Locale("ar")
|
||||||
|
Language.BANGLA -> Locale("bn")
|
||||||
|
Language.BAKHTIARI -> Locale("bqi", "IR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set night mode.
|
||||||
|
*/
|
||||||
|
fun setNightMode() {
|
||||||
|
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
||||||
|
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
|
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||||
|
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the currently selected VPN interface address configuration.
|
||||||
|
* This method reads the user's preference for VPN interface addressing and returns
|
||||||
|
* the corresponding configuration containing IPv4 and IPv6 addresses.
|
||||||
|
*
|
||||||
|
* @return The selected VpnInterfaceAddressConfig instance, or the default configuration
|
||||||
|
* if no valid selection is found or if the stored index is invalid.
|
||||||
|
*/
|
||||||
|
fun getCurrentVpnInterfaceAddressConfig(): VpnInterfaceAddressConfig {
|
||||||
|
val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt()
|
||||||
|
return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,34 @@
|
||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.dto.IPAPIInfo
|
||||||
import com.v2ray.ang.extension.responseLength
|
import com.v2ray.ang.extension.responseLength
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.net.URL
|
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
object SpeedtestUtil {
|
object SpeedtestManager {
|
||||||
|
|
||||||
private val tcpTestingSockets = ArrayList<Socket?>()
|
private val tcpTestingSockets = ArrayList<Socket?>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the TCP connection time to a given URL and port.
|
||||||
|
*
|
||||||
|
* @param url The URL to connect to.
|
||||||
|
* @param port The port to connect to.
|
||||||
|
* @return The connection time in milliseconds, or -1 if the connection failed.
|
||||||
|
*/
|
||||||
suspend fun tcping(url: String, port: Int): Long {
|
suspend fun tcping(url: String, port: Int): Long {
|
||||||
var time = -1L
|
var time = -1L
|
||||||
for (k in 0 until 2) {
|
for (k in 0 until 2) {
|
||||||
|
@ -37,15 +43,27 @@ object SpeedtestUtil {
|
||||||
return time
|
return time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the real ping time using the V2Ray library.
|
||||||
|
*
|
||||||
|
* @param config The configuration string for the V2Ray library.
|
||||||
|
* @return The ping time in milliseconds, or -1 if the ping failed.
|
||||||
|
*/
|
||||||
fun realPing(config: String): Long {
|
fun realPing(config: String): Long {
|
||||||
return try {
|
return try {
|
||||||
Libv2ray.measureOutboundDelay(config, Utils.getDelayTestUrl())
|
Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
|
Log.e(AppConfig.TAG, "Failed to measure outbound delay", e)
|
||||||
-1L
|
-1L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the ping time to a given URL using the system ping command.
|
||||||
|
*
|
||||||
|
* @param url The URL to ping.
|
||||||
|
* @return The ping time in milliseconds as a string, or "-1ms" if the ping failed.
|
||||||
|
*/
|
||||||
fun ping(url: String): String {
|
fun ping(url: String): String {
|
||||||
try {
|
try {
|
||||||
val command = "/system/bin/ping -c 3 $url"
|
val command = "/system/bin/ping -c 3 $url"
|
||||||
|
@ -60,11 +78,18 @@ object SpeedtestUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to ping URL: $url", e)
|
||||||
}
|
}
|
||||||
return "-1ms"
|
return "-1ms"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the time taken to establish a TCP connection to a given URL and port.
|
||||||
|
*
|
||||||
|
* @param url The URL to connect to.
|
||||||
|
* @param port The port to connect to.
|
||||||
|
* @return The connection time in milliseconds, or -1 if the connection failed.
|
||||||
|
*/
|
||||||
fun socketConnectTime(url: String, port: Int): Long {
|
fun socketConnectTime(url: String, port: Int): Long {
|
||||||
try {
|
try {
|
||||||
val socket = Socket()
|
val socket = Socket()
|
||||||
|
@ -80,15 +105,18 @@ object SpeedtestUtil {
|
||||||
socket.close()
|
socket.close()
|
||||||
return time
|
return time
|
||||||
} catch (e: UnknownHostException) {
|
} catch (e: UnknownHostException) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Unknown host: $url", e)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e")
|
Log.e(AppConfig.TAG, "socketConnectTime IOException: $e")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all TCP sockets that are currently being tested.
|
||||||
|
*/
|
||||||
fun closeAllTcpSockets() {
|
fun closeAllTcpSockets() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
tcpTestingSockets.forEach {
|
tcpTestingSockets.forEach {
|
||||||
|
@ -98,26 +126,19 @@ object SpeedtestUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the connection to a given URL and port.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the test is running.
|
||||||
|
* @param port The port to connect to.
|
||||||
|
* @return A pair containing the elapsed time in milliseconds and the result message.
|
||||||
|
*/
|
||||||
fun testConnection(context: Context, port: Int): Pair<Long, String> {
|
fun testConnection(context: Context, port: Int): Pair<Long, String> {
|
||||||
var result: String
|
var result: String
|
||||||
var elapsed = -1L
|
var elapsed = -1L
|
||||||
var conn: HttpURLConnection? = null
|
|
||||||
|
|
||||||
|
val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
|
||||||
try {
|
try {
|
||||||
val url = URL(Utils.getDelayTestUrl())
|
|
||||||
|
|
||||||
conn = url.openConnection(
|
|
||||||
Proxy(
|
|
||||||
Proxy.Type.HTTP,
|
|
||||||
InetSocketAddress(LOOPBACK, port)
|
|
||||||
)
|
|
||||||
) as HttpURLConnection
|
|
||||||
conn.connectTimeout = 30000
|
|
||||||
conn.readTimeout = 30000
|
|
||||||
conn.setRequestProperty("Connection", "close")
|
|
||||||
conn.instanceFollowRedirects = false
|
|
||||||
conn.useCaches = false
|
|
||||||
|
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
val code = conn.responseCode
|
val code = conn.responseCode
|
||||||
elapsed = SystemClock.elapsedRealtime() - start
|
elapsed = SystemClock.elapsedRealtime() - start
|
||||||
|
@ -133,20 +154,34 @@ object SpeedtestUtil {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// network exception
|
Log.e(AppConfig.TAG, "Connection test IOException", e)
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
|
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
result = context.getString(R.string.connection_test_error, e.message)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// library exception, eg sumsung
|
Log.e(AppConfig.TAG, "Connection test Exception", e)
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e))
|
|
||||||
result = context.getString(R.string.connection_test_error, e.message)
|
result = context.getString(R.string.connection_test_error, e.message)
|
||||||
} finally {
|
} finally {
|
||||||
conn?.disconnect()
|
conn.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pair(elapsed, result)
|
return Pair(elapsed, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRemoteIPInfo(): String? {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null
|
||||||
|
|
||||||
|
var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
|
||||||
|
var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query
|
||||||
|
var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode
|
||||||
|
|
||||||
|
return "(${country ?: "unknown"}) $ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the version of the V2Ray library.
|
||||||
|
*
|
||||||
|
* @return The version of the V2Ray library.
|
||||||
|
*/
|
||||||
fun getLibVersion(): String {
|
fun getLibVersion(): String {
|
||||||
return Libv2ray.checkVersionX()
|
return Libv2ray.checkVersionX()
|
||||||
}
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
|
import com.v2ray.ang.dto.CheckUpdateResult
|
||||||
|
import com.v2ray.ang.dto.GitHubRelease
|
||||||
|
import com.v2ray.ang.extension.concatUrl
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
object UpdateCheckerManager {
|
||||||
|
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
|
||||||
|
val url = if (includePreRelease) {
|
||||||
|
AppConfig.APP_API_URL
|
||||||
|
} else {
|
||||||
|
AppConfig.APP_API_URL.concatUrl("latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = HttpUtil.getUrlContent(url, 5000)
|
||||||
|
if (response.isNullOrEmpty()) {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response")
|
||||||
|
}
|
||||||
|
|
||||||
|
val latestRelease = if (includePreRelease) {
|
||||||
|
JsonUtil.fromJson(response, Array<GitHubRelease>::class.java)
|
||||||
|
.firstOrNull()
|
||||||
|
?: throw IllegalStateException("No pre-release found")
|
||||||
|
} else {
|
||||||
|
JsonUtil.fromJson(response, GitHubRelease::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
val latestVersion = latestRelease.tagName.removePrefix("v")
|
||||||
|
Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})")
|
||||||
|
|
||||||
|
return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) {
|
||||||
|
val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0])
|
||||||
|
CheckUpdateResult(
|
||||||
|
hasUpdate = true,
|
||||||
|
latestVersion = latestVersion,
|
||||||
|
releaseNotes = latestRelease.body,
|
||||||
|
downloadUrl = downloadUrl,
|
||||||
|
isPreRelease = latestRelease.prerelease
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CheckUpdateResult(hasUpdate = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
|
||||||
|
?: throw IllegalStateException("Failed to create connection")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val apkFile = File(context.cacheDir, "update.apk")
|
||||||
|
Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
|
||||||
|
|
||||||
|
FileOutputStream(apkFile).use { outputStream ->
|
||||||
|
connection.inputStream.use { inputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(AppConfig.TAG, "APK download completed")
|
||||||
|
return@withContext apkFile
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
|
||||||
|
return@withContext null
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
connection.disconnect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareVersions(version1: String, version2: String): Int {
|
||||||
|
val v1 = version1.split(".")
|
||||||
|
val v2 = version2.split(".")
|
||||||
|
|
||||||
|
for (i in 0 until maxOf(v1.size, v2.size)) {
|
||||||
|
val num1 = if (i < v1.size) v1[i].toInt() else 0
|
||||||
|
val num2 = if (i < v2.size) v2[i].toInt() else 0
|
||||||
|
if (num1 != num2) return num1 - num2
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDownloadUrl(release: GitHubRelease, abi: String): String {
|
||||||
|
return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl
|
||||||
|
?: release.assets.firstOrNull()?.browserDownloadUrl
|
||||||
|
?: throw IllegalStateException("No compatible APK found")
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,68 @@
|
||||||
|
package com.v2ray.ang.helper
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class CustomDividerItemDecoration(
|
||||||
|
private val divider: Drawable,
|
||||||
|
private val orientation: Int
|
||||||
|
) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
if (orientation == RecyclerView.VERTICAL) {
|
||||||
|
drawVerticalDividers(canvas, parent)
|
||||||
|
} else {
|
||||||
|
drawHorizontalDividers(canvas, parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawVerticalDividers(canvas: Canvas, parent: RecyclerView) {
|
||||||
|
val left = parent.paddingLeft
|
||||||
|
val right = parent.width - parent.paddingRight
|
||||||
|
|
||||||
|
val childCount = parent.childCount
|
||||||
|
for (i in 0 until childCount - 1) {
|
||||||
|
val child = parent.getChildAt(i)
|
||||||
|
val params = child.layoutParams as RecyclerView.LayoutParams
|
||||||
|
|
||||||
|
val top = child.bottom + params.bottomMargin
|
||||||
|
val bottom = top + divider.intrinsicHeight
|
||||||
|
|
||||||
|
divider.setBounds(left, top, right, bottom)
|
||||||
|
divider.draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawHorizontalDividers(canvas: Canvas, parent: RecyclerView) {
|
||||||
|
val top = parent.paddingTop
|
||||||
|
val bottom = parent.height - parent.paddingBottom
|
||||||
|
|
||||||
|
val childCount = parent.childCount
|
||||||
|
for (i in 0 until childCount - 1) {
|
||||||
|
val child = parent.getChildAt(i)
|
||||||
|
val params = child.layoutParams as RecyclerView.LayoutParams
|
||||||
|
|
||||||
|
val left = child.right + params.rightMargin
|
||||||
|
val right = left + divider.intrinsicWidth
|
||||||
|
|
||||||
|
divider.setBounds(left, top, right, bottom)
|
||||||
|
divider.draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
if (orientation == RecyclerView.VERTICAL) {
|
||||||
|
outRect.set(0, 0, 0, divider.intrinsicHeight)
|
||||||
|
} else {
|
||||||
|
outRect.set(0, 0, divider.intrinsicWidth, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,6 @@
|
||||||
package com.v2ray.ang.helper
|
package com.v2ray.ang.helper
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.animation.ValueAnimator.AnimatorUpdateListener
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
@ -108,7 +107,7 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
|
||||||
addUpdateListener { animation ->
|
addUpdateListener { animation ->
|
||||||
val value = animation.animatedValue as Float
|
val value = animation.animatedValue as Float
|
||||||
viewHolder.itemView.translationX = value
|
viewHolder.itemView.translationX = value
|
||||||
viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD))
|
viewHolder.itemView.alpha = 1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD)
|
||||||
}
|
}
|
||||||
interpolator = DecelerateInterpolator()
|
interpolator = DecelerateInterpolator()
|
||||||
duration = ANIMATION_DURATION
|
duration = ANIMATION_DURATION
|
||||||
|
@ -145,4 +144,4 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
|
||||||
private const val SWIPE_THRESHOLD = 0.25f
|
private const val SWIPE_THRESHOLD = 0.25f
|
||||||
private const val ANIMATION_DURATION: Long = 200
|
private const val ANIMATION_DURATION: Long = 200
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,10 +32,10 @@ import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
import com.v2ray.ang.extension.listenForPackageChanges
|
import com.v2ray.ang.extension.listenForPackageChanges
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
|
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
@ -126,7 +126,7 @@ object PluginManager {
|
||||||
if (providers.size > 1) {
|
if (providers.size > 1) {
|
||||||
val message =
|
val message =
|
||||||
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
|
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
|
||||||
Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show()
|
AngApplication.application.toast(message)
|
||||||
throw IllegalStateException(message)
|
throw IllegalStateException(message)
|
||||||
}
|
}
|
||||||
val provider = providers.single().providerInfo
|
val provider = providers.single().providerInfo
|
||||||
|
@ -224,8 +224,8 @@ object PluginManager {
|
||||||
|
|
||||||
fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
|
fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
|
||||||
is String -> value
|
is String -> value
|
||||||
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
|
// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
|
||||||
.getString(value)
|
// .getString(value)
|
||||||
|
|
||||||
null -> null
|
null -> null
|
||||||
else -> error("meta-data $key has invalid type ${value.javaClass}")
|
else -> error("meta-data $key has invalid type ${value.javaClass}")
|
||||||
|
|
|
@ -7,12 +7,17 @@ import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
/**
|
||||||
|
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
||||||
|
* It checks if the context is not null and the action is ACTION_BOOT_COMPLETED.
|
||||||
|
* If the conditions are met, it starts the V2Ray service.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param intent The Intent being received.
|
||||||
|
*/
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
//Check if context is not null and action is the one we want
|
|
||||||
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||||
//Check if flag is true and a server is selected
|
|
||||||
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
||||||
//Start v2ray
|
V2RayServiceManager.startVService(context)
|
||||||
V2RayServiceManager.startV2Ray(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,19 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class TaskerReceiver : BroadcastReceiver() {
|
class TaskerReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
||||||
|
* It retrieves the bundle from the intent and checks the switch and guid values.
|
||||||
|
* Depending on the switch value, it starts or stops the V2Ray service.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param intent The Intent being received.
|
||||||
|
*/
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
|
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
|
||||||
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
|
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
|
||||||
|
@ -22,16 +27,15 @@ class TaskerReceiver : BroadcastReceiver() {
|
||||||
return
|
return
|
||||||
} else if (switch) {
|
} else if (switch) {
|
||||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||||
Utils.startVServiceFromToggle(context)
|
V2RayServiceManager.startVServiceFromToggle(context)
|
||||||
} else {
|
} else {
|
||||||
MmkvManager.setSelectServer(guid)
|
V2RayServiceManager.startVService(context, guid)
|
||||||
V2RayServiceManager.startV2Ray(context)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Utils.stopVService(context)
|
V2RayServiceManager.stopVService(context)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,18 +11,29 @@ import android.widget.RemoteViews
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class WidgetProvider : AppWidgetProvider() {
|
class WidgetProvider : AppWidgetProvider() {
|
||||||
/**
|
/**
|
||||||
* 每次窗口小部件被更新都调用一次该方法
|
* This method is called every time the widget is updated.
|
||||||
|
* It updates the widget background based on the V2Ray service running state.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param appWidgetManager The AppWidgetManager instance.
|
||||||
|
* @param appWidgetIds The appWidgetIds for which an update is needed.
|
||||||
*/
|
*/
|
||||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning)
|
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.isRunning())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the widget background based on whether the V2Ray service is running.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param appWidgetManager The AppWidgetManager instance.
|
||||||
|
* @param appWidgetIds The appWidgetIds for which an update is needed.
|
||||||
|
* @param isRunning Boolean indicating if the V2Ray service is running.
|
||||||
|
*/
|
||||||
private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
|
private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
|
||||||
val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
|
val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
|
||||||
val intent = Intent(context, WidgetProvider::class.java)
|
val intent = Intent(context, WidgetProvider::class.java)
|
||||||
|
@ -52,15 +63,19 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接收窗口小部件发送的广播
|
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
|
||||||
|
* It handles widget click actions and updates the widget background based on the V2Ray service state.
|
||||||
|
*
|
||||||
|
* @param context The Context in which the receiver is running.
|
||||||
|
* @param intent The Intent being received.
|
||||||
*/
|
*/
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
super.onReceive(context, intent)
|
super.onReceive(context, intent)
|
||||||
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
|
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
|
||||||
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
if (V2RayServiceManager.isRunning()) {
|
||||||
Utils.stopVService(context)
|
V2RayServiceManager.stopVService(context)
|
||||||
} else {
|
} else {
|
||||||
Utils.startVServiceFromToggle(context)
|
V2RayServiceManager.startVServiceFromToggle(context)
|
||||||
}
|
}
|
||||||
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
|
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
|
||||||
AppWidgetManager.getInstance(context)?.let { manager ->
|
AppWidgetManager.getInstance(context)?.let { manager ->
|
||||||
|
|
|
@ -0,0 +1,252 @@
|
||||||
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.extension.toSpeedString
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.ui.MainActivity
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
object NotificationService {
|
||||||
|
private const val NOTIFICATION_ID = 1
|
||||||
|
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
||||||
|
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
||||||
|
private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
|
||||||
|
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
||||||
|
|
||||||
|
private var lastQueryTime = 0L
|
||||||
|
private var mBuilder: NotificationCompat.Builder? = null
|
||||||
|
private var speedNotificationJob: Job? = null
|
||||||
|
private var mNotificationManager: NotificationManager? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the speed notification.
|
||||||
|
* @param currentConfig The current profile configuration.
|
||||||
|
*/
|
||||||
|
fun startSpeedNotification(currentConfig: ProfileItem?) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
|
||||||
|
if (speedNotificationJob != null || V2RayServiceManager.isRunning() == false) return
|
||||||
|
|
||||||
|
lastQueryTime = System.currentTimeMillis()
|
||||||
|
var lastZeroSpeed = false
|
||||||
|
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||||
|
outboundTags?.remove(TAG_DIRECT)
|
||||||
|
|
||||||
|
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
while (isActive) {
|
||||||
|
val queryTime = System.currentTimeMillis()
|
||||||
|
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
||||||
|
var proxyTotal = 0L
|
||||||
|
val text = StringBuilder()
|
||||||
|
outboundTags?.forEach {
|
||||||
|
val up = V2RayServiceManager.queryStats(it, AppConfig.UPLINK)
|
||||||
|
val down = V2RayServiceManager.queryStats(it, AppConfig.DOWNLINK)
|
||||||
|
if (up + down > 0) {
|
||||||
|
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
||||||
|
proxyTotal += up + down
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val directUplink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
||||||
|
val directDownlink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
|
||||||
|
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
||||||
|
if (!zeroSpeed || !lastZeroSpeed) {
|
||||||
|
if (proxyTotal == 0L) {
|
||||||
|
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
||||||
|
}
|
||||||
|
appendSpeedString(
|
||||||
|
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||||
|
directDownlink / sinceLastQueryInSeconds
|
||||||
|
)
|
||||||
|
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
||||||
|
}
|
||||||
|
lastZeroSpeed = zeroSpeed
|
||||||
|
lastQueryTime = queryTime
|
||||||
|
delay(3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the notification.
|
||||||
|
* @param currentConfig The current profile configuration.
|
||||||
|
*/
|
||||||
|
fun showNotification(currentConfig: ProfileItem?) {
|
||||||
|
val service = getService() ?: return
|
||||||
|
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
}
|
||||||
|
|
||||||
|
val startMainIntent = Intent(service, MainActivity::class.java)
|
||||||
|
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
|
||||||
|
|
||||||
|
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||||
|
stopV2RayIntent.`package` = ANG_PACKAGE
|
||||||
|
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
||||||
|
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
|
||||||
|
|
||||||
|
val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||||
|
restartV2RayIntent.`package` = ANG_PACKAGE
|
||||||
|
restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
|
||||||
|
val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
|
||||||
|
|
||||||
|
val channelId =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
createNotificationChannel()
|
||||||
|
} else {
|
||||||
|
// If earlier version channel ID is not used
|
||||||
|
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
mBuilder = NotificationCompat.Builder(service, channelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_name)
|
||||||
|
.setContentTitle(currentConfig?.remarks)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setContentIntent(contentPendingIntent)
|
||||||
|
.addAction(
|
||||||
|
R.drawable.ic_delete_24dp,
|
||||||
|
service.getString(R.string.notification_action_stop_v2ray),
|
||||||
|
stopV2RayPendingIntent
|
||||||
|
)
|
||||||
|
.addAction(
|
||||||
|
R.drawable.ic_delete_24dp,
|
||||||
|
service.getString(R.string.title_service_restart),
|
||||||
|
restartV2RayPendingIntent
|
||||||
|
)
|
||||||
|
|
||||||
|
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
|
||||||
|
|
||||||
|
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the notification.
|
||||||
|
*/
|
||||||
|
fun cancelNotification() {
|
||||||
|
val service = getService() ?: return
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
|
} else {
|
||||||
|
service.stopForeground(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
mBuilder = null
|
||||||
|
speedNotificationJob?.cancel()
|
||||||
|
speedNotificationJob = null
|
||||||
|
mNotificationManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the speed notification.
|
||||||
|
* @param currentConfig The current profile configuration.
|
||||||
|
*/
|
||||||
|
fun stopSpeedNotification(currentConfig: ProfileItem?) {
|
||||||
|
speedNotificationJob?.let {
|
||||||
|
it.cancel()
|
||||||
|
speedNotificationJob = null
|
||||||
|
updateNotification(currentConfig?.remarks, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a notification channel for Android O and above.
|
||||||
|
* @return The channel ID.
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun createNotificationChannel(): String {
|
||||||
|
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
||||||
|
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
||||||
|
val chan = NotificationChannel(
|
||||||
|
channelId,
|
||||||
|
channelName, NotificationManager.IMPORTANCE_HIGH
|
||||||
|
)
|
||||||
|
chan.lightColor = Color.DKGRAY
|
||||||
|
chan.importance = NotificationManager.IMPORTANCE_NONE
|
||||||
|
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||||
|
getNotificationManager()?.createNotificationChannel(chan)
|
||||||
|
return channelId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the notification with the given content text and traffic data.
|
||||||
|
* @param contentText The content text.
|
||||||
|
* @param proxyTraffic The proxy traffic.
|
||||||
|
* @param directTraffic The direct traffic.
|
||||||
|
*/
|
||||||
|
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
||||||
|
if (mBuilder != null) {
|
||||||
|
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
|
||||||
|
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
|
||||||
|
} else if (proxyTraffic > directTraffic) {
|
||||||
|
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
|
||||||
|
} else {
|
||||||
|
mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
|
||||||
|
}
|
||||||
|
mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
|
||||||
|
mBuilder?.setContentText(contentText)
|
||||||
|
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the notification manager.
|
||||||
|
* @return The notification manager.
|
||||||
|
*/
|
||||||
|
private fun getNotificationManager(): NotificationManager? {
|
||||||
|
if (mNotificationManager == null) {
|
||||||
|
val service = getService() ?: return null
|
||||||
|
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
}
|
||||||
|
return mNotificationManager
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends the speed string to the given text.
|
||||||
|
* @param text The text to append to.
|
||||||
|
* @param name The name of the tag.
|
||||||
|
* @param up The uplink speed.
|
||||||
|
* @param down The downlink speed.
|
||||||
|
*/
|
||||||
|
private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
|
||||||
|
var n = name ?: "no tag"
|
||||||
|
n = n.substring(0, min(n.length, 6))
|
||||||
|
text.append(n)
|
||||||
|
for (i in n.length..6 step 2) {
|
||||||
|
text.append("\t")
|
||||||
|
}
|
||||||
|
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the service instance.
|
||||||
|
* @return The service instance.
|
||||||
|
*/
|
||||||
|
private fun getService(): Service? {
|
||||||
|
return V2RayServiceManager.serviceControl?.get()?.getService()
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ package com.v2ray.ang.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -10,8 +10,13 @@ import kotlinx.coroutines.launch
|
||||||
class ProcessService {
|
class ProcessService {
|
||||||
private var process: Process? = null
|
private var process: Process? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a process with the given command.
|
||||||
|
* @param context The context.
|
||||||
|
* @param cmd The command to run.
|
||||||
|
*/
|
||||||
fun runProcess(context: Context, cmd: MutableList<String>) {
|
fun runProcess(context: Context, cmd: MutableList<String>) {
|
||||||
Log.d(ANG_PACKAGE, cmd.toString())
|
Log.i(AppConfig.TAG, cmd.toString())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val proBuilder = ProcessBuilder(cmd)
|
val proBuilder = ProcessBuilder(cmd)
|
||||||
|
@ -22,23 +27,26 @@ class ProcessService {
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
Thread.sleep(50L)
|
Thread.sleep(50L)
|
||||||
Log.d(ANG_PACKAGE, "runProcess check")
|
Log.i(AppConfig.TAG, "runProcess check")
|
||||||
process?.waitFor()
|
process?.waitFor()
|
||||||
Log.d(ANG_PACKAGE, "runProcess exited")
|
Log.i(AppConfig.TAG, "runProcess exited")
|
||||||
}
|
}
|
||||||
Log.d(ANG_PACKAGE, process.toString())
|
Log.i(AppConfig.TAG, process.toString())
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.e(AppConfig.TAG, e.toString(), e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the running process.
|
||||||
|
*/
|
||||||
fun stopProcess() {
|
fun stopProcess() {
|
||||||
try {
|
try {
|
||||||
Log.d(ANG_PACKAGE, "runProcess destroy")
|
Log.i(AppConfig.TAG, "runProcess destroy")
|
||||||
process?.destroy()
|
process?.destroy()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.e(AppConfig.TAG, "Failed to destroy process", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.v2ray.ang.service
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -9,6 +8,8 @@ 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 android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
@ -16,18 +17,21 @@ import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
class QSTileService : TileService() {
|
class QSTileService : TileService() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the state of the tile.
|
||||||
|
* @param state The state to set.
|
||||||
|
*/
|
||||||
fun setState(state: Int) {
|
fun setState(state: Int) {
|
||||||
|
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
||||||
if (state == Tile.STATE_INACTIVE) {
|
if (state == Tile.STATE_INACTIVE) {
|
||||||
qsTile?.state = Tile.STATE_INACTIVE
|
qsTile?.state = Tile.STATE_INACTIVE
|
||||||
qsTile?.label = getString(R.string.app_name)
|
qsTile?.label = getString(R.string.app_name)
|
||||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
|
||||||
} else if (state == Tile.STATE_ACTIVE) {
|
} else if (state == Tile.STATE_ACTIVE) {
|
||||||
qsTile?.state = Tile.STATE_ACTIVE
|
qsTile?.state = Tile.STATE_ACTIVE
|
||||||
qsTile?.label = V2RayServiceManager.currentConfig?.remarks
|
qsTile?.label = V2RayServiceManager.getRunningServerName()
|
||||||
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qsTile?.updateTile()
|
qsTile?.updateTile()
|
||||||
|
@ -37,17 +41,23 @@ class QSTileService : TileService() {
|
||||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
|
|
||||||
setState(Tile.STATE_INACTIVE)
|
if (V2RayServiceManager.isRunning()) {
|
||||||
|
setState(Tile.STATE_ACTIVE)
|
||||||
|
} else {
|
||||||
|
setState(Tile.STATE_INACTIVE)
|
||||||
|
}
|
||||||
mMsgReceive = ReceiveMessageHandler(this)
|
mMsgReceive = ReceiveMessageHandler(this)
|
||||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||||
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
|
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the tile stops listening.
|
||||||
|
*/
|
||||||
override fun onStopListening() {
|
override fun onStopListening() {
|
||||||
super.onStopListening()
|
super.onStopListening()
|
||||||
|
|
||||||
|
@ -55,20 +65,23 @@ class QSTileService : TileService() {
|
||||||
applicationContext.unregisterReceiver(mMsgReceive)
|
applicationContext.unregisterReceiver(mMsgReceive)
|
||||||
mMsgReceive = null
|
mMsgReceive = null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to unregister receiver", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the tile is clicked.
|
||||||
|
*/
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
when (qsTile.state) {
|
when (qsTile.state) {
|
||||||
Tile.STATE_INACTIVE -> {
|
Tile.STATE_INACTIVE -> {
|
||||||
Utils.startVServiceFromToggle(this)
|
V2RayServiceManager.startVServiceFromToggle(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
Tile.STATE_ACTIVE -> {
|
Tile.STATE_ACTIVE -> {
|
||||||
Utils.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,26 @@ package com.v2ray.ang.service
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
|
||||||
interface ServiceControl {
|
interface ServiceControl {
|
||||||
|
/**
|
||||||
|
* Gets the service instance.
|
||||||
|
* @return The service instance.
|
||||||
|
*/
|
||||||
fun getService(): Service
|
fun getService(): Service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the service.
|
||||||
|
*/
|
||||||
fun startService()
|
fun startService()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*/
|
||||||
fun stopService()
|
fun stopService()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protects the VPN socket.
|
||||||
|
* @param socket The socket to protect.
|
||||||
|
* @return True if the socket is protected, false otherwise.
|
||||||
|
*/
|
||||||
fun vpnProtect(socket: Int): Boolean
|
fun vpnProtect(socket: Int): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import com.v2ray.ang.handler.MmkvManager
|
||||||
|
|
||||||
object SubscriptionUpdater {
|
object SubscriptionUpdater {
|
||||||
|
|
||||||
|
|
||||||
class UpdateTask(context: Context, params: WorkerParameters) :
|
class UpdateTask(context: Context, params: WorkerParameters) :
|
||||||
CoroutineWorker(context, params) {
|
CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
@ -33,9 +32,13 @@ object SubscriptionUpdater {
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the subscription update work.
|
||||||
|
* @return The result of the work.
|
||||||
|
*/
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "subscription automatic update starting")
|
Log.i(AppConfig.TAG, "subscription automatic update starting")
|
||||||
|
|
||||||
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
|
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
|
||||||
|
|
||||||
|
@ -53,10 +56,7 @@ object SubscriptionUpdater {
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
notificationManager.notify(3, notification.build())
|
notificationManager.notify(3, notification.build())
|
||||||
Log.d(
|
Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
|
||||||
AppConfig.ANG_PACKAGE,
|
|
||||||
"subscription automatic update: ---${subItem.remarks}"
|
|
||||||
)
|
|
||||||
updateConfigViaSub(Pair(sub.first, subItem))
|
updateConfigViaSub(Pair(sub.first, subItem))
|
||||||
notification.setContentText("Updating ${subItem.remarks}")
|
notification.setContentText("Updating ${subItem.remarks}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,50 +6,87 @@ import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
|
|
||||||
class V2RayProxyOnlyService : Service(), ServiceControl {
|
class V2RayProxyOnlyService : Service(), ServiceControl {
|
||||||
|
/**
|
||||||
|
* Initializes the service.
|
||||||
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the start command for the service.
|
||||||
|
* @param intent The intent.
|
||||||
|
* @param flags The flags.
|
||||||
|
* @param startId The start ID.
|
||||||
|
* @return The start mode.
|
||||||
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
V2RayServiceManager.startV2rayPoint()
|
V2RayServiceManager.startCoreLoop()
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the service.
|
||||||
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
V2RayServiceManager.stopV2rayPoint()
|
V2RayServiceManager.stopCoreLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the service instance.
|
||||||
|
* @return The service instance.
|
||||||
|
*/
|
||||||
override fun getService(): Service {
|
override fun getService(): Service {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the service.
|
||||||
|
*/
|
||||||
override fun startService() {
|
override fun startService() {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*/
|
||||||
override fun stopService() {
|
override fun stopService() {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protects the VPN socket.
|
||||||
|
* @param socket The socket to protect.
|
||||||
|
* @return True if the socket is protected, false otherwise.
|
||||||
|
*/
|
||||||
override fun vpnProtect(socket: Int): Boolean {
|
override fun vpnProtect(socket: Int): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the service.
|
||||||
|
* @param intent The intent.
|
||||||
|
* @return The binder.
|
||||||
|
*/
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the base context to the service.
|
||||||
|
* @param newBase The new base context.
|
||||||
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val context = newBase?.let {
|
val context = newBase?.let {
|
||||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
|
||||||
}
|
}
|
||||||
super.attachBaseContext(context)
|
super.attachBaseContext(context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,77 +1,108 @@
|
||||||
package com.v2ray.ang.service
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
import android.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
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.toSpeedString
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.ui.MainActivity
|
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.PluginUtil
|
import com.v2ray.ang.util.PluginUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import libv2ray.CoreCallbackHandler
|
||||||
|
import libv2ray.CoreController
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import libv2ray.V2RayPoint
|
|
||||||
import libv2ray.V2RayVPNServiceSupportsSet
|
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
object V2RayServiceManager {
|
object V2RayServiceManager {
|
||||||
private const val NOTIFICATION_ID = 1
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
|
|
||||||
private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
|
|
||||||
private const val NOTIFICATION_ICON_THRESHOLD = 3000
|
|
||||||
|
|
||||||
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback())
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
private val mMsgReceive = ReceiveMessageHandler()
|
||||||
|
private var currentConfig: ProfileItem? = null
|
||||||
|
|
||||||
var serviceControl: SoftReference<ServiceControl>? = null
|
var serviceControl: SoftReference<ServiceControl>? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
||||||
}
|
}
|
||||||
var currentConfig: ProfileItem? = null
|
|
||||||
|
|
||||||
private var lastQueryTime = 0L
|
/**
|
||||||
private var mBuilder: NotificationCompat.Builder? = null
|
* Starts the V2Ray service from a toggle action.
|
||||||
private var mDisposable: Disposable? = null
|
* @param context The context from which the service is started.
|
||||||
private var mNotificationManager: NotificationManager? = null
|
* @return True if the service was started successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
fun startVServiceFromToggle(context: Context): Boolean {
|
||||||
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
|
context.toast(R.string.app_tile_first_use)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startContextService(context)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
fun startV2Ray(context: Context) {
|
/**
|
||||||
if (v2rayPoint.isRunning) return
|
* Starts the V2Ray service.
|
||||||
|
* @param context The context from which the service is started.
|
||||||
|
* @param guid The GUID of the server configuration to use (optional).
|
||||||
|
*/
|
||||||
|
fun startVService(context: Context, guid: String? = null) {
|
||||||
|
if (guid != null) {
|
||||||
|
MmkvManager.setSelectServer(guid)
|
||||||
|
}
|
||||||
|
startContextService(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the V2Ray service.
|
||||||
|
* @param context The context from which the service is stopped.
|
||||||
|
*/
|
||||||
|
fun stopVService(context: Context) {
|
||||||
|
context.toast(R.string.toast_services_stop)
|
||||||
|
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the V2Ray service is running.
|
||||||
|
* @return True if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning() = coreController.isRunning
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the currently running server.
|
||||||
|
* @return The name of the running server.
|
||||||
|
*/
|
||||||
|
fun getRunningServerName() = currentConfig?.remarks.orEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the context service for V2Ray.
|
||||||
|
* Chooses between VPN service or Proxy-only service based on user settings.
|
||||||
|
* @param context The context from which the service is started.
|
||||||
|
*/
|
||||||
|
private fun startContextService(context: Context) {
|
||||||
|
if (coreController.isRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val guid = MmkvManager.getSelectServer() ?: return
|
val guid = MmkvManager.getSelectServer() ?: return
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||||
if (config.configType != EConfigType.CUSTOM
|
if (config.configType != EConfigType.CUSTOM
|
||||||
&& !Utils.isValidUrl(config.server)
|
&& !Utils.isValidUrl(config.server)
|
||||||
&& !Utils.isIpAddress(config.server)
|
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
||||||
) return
|
) return
|
||||||
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||||
// if (!result.status) return
|
// if (!result.status) return
|
||||||
|
@ -81,7 +112,7 @@ object V2RayServiceManager {
|
||||||
} else {
|
} else {
|
||||||
context.toast(R.string.toast_services_start)
|
context.toast(R.string.toast_services_start)
|
||||||
}
|
}
|
||||||
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) {
|
||||||
Intent(context.applicationContext, V2RayVpnService::class.java)
|
Intent(context.applicationContext, V2RayVpnService::class.java)
|
||||||
} else {
|
} else {
|
||||||
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
|
||||||
|
@ -93,61 +124,22 @@ object V2RayServiceManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class V2RayCallback : V2RayVPNServiceSupportsSet {
|
|
||||||
override fun shutdown(): Long {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
|
||||||
// called by go
|
|
||||||
return try {
|
|
||||||
serviceControl.stopService()
|
|
||||||
0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun prepare(): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun protect(l: Long): Boolean {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return true
|
|
||||||
return serviceControl.vpnProtect(l.toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setup(s: String): Long {
|
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
|
||||||
return try {
|
|
||||||
serviceControl.startService()
|
|
||||||
lastQueryTime = System.currentTimeMillis()
|
|
||||||
startSpeedNotification()
|
|
||||||
0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
|
* Starts the V2Ray core service.
|
||||||
*/
|
*/
|
||||||
|
fun startCoreLoop(): Boolean {
|
||||||
fun startV2rayPoint() {
|
if (coreController.isRunning) {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
return false
|
||||||
val guid = MmkvManager.getSelectServer() ?: return
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
|
||||||
if (v2rayPoint.isRunning) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val service = getService() ?: return false
|
||||||
|
val guid = MmkvManager.getSelectServer() ?: return false
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid) ?: return false
|
||||||
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
||||||
if (!result.status)
|
if (!result.status)
|
||||||
return
|
return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||||
|
@ -156,60 +148,188 @@ object V2RayServiceManager {
|
||||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||||
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
v2rayPoint.configureFileContent = result.content
|
|
||||||
v2rayPoint.domainName = result.domainPort
|
|
||||||
currentConfig = config
|
currentConfig = config
|
||||||
|
|
||||||
try {
|
try {
|
||||||
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
coreController.startLoop(result.content)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.e(AppConfig.TAG, "Failed to start Core loop", e)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (v2rayPoint.isRunning) {
|
if (coreController.isRunning == false) {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
|
||||||
showNotification()
|
|
||||||
|
|
||||||
PluginUtil.runPlugin(service, config, result.domainPort)
|
|
||||||
} else {
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||||
cancelNotification()
|
NotificationService.cancelNotification()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||||
|
NotificationService.showNotification(currentConfig)
|
||||||
|
NotificationService.startSpeedNotification(currentConfig)
|
||||||
|
|
||||||
|
PluginUtil.runPlugin(service, config, result.socksPort)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to startup service", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopV2rayPoint() {
|
/**
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
* Stops the V2Ray core service.
|
||||||
|
* Unregisters broadcast receivers, stops notifications, and shuts down plugins.
|
||||||
|
* @return True if the core was stopped successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
fun stopCoreLoop(): Boolean {
|
||||||
|
val service = getService() ?: return false
|
||||||
|
|
||||||
if (v2rayPoint.isRunning) {
|
if (coreController.isRunning) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
v2rayPoint.stopLoop()
|
coreController.stopLoop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
cancelNotification()
|
NotificationService.cancelNotification()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
service.unregisterReceiver(mMsgReceive)
|
service.unregisterReceiver(mMsgReceive)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
|
||||||
}
|
}
|
||||||
PluginUtil.stopPlugin()
|
PluginUtil.stopPlugin()
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the statistics for a given tag and link.
|
||||||
|
* @param tag The tag to query.
|
||||||
|
* @param link The link to query.
|
||||||
|
* @return The statistics value.
|
||||||
|
*/
|
||||||
|
fun queryStats(tag: String, link: String): Long {
|
||||||
|
return coreController.queryStats(tag, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the connection delay for the current V2Ray configuration.
|
||||||
|
* Tests with primary URL first, then falls back to alternative URL if needed.
|
||||||
|
* Also fetches remote IP information if the delay test was successful.
|
||||||
|
*/
|
||||||
|
private fun measureV2rayDelay() {
|
||||||
|
if (coreController.isRunning == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val service = getService() ?: return@launch
|
||||||
|
var time = -1L
|
||||||
|
var errorStr = ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
|
||||||
|
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
|
}
|
||||||
|
if (time == -1L) {
|
||||||
|
try {
|
||||||
|
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
|
||||||
|
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = if (time >= 0) {
|
||||||
|
service.getString(R.string.connection_test_available, time)
|
||||||
|
} else {
|
||||||
|
service.getString(R.string.connection_test_error, errorStr)
|
||||||
|
}
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
||||||
|
|
||||||
|
// Only fetch IP info if the delay test was successful
|
||||||
|
if (time >= 0) {
|
||||||
|
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current service instance.
|
||||||
|
* @return The current service instance, or null if not available.
|
||||||
|
*/
|
||||||
|
private fun getService(): Service? {
|
||||||
|
return serviceControl?.get()?.getService()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core callback handler implementation for handling V2Ray core events.
|
||||||
|
* Handles startup, shutdown, socket protection, and status emission.
|
||||||
|
*/
|
||||||
|
private class CoreCallback : CoreCallbackHandler {
|
||||||
|
/**
|
||||||
|
* Called when V2Ray core starts up.
|
||||||
|
* @return 0 for success, any other value for failure.
|
||||||
|
*/
|
||||||
|
override fun startup(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when V2Ray core shuts down.
|
||||||
|
* @return 0 for success, any other value for failure.
|
||||||
|
*/
|
||||||
|
override fun shutdown(): Long {
|
||||||
|
val serviceControl = serviceControl?.get() ?: return -1
|
||||||
|
return try {
|
||||||
|
serviceControl.stopService()
|
||||||
|
0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when V2Ray core emits status information.
|
||||||
|
* @param l Status code.
|
||||||
|
* @param s Status message.
|
||||||
|
* @return Always returns 0.
|
||||||
|
*/
|
||||||
|
override fun onEmitStatus(l: Long, s: String?): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast receiver for handling messages sent to the service.
|
||||||
|
* Handles registration, service control, and screen events.
|
||||||
|
*/
|
||||||
private class ReceiveMessageHandler : BroadcastReceiver() {
|
private class ReceiveMessageHandler : BroadcastReceiver() {
|
||||||
|
/**
|
||||||
|
* Handles received broadcast messages.
|
||||||
|
* Processes service control messages and screen state changes.
|
||||||
|
* @param ctx The context in which the receiver is running.
|
||||||
|
* @param intent The intent being received.
|
||||||
|
*/
|
||||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||||
val serviceControl = serviceControl?.get() ?: return
|
val serviceControl = serviceControl?.get() ?: return
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
AppConfig.MSG_REGISTER_CLIENT -> {
|
AppConfig.MSG_REGISTER_CLIENT -> {
|
||||||
if (v2rayPoint.isRunning) {
|
if (coreController.isRunning) {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
||||||
} else {
|
} else {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||||
|
@ -225,15 +345,15 @@ object V2RayServiceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
AppConfig.MSG_STATE_STOP -> {
|
||||||
Log.d(ANG_PACKAGE, "Stop Service")
|
Log.i(AppConfig.TAG, "Stop Service")
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
AppConfig.MSG_STATE_RESTART -> {
|
||||||
Log.d(ANG_PACKAGE, "Restart Service")
|
Log.i(AppConfig.TAG, "Restart Service")
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
Thread.sleep(500L)
|
Thread.sleep(500L)
|
||||||
startV2Ray(serviceControl.getService())
|
startVService(serviceControl.getService())
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY -> {
|
AppConfig.MSG_MEASURE_DELAY -> {
|
||||||
|
@ -243,213 +363,15 @@ object V2RayServiceManager {
|
||||||
|
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
Intent.ACTION_SCREEN_OFF -> {
|
Intent.ACTION_SCREEN_OFF -> {
|
||||||
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats")
|
||||||
stopSpeedNotification()
|
NotificationService.stopSpeedNotification(currentConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent.ACTION_SCREEN_ON -> {
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats")
|
||||||
startSpeedNotification()
|
NotificationService.startSpeedNotification(currentConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun measureV2rayDelay() {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val service = serviceControl?.get()?.getService() ?: return@launch
|
|
||||||
var time = -1L
|
|
||||||
var errstr = ""
|
|
||||||
if (v2rayPoint.isRunning) {
|
|
||||||
try {
|
|
||||||
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
|
||||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
|
||||||
}
|
|
||||||
if (time == -1L) {
|
|
||||||
try {
|
|
||||||
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(true))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
|
|
||||||
errstr = e.message?.substringAfter("\":") ?: "empty message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val result = if (time == -1L) {
|
|
||||||
service.getString(R.string.connection_test_error, errstr)
|
|
||||||
} else {
|
|
||||||
service.getString(R.string.connection_test_available, time)
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNotification() {
|
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
|
||||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
} else {
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
}
|
|
||||||
|
|
||||||
val startMainIntent = Intent(service, MainActivity::class.java)
|
|
||||||
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
|
|
||||||
|
|
||||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
stopV2RayIntent.`package` = ANG_PACKAGE
|
|
||||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
|
||||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
|
|
||||||
|
|
||||||
val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
|
||||||
restartV2RayIntent.`package` = ANG_PACKAGE
|
|
||||||
restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
|
|
||||||
val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
|
|
||||||
|
|
||||||
val channelId =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
createNotificationChannel()
|
|
||||||
} else {
|
|
||||||
// If earlier version channel ID is not used
|
|
||||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
mBuilder = NotificationCompat.Builder(service, channelId)
|
|
||||||
.setSmallIcon(R.drawable.ic_stat_name)
|
|
||||||
.setContentTitle(currentConfig?.remarks)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setContentIntent(contentPendingIntent)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_delete_24dp,
|
|
||||||
service.getString(R.string.notification_action_stop_v2ray),
|
|
||||||
stopV2RayPendingIntent
|
|
||||||
)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_delete_24dp,
|
|
||||||
service.getString(R.string.title_service_restart),
|
|
||||||
restartV2RayPendingIntent
|
|
||||||
)
|
|
||||||
//.build()
|
|
||||||
|
|
||||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
|
||||||
|
|
||||||
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private fun createNotificationChannel(): String {
|
|
||||||
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
|
||||||
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
|
||||||
val chan = NotificationChannel(
|
|
||||||
channelId,
|
|
||||||
channelName, NotificationManager.IMPORTANCE_HIGH
|
|
||||||
)
|
|
||||||
chan.lightColor = Color.DKGRAY
|
|
||||||
chan.importance = NotificationManager.IMPORTANCE_NONE
|
|
||||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
|
||||||
getNotificationManager()?.createNotificationChannel(chan)
|
|
||||||
return channelId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelNotification() {
|
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
|
||||||
} else {
|
|
||||||
service.stopForeground(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
mBuilder = null
|
|
||||||
mDisposable?.dispose()
|
|
||||||
mDisposable = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
|
|
||||||
if (mBuilder != null) {
|
|
||||||
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
|
|
||||||
} else if (proxyTraffic > directTraffic) {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
|
|
||||||
} else {
|
|
||||||
mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
|
|
||||||
}
|
|
||||||
mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
|
|
||||||
mBuilder?.setContentText(contentText) // Emui4.1 need content text even if style is set as BigTextStyle
|
|
||||||
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNotificationManager(): NotificationManager? {
|
|
||||||
if (mNotificationManager == null) {
|
|
||||||
val service = serviceControl?.get()?.getService() ?: return null
|
|
||||||
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
}
|
|
||||||
return mNotificationManager
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startSpeedNotification() {
|
|
||||||
if (mDisposable == null &&
|
|
||||||
v2rayPoint.isRunning &&
|
|
||||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
|
|
||||||
) {
|
|
||||||
var lastZeroSpeed = false
|
|
||||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
|
||||||
outboundTags?.remove(TAG_DIRECT)
|
|
||||||
|
|
||||||
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.subscribe {
|
|
||||||
val queryTime = System.currentTimeMillis()
|
|
||||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
|
||||||
var proxyTotal = 0L
|
|
||||||
val text = StringBuilder()
|
|
||||||
outboundTags?.forEach {
|
|
||||||
val up = v2rayPoint.queryStats(it, AppConfig.UPLINK)
|
|
||||||
val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK)
|
|
||||||
if (up + down > 0) {
|
|
||||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
|
||||||
proxyTotal += up + down
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
|
||||||
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
|
|
||||||
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
|
||||||
if (!zeroSpeed || !lastZeroSpeed) {
|
|
||||||
if (proxyTotal == 0L) {
|
|
||||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
|
||||||
}
|
|
||||||
appendSpeedString(
|
|
||||||
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
|
||||||
directDownlink / sinceLastQueryInSeconds
|
|
||||||
)
|
|
||||||
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
|
||||||
}
|
|
||||||
lastZeroSpeed = zeroSpeed
|
|
||||||
lastQueryTime = queryTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
|
|
||||||
var n = name ?: "no tag"
|
|
||||||
n = n.substring(0, min(n.length, 6))
|
|
||||||
text.append(n)
|
|
||||||
for (i in n.length..6 step 2) {
|
|
||||||
text.append("\t")
|
|
||||||
}
|
|
||||||
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopSpeedNotification() {
|
|
||||||
mDisposable?.let {
|
|
||||||
it.dispose() //stop queryStats
|
|
||||||
mDisposable = null
|
|
||||||
updateNotification(currentConfig?.remarks, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@ import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.extension.serializable
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.handler.V2rayConfigManager
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.PluginUtil
|
import com.v2ray.ang.util.PluginUtil
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -26,12 +26,22 @@ import java.util.concurrent.Executors
|
||||||
class V2RayTestService : Service() {
|
class V2RayTestService : Service() {
|
||||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
|
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the V2Ray environment.
|
||||||
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Seq.setContext(this)
|
Seq.setContext(this)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the start command for the service.
|
||||||
|
* @param intent The intent.
|
||||||
|
* @param flags The flags.
|
||||||
|
* @param startId The start ID.
|
||||||
|
* @return The start mode.
|
||||||
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
MSG_MEASURE_CONFIG -> {
|
MSG_MEASURE_CONFIG -> {
|
||||||
|
@ -49,10 +59,20 @@ class V2RayTestService : Service() {
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the service.
|
||||||
|
* @param intent The intent.
|
||||||
|
* @return The binder.
|
||||||
|
*/
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the real ping test.
|
||||||
|
* @param guid The GUID of the configuration.
|
||||||
|
* @return The ping result.
|
||||||
|
*/
|
||||||
private fun startRealPing(guid: String): Long {
|
private fun startRealPing(guid: String): Long {
|
||||||
val retFailure = -1L
|
val retFailure = -1L
|
||||||
|
|
||||||
|
@ -61,11 +81,11 @@ class V2RayTestService : Service() {
|
||||||
val delay = PluginUtil.realPingHy2(this, config)
|
val delay = PluginUtil.realPingHy2(this, config)
|
||||||
return delay
|
return delay
|
||||||
} else {
|
} else {
|
||||||
val config = V2rayConfigManager.getV2rayConfig(this, guid)
|
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
|
||||||
if (!config.status) {
|
if (!configResult.status) {
|
||||||
return retFailure
|
return retFailure
|
||||||
}
|
}
|
||||||
return SpeedtestUtil.realPing(config.content)
|
return SpeedtestManager.realPing(configResult.content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,8 @@ 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.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
|
@ -35,18 +33,11 @@ import java.lang.ref.SoftReference
|
||||||
class V2RayVpnService : VpnService(), ServiceControl {
|
class V2RayVpnService : VpnService(), ServiceControl {
|
||||||
companion object {
|
companion object {
|
||||||
private const val VPN_MTU = 1500
|
private const val VPN_MTU = 1500
|
||||||
private const val PRIVATE_VLAN4_CLIENT = "10.10.10.1"
|
|
||||||
private const val PRIVATE_VLAN4_ROUTER = "10.10.10.2"
|
|
||||||
private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:10:1"
|
|
||||||
private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:10:2"
|
|
||||||
private const val TUN2SOCKS = "libtun2socks.so"
|
private const val TUN2SOCKS = "libtun2socks.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private lateinit var mInterface: ParcelFileDescriptor
|
private lateinit var mInterface: ParcelFileDescriptor
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
|
||||||
//val fd: Int get() = mInterface.fd
|
|
||||||
private lateinit var process: Process
|
private lateinit var process: Process
|
||||||
|
|
||||||
/**destroy
|
/**destroy
|
||||||
|
@ -88,7 +79,6 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||||
StrictMode.setThreadPolicy(policy)
|
StrictMode.setThreadPolicy(policy)
|
||||||
V2RayServiceManager.serviceControl = SoftReference(this)
|
V2RayServiceManager.serviceControl = SoftReference(this)
|
||||||
|
@ -105,225 +95,17 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
V2RayServiceManager.cancelNotification()
|
NotificationService.cancelNotification()
|
||||||
}
|
|
||||||
|
|
||||||
private fun setup() {
|
|
||||||
val prepare = prepare(this)
|
|
||||||
if (prepare != null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the old interface has exactly the same parameters, use it!
|
|
||||||
// Configure a builder while parsing the parameters.
|
|
||||||
val builder = Builder()
|
|
||||||
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
|
||||||
|
|
||||||
builder.setMtu(VPN_MTU)
|
|
||||||
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
|
||||||
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
|
||||||
val bypassLan = SettingsManager.routingRulesetsBypassLan()
|
|
||||||
if (bypassLan) {
|
|
||||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
|
|
||||||
val addr = it.split('/')
|
|
||||||
builder.addRoute(addr[0], addr[1].toInt())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
builder.addRoute("0.0.0.0", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
|
||||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
|
||||||
if (bypassLan) {
|
|
||||||
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
|
||||||
} else {
|
|
||||||
builder.addRoute("::", 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
|
||||||
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
|
||||||
// } else {
|
|
||||||
Utils.getVpnDnsServers()
|
|
||||||
.forEach {
|
|
||||||
if (Utils.isPureIpAddress(it)) {
|
|
||||||
builder.addDnsServer(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
|
|
||||||
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
|
||||||
|
|
||||||
val selfPackageName = BuildConfig.APPLICATION_ID
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
|
|
||||||
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
|
||||||
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
|
|
||||||
//process self package
|
|
||||||
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
|
|
||||||
apps?.forEach {
|
|
||||||
try {
|
|
||||||
if (bypassApps)
|
|
||||||
builder.addDisallowedApplication(it)
|
|
||||||
else
|
|
||||||
builder.addAllowedApplication(it)
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
builder.addDisallowedApplication(selfPackageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the old interface since the parameters have been changed.
|
|
||||||
try {
|
|
||||||
mInterface.close()
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
try {
|
|
||||||
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
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.
|
|
||||||
try {
|
|
||||||
mInterface = builder.establish()!!
|
|
||||||
isRunning = true
|
|
||||||
runTun2socks()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// non-nullable lateinit var
|
|
||||||
e.printStackTrace()
|
|
||||||
stopV2Ray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runTun2socks() {
|
|
||||||
val socksPort = SettingsManager.getSocksPort()
|
|
||||||
val cmd = arrayListOf(
|
|
||||||
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
|
||||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
|
||||||
"--netif-netmask", "255.255.255.252",
|
|
||||||
"--socks-server-addr", "$LOOPBACK:${socksPort}",
|
|
||||||
"--tunmtu", VPN_MTU.toString(),
|
|
||||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
|
||||||
"--enable-udprelay",
|
|
||||||
"--loglevel", "notice"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
|
|
||||||
cmd.add("--netif-ip6addr")
|
|
||||||
cmd.add(PRIVATE_VLAN6_ROUTER)
|
|
||||||
}
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
|
|
||||||
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
|
||||||
cmd.add("--dnsgw")
|
|
||||||
cmd.add("$LOOPBACK:${localDnsPort}")
|
|
||||||
}
|
|
||||||
Log.d(packageName, cmd.toString())
|
|
||||||
|
|
||||||
try {
|
|
||||||
val proBuilder = ProcessBuilder(cmd)
|
|
||||||
proBuilder.redirectErrorStream(true)
|
|
||||||
process = proBuilder
|
|
||||||
.directory(applicationContext.filesDir)
|
|
||||||
.start()
|
|
||||||
Thread {
|
|
||||||
Log.d(packageName, "$TUN2SOCKS check")
|
|
||||||
process.waitFor()
|
|
||||||
Log.d(packageName, "$TUN2SOCKS exited")
|
|
||||||
if (isRunning) {
|
|
||||||
Log.d(packageName, "$TUN2SOCKS restart")
|
|
||||||
runTun2socks()
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
Log.d(packageName, process.toString())
|
|
||||||
|
|
||||||
sendFd()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(packageName, e.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendFd() {
|
|
||||||
val fd = mInterface.fileDescriptor
|
|
||||||
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
|
||||||
Log.d(packageName, path)
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
var tries = 0
|
|
||||||
while (true) try {
|
|
||||||
Thread.sleep(50L shl tries)
|
|
||||||
Log.d(packageName, "sendFd tries: $tries")
|
|
||||||
LocalSocket().use { localSocket ->
|
|
||||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
|
||||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
|
||||||
localSocket.outputStream.write(42)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(packageName, e.toString())
|
|
||||||
if (tries > 5) break
|
|
||||||
tries += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
V2RayServiceManager.startV2rayPoint()
|
if (V2RayServiceManager.startCoreLoop()) {
|
||||||
|
startService()
|
||||||
|
}
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
//return super.onStartCommand(intent, flags, startId)
|
//return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopV2Ray(isForced: Boolean = true) {
|
|
||||||
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
|
|
||||||
// val emptyInfo = VpnNetworkInfo()
|
|
||||||
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
|
||||||
// saveVpnNetworkInfo(configName, info)
|
|
||||||
isRunning = false
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
try {
|
|
||||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Log.d(packageName, "tun2socks destroy")
|
|
||||||
process.destroy()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(packageName, e.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
V2RayServiceManager.stopV2rayPoint()
|
|
||||||
|
|
||||||
if (isForced) {
|
|
||||||
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
|
||||||
//It's strage but true.
|
|
||||||
//This can be verified by putting stopself() behind and call stopLoop and startLoop
|
|
||||||
//in a row for several times. You will find that later created v2ray core report port in use
|
|
||||||
//which means the first v2ray core somehow failed to stop and release the port.
|
|
||||||
stopSelf()
|
|
||||||
|
|
||||||
try {
|
|
||||||
mInterface.close()
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getService(): Service {
|
override fun getService(): Service {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -343,8 +125,251 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val context = newBase?.let {
|
val context = newBase?.let {
|
||||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
|
||||||
}
|
}
|
||||||
super.attachBaseContext(context)
|
super.attachBaseContext(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the VPN service.
|
||||||
|
* Prepares the VPN and configures it if preparation is successful.
|
||||||
|
*/
|
||||||
|
private fun setup() {
|
||||||
|
val prepare = prepare(this)
|
||||||
|
if (prepare != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setupVpnService() != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runTun2socks()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the VPN service.
|
||||||
|
* @return True if the VPN service was configured successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun setupVpnService(): Boolean {
|
||||||
|
// If the old interface has exactly the same parameters, use it!
|
||||||
|
// Configure a builder while parsing the parameters.
|
||||||
|
val builder = Builder()
|
||||||
|
val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
|
||||||
|
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||||
|
|
||||||
|
builder.setMtu(VPN_MTU)
|
||||||
|
builder.addAddress(vpnConfig.ipv4Client, 30)
|
||||||
|
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
|
val bypassLan = SettingsManager.routingRulesetsBypassLan()
|
||||||
|
if (bypassLan) {
|
||||||
|
AppConfig.ROUTED_IP_LIST.forEach {
|
||||||
|
val addr = it.split('/')
|
||||||
|
builder.addRoute(addr[0], addr[1].toInt())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.addRoute("0.0.0.0", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||||
|
builder.addAddress(vpnConfig.ipv6Client, 126)
|
||||||
|
if (bypassLan) {
|
||||||
|
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
||||||
|
builder.addRoute("fc00::", 18) //Xray-core default FakeIPv6 Pool
|
||||||
|
} else {
|
||||||
|
builder.addRoute("::", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||||
|
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
|
// } else {
|
||||||
|
SettingsManager.getVpnDnsServers()
|
||||||
|
.forEach {
|
||||||
|
if (Utils.isPureIpAddress(it)) {
|
||||||
|
builder.addDnsServer(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
|
builder.setSession(V2RayServiceManager.getRunningServerName())
|
||||||
|
|
||||||
|
val selfPackageName = BuildConfig.APPLICATION_ID
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
|
||||||
|
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
|
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
|
||||||
|
//process self package
|
||||||
|
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
|
||||||
|
apps?.forEach {
|
||||||
|
try {
|
||||||
|
if (bypassApps)
|
||||||
|
builder.addDisallowedApplication(it)
|
||||||
|
else
|
||||||
|
builder.addAllowedApplication(it)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.addDisallowedApplication(selfPackageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the old interface since the parameters have been changed.
|
||||||
|
try {
|
||||||
|
mInterface.close()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
try {
|
||||||
|
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to request default network", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
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.
|
||||||
|
try {
|
||||||
|
mInterface = builder.establish()!!
|
||||||
|
isRunning = true
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// non-nullable lateinit var
|
||||||
|
Log.e(AppConfig.TAG, "Failed to establish VPN interface", e)
|
||||||
|
stopV2Ray()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the tun2socks process.
|
||||||
|
* Starts the tun2socks process with the appropriate parameters.
|
||||||
|
*/
|
||||||
|
private fun runTun2socks() {
|
||||||
|
Log.i(AppConfig.TAG, "Start run $TUN2SOCKS")
|
||||||
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
|
val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
|
||||||
|
val cmd = arrayListOf(
|
||||||
|
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||||
|
"--netif-ipaddr", vpnConfig.ipv4Router,
|
||||||
|
"--netif-netmask", "255.255.255.252",
|
||||||
|
"--socks-server-addr", "$LOOPBACK:${socksPort}",
|
||||||
|
"--tunmtu", VPN_MTU.toString(),
|
||||||
|
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||||
|
"--enable-udprelay",
|
||||||
|
"--loglevel", "notice"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
|
||||||
|
cmd.add("--netif-ip6addr")
|
||||||
|
cmd.add(vpnConfig.ipv6Router)
|
||||||
|
}
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
|
||||||
|
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
||||||
|
cmd.add("--dnsgw")
|
||||||
|
cmd.add("$LOOPBACK:${localDnsPort}")
|
||||||
|
}
|
||||||
|
Log.i(AppConfig.TAG, cmd.toString())
|
||||||
|
|
||||||
|
try {
|
||||||
|
val proBuilder = ProcessBuilder(cmd)
|
||||||
|
proBuilder.redirectErrorStream(true)
|
||||||
|
process = proBuilder
|
||||||
|
.directory(applicationContext.filesDir)
|
||||||
|
.start()
|
||||||
|
Thread {
|
||||||
|
Log.i(AppConfig.TAG, "$TUN2SOCKS check")
|
||||||
|
process.waitFor()
|
||||||
|
Log.i(AppConfig.TAG, "$TUN2SOCKS exited")
|
||||||
|
if (isRunning) {
|
||||||
|
Log.i(AppConfig.TAG, "$TUN2SOCKS restart")
|
||||||
|
runTun2socks()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}")
|
||||||
|
|
||||||
|
sendFd()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the file descriptor to the tun2socks process.
|
||||||
|
* Attempts to send the file descriptor multiple times if necessary.
|
||||||
|
*/
|
||||||
|
private fun sendFd() {
|
||||||
|
val fd = mInterface.fileDescriptor
|
||||||
|
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||||
|
Log.i(AppConfig.TAG, "LocalSocket path : $path")
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
var tries = 0
|
||||||
|
while (true) try {
|
||||||
|
Thread.sleep(50L shl tries)
|
||||||
|
Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries")
|
||||||
|
LocalSocket().use { localSocket ->
|
||||||
|
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||||
|
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||||
|
localSocket.outputStream.write(42)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to send file descriptor, try: $tries", e)
|
||||||
|
if (tries > 5) break
|
||||||
|
tries += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the V2Ray service.
|
||||||
|
* @param isForced Whether to force stop the service.
|
||||||
|
*/
|
||||||
|
private fun stopV2Ray(isForced: Boolean = true) {
|
||||||
|
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
|
||||||
|
// val emptyInfo = VpnNetworkInfo()
|
||||||
|
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
||||||
|
// saveVpnNetworkInfo(configName, info)
|
||||||
|
isRunning = false
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
try {
|
||||||
|
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.i(AppConfig.TAG, "$TUN2SOCKS destroy")
|
||||||
|
process.destroy()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
V2RayServiceManager.stopCoreLoop()
|
||||||
|
|
||||||
|
if (isForced) {
|
||||||
|
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
|
||||||
|
//It's strage but true.
|
||||||
|
//This can be verified by putting stopself() behind and call stopLoop and startLoop
|
||||||
|
//in a row for several times. You will find that later created v2ray core report port in use
|
||||||
|
//which means the first v2ray core somehow failed to stop and release the port.
|
||||||
|
stopSelf()
|
||||||
|
|
||||||
|
try {
|
||||||
|
mInterface.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to close VPN interface", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,26 +6,42 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityAboutBinding
|
import com.v2ray.ang.databinding.ActivityAboutBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.ZipUtil
|
import com.v2ray.ang.util.ZipUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
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) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
@ -33,12 +49,13 @@ class AboutActivity : BaseActivity() {
|
||||||
title = getString(R.string.title_about)
|
title = getString(R.string.title_about)
|
||||||
|
|
||||||
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
|
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
|
||||||
|
|
||||||
binding.layoutBackup.setOnClickListener {
|
binding.layoutBackup.setOnClickListener {
|
||||||
val ret = backupConfiguration(extDir.absolutePath)
|
val ret = backupConfiguration(extDir.absolutePath)
|
||||||
if (ret.first) {
|
if (ret.first) {
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,67 +67,69 @@ class AboutActivity : BaseActivity() {
|
||||||
Intent(Intent.ACTION_SEND).setType("application/zip")
|
Intent(Intent.ACTION_SEND).setType("application/zip")
|
||||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.putExtra(
|
.putExtra(
|
||||||
Intent.EXTRA_STREAM, FileProvider.getUriForFile(
|
Intent.EXTRA_STREAM,
|
||||||
|
FileProvider.getUriForFile(
|
||||||
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
|
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
|
||||||
)
|
)
|
||||||
), getString(R.string.title_configuration_share)
|
), getString(R.string.title_configuration_share)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutRestore.setOnClickListener {
|
binding.layoutRestore.setOnClickListener {
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val permission =
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
} else {
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
} else {
|
||||||
}
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
RxPermissions(this)
|
|
||||||
.request(permission)
|
|
||||||
.subscribe {
|
|
||||||
if (it) {
|
|
||||||
try {
|
|
||||||
showFileChooser()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||||
|
try {
|
||||||
|
showFileChooser()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestPermissionLauncher.launch(permission)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutSoureCcode.setOnClickListener {
|
binding.layoutSoureCcode.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGUrl)
|
Utils.openUri(this, AppConfig.APP_URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutFeedback.setOnClickListener {
|
binding.layoutFeedback.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGIssues)
|
Utils.openUri(this, AppConfig.APP_ISSUES_URL)
|
||||||
}
|
}
|
||||||
binding.layoutOssLicenses.setOnClickListener{
|
|
||||||
val webView = android.webkit.WebView(this);
|
binding.layoutOssLicenses.setOnClickListener {
|
||||||
webView.loadUrl("file:///android_asset/open_source_licenses.html");
|
val webView = android.webkit.WebView(this)
|
||||||
|
webView.loadUrl("file:///android_asset/open_source_licenses.html")
|
||||||
android.app.AlertDialog.Builder(this)
|
android.app.AlertDialog.Builder(this)
|
||||||
.setTitle("Open source licenses")
|
.setTitle("Open source licenses")
|
||||||
.setView(webView)
|
.setView(webView)
|
||||||
.setPositiveButton("OK", android.content.DialogInterface.OnClickListener { dialog, whichButton -> dialog.dismiss() }).show()
|
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutTgChannel.setOnClickListener {
|
binding.layoutTgChannel.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.TgChannelUrl)
|
Utils.openUri(this, AppConfig.TG_CHANNEL_URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.layoutPrivacyPolicy.setOnClickListener {
|
binding.layoutPrivacyPolicy.setOnClickListener {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
|
Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY)
|
||||||
}
|
}
|
||||||
|
|
||||||
"v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})".also {
|
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
||||||
binding.tvVersion.text = it
|
binding.tvVersion.text = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
|
private fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
|
||||||
val dateFormated = SimpleDateFormat(
|
val dateFormated = SimpleDateFormat(
|
||||||
"yyyy-MM-dd-HH-mm-ss",
|
"yyyy-MM-dd-HH-mm-ss",
|
||||||
Locale.getDefault()
|
Locale.getDefault()
|
||||||
|
@ -131,7 +150,7 @@ class AboutActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreConfiguration(zipFile: File): Boolean {
|
private fun restoreConfiguration(zipFile: File): Boolean {
|
||||||
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
|
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
|
||||||
|
|
||||||
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
|
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
|
||||||
|
@ -151,15 +170,15 @@ class AboutActivity : BaseActivity() {
|
||||||
try {
|
try {
|
||||||
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||||
} catch (ex: android.content.ActivityNotFoundException) {
|
} catch (ex: android.content.ActivityNotFoundException) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "File chooser activity not found: ${ex.message}", ex)
|
Log.e(AppConfig.TAG, "File chooser activity not found", ex)
|
||||||
toast(R.string.toast_require_file_manager)
|
toast(R.string.toast_require_file_manager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
|
@ -169,15 +188,14 @@ class AboutActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (restoreConfiguration(targetFile)) {
|
if (restoreConfiguration(targetFile)) {
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
|
Log.e(AppConfig.TAG, "Error during file restore", e)
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -6,10 +6,16 @@ import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.helper.CustomDividerItemDecoration
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -34,6 +40,26 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, Utils.getLocale()))
|
super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, SettingsManager.getLocale()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a custom divider to a RecyclerView.
|
||||||
|
*
|
||||||
|
* @param recyclerView The target RecyclerView to which the divider will be added.
|
||||||
|
* @param context The context used to access resources.
|
||||||
|
* @param drawableResId The resource ID of the drawable to be used as the divider.
|
||||||
|
* @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL).
|
||||||
|
*/
|
||||||
|
fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, context: Context?, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) {
|
||||||
|
// Get the drawable from resources
|
||||||
|
val drawable = ContextCompat.getDrawable(context!!, drawableResId)
|
||||||
|
requireNotNull(drawable) { "Drawable resource not found" }
|
||||||
|
|
||||||
|
// Create a DividerItemDecoration with the specified orientation
|
||||||
|
val dividerItemDecoration = CustomDividerItemDecoration(drawable, orientation)
|
||||||
|
|
||||||
|
// Add the divider to the RecyclerView
|
||||||
|
recyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityCheckUpdateBinding
|
||||||
|
import com.v2ray.ang.dto.CheckUpdateResult
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
|
import com.v2ray.ang.handler.UpdateCheckerManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class CheckUpdateActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
title = getString(R.string.update_check_for_update)
|
||||||
|
|
||||||
|
binding.layoutCheckUpdate.setOnClickListener {
|
||||||
|
checkForUpdates(binding.checkPreRelease.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
|
||||||
|
}
|
||||||
|
binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)
|
||||||
|
|
||||||
|
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
|
||||||
|
binding.tvVersion.text = it
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForUpdates(binding.checkPreRelease.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkForUpdates(includePreRelease: Boolean) {
|
||||||
|
toast(R.string.update_checking_for_update)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
|
||||||
|
if (result.hasUpdate) {
|
||||||
|
showUpdateDialog(result)
|
||||||
|
} else {
|
||||||
|
toastSuccess(R.string.update_already_latest_version)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
|
||||||
|
toastError(e.message ?: getString(R.string.toast_failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUpdateDialog(result: CheckUpdateResult) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.update_new_version_found, result.latestVersion))
|
||||||
|
.setMessage(result.releaseNotes)
|
||||||
|
.setPositiveButton(R.string.update_now) { _, _ ->
|
||||||
|
result.downloadUrl?.let {
|
||||||
|
Utils.openUri(this, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,19 @@
|
||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -22,7 +24,7 @@ import java.io.IOException
|
||||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
var logsetsAll: MutableList<String> = mutableListOf()
|
private var logsetsAll: MutableList<String> = mutableListOf()
|
||||||
var logsets: MutableList<String> = mutableListOf()
|
var logsets: MutableList<String> = mutableListOf()
|
||||||
private val adapter by lazy { LogcatRecyclerAdapter(this) }
|
private val adapter by lazy { LogcatRecyclerAdapter(this) }
|
||||||
|
|
||||||
|
@ -34,8 +36,8 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
|
||||||
|
|
||||||
binding.refreshLayout.setOnRefreshListener(this)
|
binding.refreshLayout.setOnRefreshListener(this)
|
||||||
|
|
||||||
|
@ -59,16 +61,16 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
Runtime.getRuntime().exec(lst.toTypedArray())
|
Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
val allText = process.inputStream.bufferedReader().use { it.readLines() }
|
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
logsetsAll = allText.toMutableList()
|
logsetsAll = allText.toMutableList()
|
||||||
logsets = allText.toMutableList()
|
logsets = allText.toMutableList()
|
||||||
adapter.notifyDataSetChanged()
|
refreshData()
|
||||||
binding.refreshLayout.isRefreshing = false
|
binding.refreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to get logcat", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,11 +87,11 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
logsetsAll.clear()
|
logsetsAll.clear()
|
||||||
logsets.clear()
|
logsets.clear()
|
||||||
adapter.notifyDataSetChanged()
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to clear logcat", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +121,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.copy_all -> {
|
R.id.copy_all -> {
|
||||||
Utils.setClipboard(this, logsets.joinToString("\n"))
|
Utils.setClipboard(this, logsets.joinToString("\n"))
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,11 +141,16 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
logsetsAll.filter { it.contains(key) }.toMutableList()
|
logsetsAll.filter { it.contains(key) }.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
getLogcat()
|
getLogcat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun refreshData() {
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,32 @@
|
||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
|
||||||
|
|
||||||
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
|
||||||
private var mActivity: LogcatActivity = activity
|
private var mActivity: LogcatActivity = activity
|
||||||
|
|
||||||
|
|
||||||
override fun getItemCount() = mActivity.logsets.size
|
override fun getItemCount() = mActivity.logsets.size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||||
val content = mActivity.logsets[position]
|
try {
|
||||||
holder.itemSubSettingBinding.logContent.text = content
|
val log = mActivity.logsets[position]
|
||||||
|
if (log.isEmpty()) {
|
||||||
|
holder.itemSubSettingBinding.logTag.text = ""
|
||||||
|
holder.itemSubSettingBinding.logContent.text = ""
|
||||||
|
} else {
|
||||||
|
val content = log.split("):", limit = 2)
|
||||||
|
holder.itemSubSettingBinding.logTag.text = content.first().split("(", limit = 2).first().trim()
|
||||||
|
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error binding log view data", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ActivityNotFoundException
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
@ -23,17 +23,17 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.VPN
|
import com.v2ray.ang.AppConfig.VPN
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityMainBinding
|
import com.v2ray.ang.databinding.ActivityMainBinding
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MigrateManager
|
import com.v2ray.ang.handler.MigrateManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
@ -41,14 +41,10 @@ import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.drakeet.support.toast.ToastCompat
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||||
private val binding by lazy {
|
private val binding by lazy {
|
||||||
|
@ -81,6 +77,53 @@ 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.READ_CONTENT_FROM_URI ->
|
||||||
|
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "*/*"
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
}, getString(R.string.title_file_chooser)))
|
||||||
|
|
||||||
|
Action.POST_NOTIFICATIONS -> {}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
pendingAction = Action.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pendingAction: Action = Action.NONE
|
||||||
|
|
||||||
|
enum class Action {
|
||||||
|
NONE,
|
||||||
|
IMPORT_QR_CODE_CONFIG,
|
||||||
|
READ_CONTENT_FROM_URI,
|
||||||
|
POST_NOTIFICATIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
val uri = it.data?.data
|
||||||
|
if (it.resultCode == RESULT_OK && uri != null) {
|
||||||
|
readContentFromUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
if (it.resultCode == RESULT_OK) {
|
||||||
|
importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
@ -89,7 +132,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
|
|
||||||
binding.fab.setOnClickListener {
|
binding.fab.setOnClickListener {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
Utils.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
||||||
val intent = VpnService.prepare(this)
|
val intent = VpnService.prepare(this)
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
|
@ -111,7 +154,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) {
|
||||||
|
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
|
||||||
|
} else {
|
||||||
|
binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
|
||||||
|
}
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
|
@ -129,12 +177,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
migrateLegacy()
|
migrateLegacy()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
RxPermissions(this)
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
pendingAction = Action.POST_NOTIFICATIONS
|
||||||
.subscribe {
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
if (!it)
|
}
|
||||||
toast(R.string.toast_permission_denied_notification)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
@ -142,13 +188,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
} else {
|
} else {
|
||||||
//super.onBackPressed()
|
isEnabled = false
|
||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
isEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun setupViewModel() {
|
private fun setupViewModel() {
|
||||||
mainViewModel.updateListAction.observe(this) { index ->
|
mainViewModel.updateListAction.observe(this) { index ->
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
|
@ -214,23 +262,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
binding.tabGroup.isVisible = true
|
binding.tabGroup.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startV2Ray() {
|
private fun startV2Ray() {
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
toast(R.string.title_file_chooser)
|
toast(R.string.title_file_chooser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
V2RayServiceManager.startV2Ray(this)
|
V2RayServiceManager.startVService(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartV2Ray() {
|
private fun restartV2Ray() {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
Utils.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
|
}
|
||||||
|
lifecycleScope.launch {
|
||||||
|
delay(500)
|
||||||
|
startV2Ray()
|
||||||
}
|
}
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
startV2Ray()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onResume() {
|
public override fun onResume() {
|
||||||
|
@ -267,7 +314,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.import_qrcode -> {
|
R.id.import_qrcode -> {
|
||||||
importQRcode(true)
|
importQRcode()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,6 +323,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.import_local -> {
|
||||||
|
importConfigLocal()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.import_manually_vmess -> {
|
R.id.import_manually_vmess -> {
|
||||||
importManually(EConfigType.VMESS.value)
|
importManually(EConfigType.VMESS.value)
|
||||||
true
|
true
|
||||||
|
@ -316,44 +368,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.import_config_custom_clipboard -> {
|
|
||||||
importConfigCustomClipboard()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_config_custom_local -> {
|
|
||||||
importConfigCustomLocal()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_config_custom_url -> {
|
|
||||||
importConfigCustomUrlClipboard()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_config_custom_url_scan -> {
|
|
||||||
importQRcode(false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.sub_update -> {
|
|
||||||
importConfigViaSub()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.export_all -> {
|
R.id.export_all -> {
|
||||||
binding.pbWaiting.show()
|
exportAll()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.exportAllServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (ret > 0)
|
|
||||||
toast(getString(R.string.title_export_config_count, ret))
|
|
||||||
else
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -375,77 +391,31 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.del_all_config -> {
|
R.id.del_all_config -> {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
delAllConfig()
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.removeAllServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
toast(getString(R.string.title_del_config_count, ret))
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.del_duplicate_config -> {
|
R.id.del_duplicate_config -> {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
delDuplicateConfig()
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.removeDuplicateServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
toast(getString(R.string.title_del_duplicate_config_count, ret))
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.del_invalid_config -> {
|
R.id.del_invalid_config -> {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
|
delInvalidConfig()
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
binding.pbWaiting.show()
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val ret = mainViewModel.removeInvalidServer()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
toast(getString(R.string.title_del_config_count, ret))
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.sort_by_test_results -> {
|
R.id.sort_by_test_results -> {
|
||||||
binding.pbWaiting.show()
|
sortByTestResults()
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
mainViewModel.sortByTestResults()
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
binding.pbWaiting.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.sub_update -> {
|
||||||
|
importConfigViaSub()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,39 +431,17 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
/**
|
/**
|
||||||
* import config from qrcode
|
* import config from qrcode
|
||||||
*/
|
*/
|
||||||
private fun importQRcode(forConfig: Boolean): Boolean {
|
private fun importQRcode(): 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)
|
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
|
||||||
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
|
} else {
|
||||||
// } catch (e: Exception) {
|
pendingAction = Action.IMPORT_QR_CODE_CONFIG
|
||||||
RxPermissions(this)
|
requestPermissionLauncher.launch(permission)
|
||||||
.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)
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
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
|
||||||
*/
|
*/
|
||||||
|
@ -503,7 +451,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
val clipboard = Utils.getClipboard(this)
|
val clipboard = Utils.getClipboard(this)
|
||||||
importBatchConfig(clipboard)
|
importBatchConfig(clipboard)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to import config from clipboard", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -524,100 +472,38 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
}
|
}
|
||||||
|
|
||||||
countSub > 0 -> initGroupTab()
|
countSub > 0 -> initGroupTab()
|
||||||
else -> toast(R.string.toast_failure)
|
else -> toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to import batch config", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun importConfigCustomClipboard()
|
|
||||||
: Boolean {
|
|
||||||
try {
|
|
||||||
val configText = Utils.getClipboard(this)
|
|
||||||
if (TextUtils.isEmpty(configText)) {
|
|
||||||
toast(R.string.toast_none_data_clipboard)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
importCustomizeConfig(configText)
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* import config from local config file
|
* import config from local config file
|
||||||
*/
|
*/
|
||||||
private fun importConfigCustomLocal(): Boolean {
|
private fun importConfigLocal(): Boolean {
|
||||||
try {
|
try {
|
||||||
showFileChooser()
|
showFileChooser()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to import config from local file", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun importConfigCustomUrlClipboard()
|
|
||||||
: Boolean {
|
|
||||||
try {
|
|
||||||
val url = Utils.getClipboard(this)
|
|
||||||
if (TextUtils.isEmpty(url)) {
|
|
||||||
toast(R.string.toast_none_data_clipboard)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return importConfigCustomUrl(url)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* import config from url
|
|
||||||
*/
|
|
||||||
private fun importConfigCustomUrl(url: String?): Boolean {
|
|
||||||
try {
|
|
||||||
if (!Utils.isValidUrl(url)) {
|
|
||||||
toast(R.string.toast_invalid_url)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val configText = try {
|
|
||||||
Utils.getUrlContentWithCustomUserAgent(url)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
""
|
|
||||||
}
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
importCustomizeConfig(configText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* import config from sub
|
* import config from sub
|
||||||
*/
|
*/
|
||||||
private fun importConfigViaSub(): Boolean {
|
private fun importConfigViaSub(): Boolean {
|
||||||
// val dialog = AlertDialog.Builder(this)
|
|
||||||
// .setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
|
||||||
// .setCancelable(false)
|
|
||||||
// .show()
|
|
||||||
binding.pbWaiting.show()
|
binding.pbWaiting.show()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -628,15 +514,96 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
toast(getString(R.string.title_update_config_count, count))
|
toast(getString(R.string.title_update_config_count, count))
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
//dialog.dismiss()
|
|
||||||
binding.pbWaiting.hide()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun exportAll() {
|
||||||
|
binding.pbWaiting.show()
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val ret = mainViewModel.exportAllServer()
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (ret > 0)
|
||||||
|
toast(getString(R.string.title_export_config_count, ret))
|
||||||
|
else
|
||||||
|
toastError(R.string.toast_failure)
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delAllConfig() {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
binding.pbWaiting.show()
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val ret = mainViewModel.removeAllServer()
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
toast(getString(R.string.title_del_config_count, ret))
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delDuplicateConfig() {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
binding.pbWaiting.show()
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val ret = mainViewModel.removeDuplicateServer()
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
toast(getString(R.string.title_del_duplicate_config_count, ret))
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delInvalidConfig() {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
binding.pbWaiting.show()
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val ret = mainViewModel.removeInvalidServer()
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
toast(getString(R.string.title_del_config_count, ret))
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortByTestResults() {
|
||||||
|
binding.pbWaiting.show()
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
mainViewModel.sortByTestResults()
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* show file chooser
|
* show file chooser
|
||||||
*/
|
*/
|
||||||
|
@ -645,17 +612,17 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
intent.type = "*/*"
|
intent.type = "*/*"
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
try {
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
} catch (ex: ActivityNotFoundException) {
|
} else {
|
||||||
toast(R.string.toast_require_file_manager)
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
val uri = it.data?.data
|
pendingAction = Action.READ_CONTENT_FROM_URI
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
|
||||||
readContentFromUri(uri)
|
} else {
|
||||||
|
requestPermissionLauncher.launch(permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -668,42 +635,17 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
|
||||||
.request(permission)
|
|
||||||
.subscribe {
|
|
||||||
if (it) {
|
|
||||||
try {
|
|
||||||
contentResolver.openInputStream(uri).use { input ->
|
|
||||||
importCustomizeConfig(input?.bufferedReader()?.readText())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
* import customize config
|
try {
|
||||||
*/
|
contentResolver.openInputStream(uri).use { input ->
|
||||||
private fun importCustomizeConfig(server: String?) {
|
importBatchConfig(input?.bufferedReader()?.readText())
|
||||||
try {
|
}
|
||||||
if (server == null || TextUtils.isEmpty(server)) {
|
} catch (e: Exception) {
|
||||||
toast(R.string.toast_none_data)
|
Log.e(AppConfig.TAG, "Failed to read content from URI", e)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (mainViewModel.appendCustomConfigServer(server)) {
|
} else {
|
||||||
mainViewModel.reloadServerList()
|
requestPermissionLauncher.launch(permission)
|
||||||
toast(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
|
||||||
e.printStackTrace()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -732,35 +674,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||||
// Handle navigation view item clicks here.
|
// Handle navigation view item clicks here.
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.sub_setting -> {
|
R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
|
||||||
requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
|
R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java))
|
||||||
}
|
R.id.routing_setting -> requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
|
||||||
|
R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java))
|
||||||
|
R.id.settings -> startActivity(
|
||||||
|
Intent(this, SettingsActivity::class.java)
|
||||||
|
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
||||||
|
)
|
||||||
|
|
||||||
R.id.settings -> {
|
R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
|
||||||
startActivity(
|
R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
|
||||||
Intent(this, SettingsActivity::class.java)
|
R.id.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java))
|
||||||
.putExtra("isRunning", mainViewModel.isRunning.value == true)
|
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.routing_setting -> {
|
|
||||||
requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
R.id.promotion -> {
|
|
||||||
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.logcat -> {
|
|
||||||
startActivity(Intent(this, LogcatActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.about -> {
|
|
||||||
startActivity(Intent(this, AboutActivity::class.java))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,11 +3,13 @@ package com.v2ray.ang.ui
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AngApplication.Companion.application
|
import com.v2ray.ang.AngApplication.Companion.application
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
@ -16,16 +18,18 @@ import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
import kotlinx.coroutines.Dispatchers
|
||||||
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
|
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -37,169 +41,278 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||||
private val share_method: Array<out String> by lazy {
|
private val share_method: Array<out String> by lazy {
|
||||||
mActivity.resources.getStringArray(R.array.share_method)
|
mActivity.resources.getStringArray(R.array.share_method)
|
||||||
}
|
}
|
||||||
|
private val share_method_more: Array<out String> by lazy {
|
||||||
|
mActivity.resources.getStringArray(R.array.share_method_more)
|
||||||
|
}
|
||||||
var isRunning = false
|
var isRunning = false
|
||||||
|
private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total number of items in the adapter (servers count + footer view)
|
||||||
|
* @return The total item count
|
||||||
|
*/
|
||||||
override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
|
override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||||
if (holder is MainViewHolder) {
|
if (holder is MainViewHolder) {
|
||||||
val guid = mActivity.mainViewModel.serversCache[position].guid
|
val guid = mActivity.mainViewModel.serversCache[position].guid
|
||||||
val profile = mActivity.mainViewModel.serversCache[position].profile
|
val profile = mActivity.mainViewModel.serversCache[position].profile
|
||||||
// //filter
|
val isCustom = profile.configType == EConfigType.CUSTOM
|
||||||
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
|
|
||||||
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
|
|
||||||
// ) {
|
|
||||||
// holder.itemMainBinding.cardView.visibility = View.GONE
|
|
||||||
// } else {
|
|
||||||
// holder.itemMainBinding.cardView.visibility = View.VISIBLE
|
|
||||||
// }
|
|
||||||
|
|
||||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
|
||||||
|
|
||||||
holder.itemMainBinding.tvName.text = profile.remarks
|
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
|
//Name address
|
||||||
|
holder.itemMainBinding.tvName.text = profile.remarks
|
||||||
|
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
||||||
|
holder.itemMainBinding.tvType.text = profile.configType.name
|
||||||
|
|
||||||
|
//TestResult
|
||||||
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
||||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//layoutIndicator
|
||||||
if (guid == MmkvManager.getSelectServer()) {
|
if (guid == MmkvManager.getSelectServer()) {
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
|
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||||
}
|
}
|
||||||
holder.itemMainBinding.tvSubscription.text = MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks ?: ""
|
|
||||||
|
|
||||||
var shareOptions = share_method.asList()
|
//subscription remarks
|
||||||
when (profile.configType) {
|
val subRemarks = getSubscriptionRemarks(profile)
|
||||||
EConfigType.CUSTOM -> {
|
holder.itemMainBinding.tvSubscription.text = subRemarks
|
||||||
holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
|
holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE
|
||||||
shareOptions = shareOptions.takeLast(1)
|
|
||||||
|
//layout
|
||||||
|
if (doubleColumnDisplay) {
|
||||||
|
holder.itemMainBinding.layoutShare.visibility = View.GONE
|
||||||
|
holder.itemMainBinding.layoutEdit.visibility = View.GONE
|
||||||
|
holder.itemMainBinding.layoutRemove.visibility = View.GONE
|
||||||
|
holder.itemMainBinding.layoutMore.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
//share method
|
||||||
|
val shareOptions = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
||||||
|
|
||||||
|
holder.itemMainBinding.layoutMore.setOnClickListener {
|
||||||
|
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.itemMainBinding.layoutShare.visibility = View.VISIBLE
|
||||||
|
holder.itemMainBinding.layoutEdit.visibility = View.VISIBLE
|
||||||
|
holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE
|
||||||
|
holder.itemMainBinding.layoutMore.visibility = View.GONE
|
||||||
|
|
||||||
|
//share method
|
||||||
|
val shareOptions = if (isCustom) share_method.asList().takeLast(1) else share_method.asList()
|
||||||
|
|
||||||
|
holder.itemMainBinding.layoutShare.setOnClickListener {
|
||||||
|
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
||||||
holder.itemMainBinding.tvType.text = profile.configType.name
|
editServer(guid, profile)
|
||||||
}
|
}
|
||||||
}
|
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
||||||
|
removeServer(guid, position)
|
||||||
// 隐藏主页服务器地址为xxx:xxx:***/xxx.xxx.xxx.***
|
|
||||||
val strState = "${
|
|
||||||
profile.server?.let {
|
|
||||||
if (it.contains(":"))
|
|
||||||
it.split(":").take(2).joinToString(":", postfix = ":***")
|
|
||||||
else
|
|
||||||
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
|
|
||||||
}
|
|
||||||
} : ${profile.serverPort}"
|
|
||||||
|
|
||||||
holder.itemMainBinding.tvStatistics.text = strState
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
|
||||||
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
|
||||||
try {
|
|
||||||
when (i) {
|
|
||||||
0 -> {
|
|
||||||
if (profile.configType == EConfigType.CUSTOM) {
|
|
||||||
shareFullContent(guid)
|
|
||||||
} else {
|
|
||||||
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
|
||||||
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
|
||||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
|
|
||||||
mActivity.toast(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
mActivity.toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
2 -> shareFullContent(guid)
|
|
||||||
else -> mActivity.toast("else")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
|
||||||
val intent = Intent().putExtra("guid", guid)
|
|
||||||
.putExtra("isRunning", isRunning)
|
|
||||||
.putExtra("createConfigType", profile.configType.value)
|
|
||||||
if (profile.configType == EConfigType.CUSTOM) {
|
|
||||||
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
|
||||||
} else {
|
|
||||||
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
|
||||||
if (guid != MmkvManager.getSelectServer()) {
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
|
||||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
removeServer(guid, position)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
removeServer(guid, position)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
application.toast(R.string.toast_action_not_allowed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||||
val selected = MmkvManager.getSelectServer()
|
setSelectServer(guid)
|
||||||
if (guid != selected) {
|
|
||||||
MmkvManager.setSelectServer(guid)
|
|
||||||
if (!TextUtils.isEmpty(selected)) {
|
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
|
|
||||||
}
|
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
|
||||||
if (isRunning) {
|
|
||||||
Utils.stopVService(mActivity)
|
|
||||||
Observable.timer(500, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
V2RayServiceManager.startV2Ray(mActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (holder is FooterViewHolder) {
|
|
||||||
//if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) {
|
|
||||||
if (true) {
|
|
||||||
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
|
||||||
} else {
|
|
||||||
holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
|
||||||
Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// if (holder is FooterViewHolder) {
|
||||||
|
// if (true) {
|
||||||
|
// holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
||||||
|
// } else {
|
||||||
|
// holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
||||||
|
// Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareFullContent(guid: String) {
|
/**
|
||||||
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
|
* Gets the server address information
|
||||||
mActivity.toast(R.string.toast_success)
|
* Hides part of IP or domain information for privacy protection
|
||||||
|
* @param profile The server configuration
|
||||||
|
* @return Formatted address string
|
||||||
|
*/
|
||||||
|
private fun getAddress(profile: ProfileItem): String {
|
||||||
|
// Hide xxx:xxx:***/xxx.xxx.xxx.***
|
||||||
|
return "${
|
||||||
|
profile.server?.let {
|
||||||
|
if (it.contains(":"))
|
||||||
|
it.split(":").take(2).joinToString(":", postfix = ":***")
|
||||||
|
else
|
||||||
|
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
|
||||||
|
}
|
||||||
|
} : ${profile.serverPort}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the subscription remarks information
|
||||||
|
* @param profile The server configuration
|
||||||
|
* @return Subscription remarks string, or empty string if none
|
||||||
|
*/
|
||||||
|
private fun getSubscriptionRemarks(profile: ProfileItem): String {
|
||||||
|
val subRemarks =
|
||||||
|
if (mActivity.mainViewModel.subscriptionId.isEmpty())
|
||||||
|
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
|
||||||
|
else
|
||||||
|
null
|
||||||
|
return subRemarks?.toString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares server configuration
|
||||||
|
* Displays a dialog with sharing options and executes the selected action
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
* @param profile The server configuration
|
||||||
|
* @param position The position in the list
|
||||||
|
* @param shareOptions The list of share options
|
||||||
|
* @param skip The number of options to skip
|
||||||
|
*/
|
||||||
|
private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List<String>, skip: Int) {
|
||||||
|
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
when (i + skip) {
|
||||||
|
0 -> showQRCode(guid)
|
||||||
|
1 -> share2Clipboard(guid)
|
||||||
|
2 -> shareFullContent(guid)
|
||||||
|
3 -> editServer(guid, profile)
|
||||||
|
4 -> removeServer(guid, position)
|
||||||
|
else -> mActivity.toast("else")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error when sharing server", e)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays QR code for the server configuration
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
*/
|
||||||
|
private fun showQRCode(guid: String) {
|
||||||
|
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
||||||
|
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
||||||
|
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares server configuration to clipboard
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
*/
|
||||||
|
private fun share2Clipboard(guid: String) {
|
||||||
|
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
|
||||||
|
mActivity.toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
mActivity.toast(R.string.toast_failure)
|
mActivity.toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares full server configuration content to clipboard
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
*/
|
||||||
|
private fun shareFullContent(guid: String) {
|
||||||
|
mActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (result == 0) {
|
||||||
|
mActivity.toastSuccess(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
mActivity.toastError(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits server configuration
|
||||||
|
* Opens appropriate editing interface based on configuration type
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
* @param profile The server configuration
|
||||||
|
*/
|
||||||
|
private fun editServer(guid: String, profile: ProfileItem) {
|
||||||
|
val intent = Intent().putExtra("guid", guid)
|
||||||
|
.putExtra("isRunning", isRunning)
|
||||||
|
.putExtra("createConfigType", profile.configType.value)
|
||||||
|
if (profile.configType == EConfigType.CUSTOM) {
|
||||||
|
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
||||||
|
} else {
|
||||||
|
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes server configuration
|
||||||
|
* Handles confirmation dialog and related checks
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
* @param position The position in the list
|
||||||
|
*/
|
||||||
private fun removeServer(guid: String, position: Int) {
|
private fun removeServer(guid: String, position: Int) {
|
||||||
|
if (guid != MmkvManager.getSelectServer()) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
|
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
removeServerSub(guid, position)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
removeServerSub(guid, position)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
application.toast(R.string.toast_action_not_allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the actual server removal process
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
* @param position The position in the list
|
||||||
|
*/
|
||||||
|
private fun removeServerSub(guid: String, position: Int) {
|
||||||
mActivity.mainViewModel.removeServer(guid)
|
mActivity.mainViewModel.removeServer(guid)
|
||||||
notifyItemRemoved(position)
|
notifyItemRemoved(position)
|
||||||
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
|
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected server
|
||||||
|
* Updates UI and restarts service if needed
|
||||||
|
* @param guid The server unique identifier to select
|
||||||
|
*/
|
||||||
|
private fun setSelectServer(guid: String) {
|
||||||
|
val selected = MmkvManager.getSelectServer()
|
||||||
|
if (guid != selected) {
|
||||||
|
MmkvManager.setSelectServer(guid)
|
||||||
|
if (!TextUtils.isEmpty(selected)) {
|
||||||
|
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
|
||||||
|
}
|
||||||
|
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
||||||
|
if (isRunning) {
|
||||||
|
V2RayServiceManager.stopVService(mActivity)
|
||||||
|
mActivity.lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
delay(500)
|
||||||
|
V2RayServiceManager.startVService(mActivity)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
VIEW_TYPE_ITEM ->
|
VIEW_TYPE_ITEM ->
|
||||||
|
@ -246,4 +359,4 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,35 +1,35 @@
|
||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.widget.Toast
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityBypassListBinding
|
import com.v2ray.ang.databinding.ActivityBypassListBinding
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.extension.v2RayApplication
|
import com.v2ray.ang.extension.v2RayApplication
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import es.dmoral.toasty.Toasty
|
||||||
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() {
|
||||||
private val binding by lazy {
|
private val binding by lazy { ActivityBypassListBinding.inflate(layoutInflater) }
|
||||||
ActivityBypassListBinding.inflate(layoutInflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var adapter: PerAppProxyAdapter? = null
|
private var adapter: PerAppProxyAdapter? = null
|
||||||
private var appsAll: List<AppInfo>? = null
|
private var appsAll: List<AppInfo>? = null
|
||||||
|
@ -38,98 +38,43 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
title = getString(R.string.per_app_proxy_settings)
|
||||||
binding.recyclerView.addItemDecoration(dividerItemDecoration)
|
|
||||||
|
|
||||||
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
|
|
||||||
AppManagerUtil.rxLoadNetworkAppList(this)
|
lifecycleScope.launch {
|
||||||
.subscribeOn(Schedulers.io())
|
try {
|
||||||
.map {
|
binding.pbWaiting.show()
|
||||||
if (blacklist != null) {
|
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
it.forEach { one ->
|
val apps = withContext(Dispatchers.IO) {
|
||||||
if (blacklist.contains(one.packageName)) {
|
val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
|
||||||
one.isSelected = 1
|
|
||||||
} else {
|
if (blacklist != null) {
|
||||||
one.isSelected = 0
|
appsList.forEach { app ->
|
||||||
|
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
|
||||||
}
|
}
|
||||||
}
|
appsList.sortedWith { p1, p2 ->
|
||||||
val comparator = Comparator<AppInfo> { p1, p2 ->
|
when {
|
||||||
when {
|
p1.isSelected > p2.isSelected -> -1
|
||||||
p1.isSelected > p2.isSelected -> -1
|
p1.isSelected == p2.isSelected -> 0
|
||||||
p1.isSelected == p2.isSelected -> 0
|
else -> 1
|
||||||
else -> 1
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
it.sortedWith(comparator)
|
|
||||||
} else {
|
|
||||||
val comparator = object : Comparator<AppInfo> {
|
|
||||||
val collator = Collator.getInstance()
|
val collator = Collator.getInstance()
|
||||||
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
|
appsList.sortedWith(compareBy(collator) { it.appName })
|
||||||
}
|
}
|
||||||
it.sortedWith(comparator)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// .map {
|
appsAll = apps
|
||||||
// val comparator = object : Comparator<AppInfo> {
|
adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
|
||||||
// val collator = Collator.getInstance()
|
|
||||||
// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
|
|
||||||
// }
|
|
||||||
// it.sortedWith(comparator)
|
|
||||||
// }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
appsAll = it
|
|
||||||
adapter = PerAppProxyAdapter(this, it, blacklist)
|
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.pbWaiting.visibility = View.GONE
|
binding.pbWaiting.hide()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
Log.e(ANG_PACKAGE, "Error loading apps", e)
|
||||||
}
|
}
|
||||||
/***
|
|
||||||
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
||||||
var dst = 0
|
|
||||||
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
dst += dy
|
|
||||||
if (dst > threshold) {
|
|
||||||
header_view.hide()
|
|
||||||
dst = 0
|
|
||||||
} else if (dst < -20) {
|
|
||||||
header_view.show()
|
|
||||||
dst = 0
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var hiding = false
|
|
||||||
fun View.hide() {
|
|
||||||
val target = -height.toFloat()
|
|
||||||
if (hiding || translationY == target) return
|
|
||||||
animate()
|
|
||||||
.translationY(target)
|
|
||||||
.setInterpolator(AccelerateInterpolator(2F))
|
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
|
||||||
hiding = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
hiding = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var showing = false
|
|
||||||
fun View.show() {
|
|
||||||
val target = 0f
|
|
||||||
if (showing || translationY == target) return
|
|
||||||
animate()
|
|
||||||
.translationY(target)
|
|
||||||
.setInterpolator(DecelerateInterpolator(2F))
|
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
|
||||||
showing = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
showing = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
***/
|
|
||||||
|
|
||||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
||||||
|
@ -141,35 +86,9 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
||||||
|
|
||||||
/***
|
binding.layoutSwitchBypassAppsTips.setOnClickListener {
|
||||||
et_search.setOnEditorActionListener { v, actionId, event ->
|
Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show()
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
|
||||||
//hide
|
|
||||||
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
||||||
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
|
|
||||||
|
|
||||||
val key = v.text.toString().toUpperCase()
|
|
||||||
val apps = ArrayList<AppInfo>()
|
|
||||||
if (TextUtils.isEmpty(key)) {
|
|
||||||
appsAll?.forEach {
|
|
||||||
apps.add(it)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
appsAll?.forEach {
|
|
||||||
if (it.appName.toUpperCase().indexOf(key) >= 0) {
|
|
||||||
apps.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
|
||||||
recycler_view.adapter = adapter
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
***/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
@ -199,8 +118,10 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
R.id.select_all -> adapter?.let {
|
R.id.select_all -> adapter?.let { it ->
|
||||||
val pkgNames = it.apps.map { it.packageName }
|
val pkgNames = it.apps.map { it.packageName }
|
||||||
if (it.blacklist.containsAll(pkgNames)) {
|
if (it.blacklist.containsAll(pkgNames)) {
|
||||||
it.apps.forEach {
|
it.apps.forEach {
|
||||||
|
@ -237,13 +158,20 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
|
|
||||||
private fun selectProxyApp() {
|
private fun selectProxyApp() {
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
val url = AppConfig.androidpackagenamelistUrl
|
binding.pbWaiting.show()
|
||||||
|
|
||||||
|
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val content = Utils.getUrlContext(url, 5000)
|
var content = HttpUtil.getUrlContent(url, 5000)
|
||||||
|
if (content.isNullOrEmpty()) {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
|
||||||
|
}
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
Log.d(ANG_PACKAGE, content)
|
Log.i(AppConfig.TAG, content)
|
||||||
selectProxyApp(content, true)
|
selectProxyApp(content, true)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,7 +180,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
val content = Utils.getClipboard(applicationContext)
|
val content = Utils.getClipboard(applicationContext)
|
||||||
if (TextUtils.isEmpty(content)) return
|
if (TextUtils.isEmpty(content)) return
|
||||||
selectProxyApp(content, false)
|
selectProxyApp(content, false)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportProxyApp() {
|
private fun exportProxyApp() {
|
||||||
|
@ -262,9 +190,10 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
lst = lst + System.getProperty("line.separator") + it
|
lst = lst + System.getProperty("line.separator") + it
|
||||||
}
|
}
|
||||||
Utils.setClipboard(applicationContext, lst)
|
Utils.setClipboard(applicationContext, lst)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun selectProxyApp(content: String, force: Boolean): Boolean {
|
private fun selectProxyApp(content: String, force: Boolean): Boolean {
|
||||||
try {
|
try {
|
||||||
val proxyApps = if (TextUtils.isEmpty(content)) {
|
val proxyApps = if (TextUtils.isEmpty(content)) {
|
||||||
|
@ -277,10 +206,10 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
adapter?.blacklist?.clear()
|
adapter?.blacklist?.clear()
|
||||||
|
|
||||||
if (binding.switchBypassApps.isChecked) {
|
if (binding.switchBypassApps.isChecked) {
|
||||||
adapter?.let {
|
adapter?.let { it ->
|
||||||
it.apps.forEach block@{
|
it.apps.forEach block@{
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
Log.d(ANG_PACKAGE, packageName)
|
Log.i(AppConfig.TAG, packageName)
|
||||||
if (!inProxyApps(proxyApps, packageName, force)) {
|
if (!inProxyApps(proxyApps, packageName, force)) {
|
||||||
adapter?.blacklist?.add(packageName)
|
adapter?.blacklist?.add(packageName)
|
||||||
println(packageName)
|
println(packageName)
|
||||||
|
@ -290,10 +219,10 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
it.notifyDataSetChanged()
|
it.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
adapter?.let {
|
adapter?.let { it ->
|
||||||
it.apps.forEach block@{
|
it.apps.forEach block@{
|
||||||
val packageName = it.packageName
|
val packageName = it.packageName
|
||||||
Log.d(ANG_PACKAGE, packageName)
|
Log.i(AppConfig.TAG, packageName)
|
||||||
if (inProxyApps(proxyApps, packageName, force)) {
|
if (inProxyApps(proxyApps, packageName, force)) {
|
||||||
adapter?.blacklist?.add(packageName)
|
adapter?.blacklist?.add(packageName)
|
||||||
println(packageName)
|
println(packageName)
|
||||||
|
@ -304,7 +233,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Error selecting proxy app", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -339,7 +268,12 @@ class PerAppProxyActivity : BaseActivity() {
|
||||||
|
|
||||||
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun refreshData() {
|
||||||
|
adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
|
|
||||||
|
@ -35,7 +34,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
||||||
val view = View(ctx)
|
val view = View(ctx)
|
||||||
view.layoutParams = ViewGroup.LayoutParams(
|
view.layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0
|
0
|
||||||
)
|
)
|
||||||
BaseViewHolder(view)
|
BaseViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -78,7 +79,7 @@ class RoutingEditActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsManager.saveRoutingRuleset(position, rulesetItem)
|
SettingsManager.saveRoutingRuleset(position, rulesetItem)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
||||||
import com.v2ray.ang.dto.RulesetItem
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
|
@ -40,6 +41,16 @@ class RoutingSettingActivity : BaseActivity() {
|
||||||
resources.getStringArray(R.array.preset_rulesets)
|
resources.getStringArray(R.array.preset_rulesets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val requestCameraPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted: Boolean ->
|
||||||
|
if (isGranted) {
|
||||||
|
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
@ -48,20 +59,15 @@ class RoutingSettingActivity : BaseActivity() {
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
|
binding.tvDomainStrategySummary.text = getDomainStrategy()
|
||||||
found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
|
binding.layoutDomainStrategy.setOnClickListener {
|
||||||
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
setDomainStrategy()
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,97 +81,90 @@ class RoutingSettingActivity : BaseActivity() {
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
R.id.add_rule -> {
|
R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true }
|
||||||
startActivity(Intent(this, RoutingEditActivity::class.java))
|
R.id.import_predefined_rulesets -> importPredefined().let { true }
|
||||||
true
|
R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true }
|
||||||
}
|
R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true }
|
||||||
|
R.id.export_rulesets_to_clipboard -> export2Clipboard().let { true }
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
R.id.user_asset_setting -> {
|
private fun getDomainStrategy(): String {
|
||||||
startActivity(Intent(this, UserAssetActivity::class.java))
|
return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first()
|
||||||
true
|
}
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_predefined_rulesets -> {
|
private fun setDomainStrategy() {
|
||||||
|
android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
val value = routing_domain_strategy[i]
|
||||||
|
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
|
||||||
|
binding.tvDomainStrategySummary.text = value
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to set domain strategy", e)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importPredefined() {
|
||||||
|
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
||||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
try {
|
||||||
try {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
|
||||||
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
|
launch(Dispatchers.Main) {
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
refreshData()
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.import_rulesets_from_clipboard -> {
|
|
||||||
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val clipboard = try {
|
|
||||||
Utils.getClipboard(this)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
return@setPositiveButton
|
|
||||||
}
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = SettingsManager.resetRoutingRulesets(clipboard)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (result) {
|
|
||||||
refreshData()
|
refreshData()
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
//do nothing
|
//do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
true
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.import_rulesets_from_qrcode -> {
|
private fun importFromClipboard() {
|
||||||
RxPermissions(this)
|
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||||
.request(Manifest.permission.CAMERA)
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
.subscribe {
|
val clipboard = try {
|
||||||
if (it)
|
Utils.getClipboard(this)
|
||||||
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
|
} catch (e: Exception) {
|
||||||
else
|
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
||||||
toast(R.string.toast_permission_denied)
|
toastError(R.string.toast_failure)
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val result = SettingsManager.resetRoutingRulesets(clipboard)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (result) {
|
||||||
|
refreshData()
|
||||||
|
toastSuccess(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toastError(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
R.id.export_rulesets_to_clipboard -> {
|
|
||||||
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
|
||||||
if (rulesetList.isNullOrEmpty()) {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
} else {
|
|
||||||
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
}
|
}
|
||||||
true
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
}
|
//do nothing
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
private fun export2Clipboard() {
|
||||||
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
|
if (rulesetList.isNullOrEmpty()) {
|
||||||
|
toastError(R.string.toast_failure)
|
||||||
|
} else {
|
||||||
|
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
|
||||||
|
toastSuccess(R.string.toast_success)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
@ -182,9 +181,9 @@ class RoutingSettingActivity : BaseActivity() {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (result) {
|
if (result) {
|
||||||
refreshData()
|
refreshData()
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,10 +195,10 @@ class RoutingSettingActivity : BaseActivity() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun refreshData() {
|
fun refreshData() {
|
||||||
rulesets.clear()
|
rulesets.clear()
|
||||||
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
|
@ -4,47 +4,49 @@ import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
|
||||||
class ScScannerActivity : BaseActivity() {
|
class ScScannerActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private val requestCameraPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted: Boolean ->
|
||||||
|
if (isGranted) {
|
||||||
|
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_none)
|
setContentView(R.layout.activity_none)
|
||||||
importQRcode()
|
importQRcode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importQRcode(): Boolean {
|
private fun importQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe { granted ->
|
|
||||||
if (granted) {
|
|
||||||
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
if (it.resultCode == RESULT_OK) {
|
if (it.resultCode == RESULT_OK) {
|
||||||
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
|
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
|
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
|
||||||
|
|
||||||
if (count + countSub > 0) {
|
if (count + countSub > 0) {
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ package com.v2ray.ang.ui
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
class ScSwitchActivity : BaseActivity() {
|
class ScSwitchActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -12,10 +11,10 @@ class ScSwitchActivity : BaseActivity() {
|
||||||
|
|
||||||
setContentView(R.layout.activity_none)
|
setContentView(R.layout.activity_none)
|
||||||
|
|
||||||
if (V2RayServiceManager.v2rayPoint.isRunning) {
|
if (V2RayServiceManager.isRunning()) {
|
||||||
Utils.stopVService(this)
|
V2RayServiceManager.stopVService(this)
|
||||||
} else {
|
} else {
|
||||||
Utils.startVServiceFromToggle(this)
|
V2RayServiceManager.startVServiceFromToggle(this)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,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.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
@ -20,7 +22,39 @@ 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) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
|
||||||
|
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 +106,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 +131,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@ package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -14,13 +13,11 @@ import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.AppConfig.DEFAULT_PORT
|
import com.v2ray.ang.AppConfig.DEFAULT_PORT
|
||||||
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
||||||
import com.v2ray.ang.AppConfig.REALITY
|
import com.v2ray.ang.AppConfig.REALITY
|
||||||
import com.v2ray.ang.AppConfig.TLS
|
import com.v2ray.ang.AppConfig.TLS
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
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_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
|
||||||
|
@ -28,6 +25,7 @@ import com.v2ray.ang.dto.NetworkType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.JsonUtil
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
@ -126,6 +124,8 @@ class ServerActivity : BaseActivity() {
|
||||||
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
|
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
|
||||||
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
|
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
|
||||||
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
|
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
|
||||||
|
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
|
||||||
|
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
|
||||||
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
|
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
|
||||||
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
||||||
|
|
||||||
|
@ -326,7 +326,7 @@ class ServerActivity : BaseActivity() {
|
||||||
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
|
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
|
||||||
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
|
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
|
||||||
et_local_address?.text = Utils.getEditable(
|
et_local_address?.text = Utils.getEditable(
|
||||||
config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6"
|
config.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
)
|
)
|
||||||
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
|
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
|
||||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
|
@ -334,6 +334,8 @@ class ServerActivity : BaseActivity() {
|
||||||
et_port_hop?.text = Utils.getEditable(config.portHopping)
|
et_port_hop?.text = Utils.getEditable(config.portHopping)
|
||||||
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
|
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
|
||||||
et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
|
et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
|
||||||
|
et_bandwidth_down?.text = Utils.getEditable(config.bandwidthDown)
|
||||||
|
et_bandwidth_up?.text = Utils.getEditable(config.bandwidthUp)
|
||||||
}
|
}
|
||||||
val securityEncryptions =
|
val securityEncryptions =
|
||||||
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||||
|
@ -350,13 +352,13 @@ class ServerActivity : BaseActivity() {
|
||||||
container_alpn?.visibility = View.VISIBLE
|
container_alpn?.visibility = View.VISIBLE
|
||||||
|
|
||||||
et_sni?.text = Utils.getEditable(config.sni)
|
et_sni?.text = Utils.getEditable(config.sni)
|
||||||
config.fingerPrint?.let {
|
config.fingerPrint?.let { it ->
|
||||||
val utlsIndex = Utils.arrayFind(uTlsItems, it)
|
val utlsIndex = Utils.arrayFind(uTlsItems, it)
|
||||||
sp_stream_fingerprint?.setSelection(utlsIndex)
|
utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) }
|
||||||
}
|
}
|
||||||
config.alpn?.let {
|
config.alpn?.let { it ->
|
||||||
val alpnIndex = Utils.arrayFind(alpns, it)
|
val alpnIndex = Utils.arrayFind(alpns, it)
|
||||||
sp_stream_alpn?.setSelection(alpnIndex)
|
alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) }
|
||||||
}
|
}
|
||||||
if (config.security == TLS) {
|
if (config.security == TLS) {
|
||||||
container_allow_insecure?.visibility = View.VISIBLE
|
container_allow_insecure?.visibility = View.VISIBLE
|
||||||
|
@ -417,7 +419,7 @@ class ServerActivity : BaseActivity() {
|
||||||
et_public_key?.text = null
|
et_public_key?.text = null
|
||||||
et_reserved1?.text = Utils.getEditable("0,0,0")
|
et_reserved1?.text = Utils.getEditable("0,0,0")
|
||||||
et_local_address?.text =
|
et_local_address?.text =
|
||||||
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4)
|
||||||
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -476,9 +478,9 @@ 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.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -513,6 +515,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,21 +2,22 @@ package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.blacksquircle.ui.editorkit.utils.EditorTheme
|
import com.blacksquircle.ui.editorkit.utils.EditorTheme
|
||||||
import com.blacksquircle.ui.language.json.JsonLanguage
|
import com.blacksquircle.ui.language.json.JsonLanguage
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
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.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import me.drakeet.support.toast.ToastCompat
|
|
||||||
|
|
||||||
class ServerCustomConfigActivity : BaseActivity() {
|
class ServerCustomConfigActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
|
||||||
|
@ -77,8 +78,8 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||||
val profileItem = try {
|
val profileItem = try {
|
||||||
CustomFmt.parse(binding.editor.text.toString())
|
CustomFmt.parse(binding.editor.text.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to parse custom configuration", e)
|
||||||
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
|
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ class SettingsActivity : BaseActivity() {
|
||||||
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 vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
|
||||||
|
private val vpnInterfaceAddress by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
|
||||||
|
|
||||||
private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
|
private val 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) }
|
||||||
|
@ -161,7 +162,7 @@ class SettingsActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
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.DELAY_TEST_URL else nval
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mode?.setOnPreferenceChangeListener { _, newValue ->
|
mode?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
@ -202,7 +203,7 @@ class SettingsActivity : BaseActivity() {
|
||||||
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)
|
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.DELAY_TEST_URL)
|
||||||
|
|
||||||
initSharedPreference()
|
initSharedPreference()
|
||||||
}
|
}
|
||||||
|
@ -238,6 +239,7 @@ class SettingsActivity : BaseActivity() {
|
||||||
AppConfig.PREF_SPEED_ENABLED,
|
AppConfig.PREF_SPEED_ENABLED,
|
||||||
AppConfig.PREF_CONFIRM_REMOVE,
|
AppConfig.PREF_CONFIRM_REMOVE,
|
||||||
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
AppConfig.PREF_START_SCAN_IMMEDIATE,
|
||||||
|
AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
|
||||||
AppConfig.PREF_PREFER_IPV6,
|
AppConfig.PREF_PREFER_IPV6,
|
||||||
AppConfig.PREF_PROXY_SHARING,
|
AppConfig.PREF_PROXY_SHARING,
|
||||||
AppConfig.PREF_ALLOW_INSECURE
|
AppConfig.PREF_ALLOW_INSECURE
|
||||||
|
@ -248,12 +250,14 @@ class SettingsActivity : BaseActivity() {
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
AppConfig.PREF_VPN_BYPASS_LAN,
|
AppConfig.PREF_VPN_BYPASS_LAN,
|
||||||
|
AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
|
||||||
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,
|
||||||
AppConfig.PREF_LANGUAGE,
|
AppConfig.PREF_LANGUAGE,
|
||||||
AppConfig.PREF_UI_MODE_NIGHT,
|
AppConfig.PREF_UI_MODE_NIGHT,
|
||||||
AppConfig.PREF_LOGLEVEL,
|
AppConfig.PREF_LOGLEVEL,
|
||||||
|
AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
|
||||||
AppConfig.PREF_MODE
|
AppConfig.PREF_MODE
|
||||||
).forEach { key ->
|
).forEach { key ->
|
||||||
if (MmkvManager.decodeSettingsString(key) != null) {
|
if (MmkvManager.decodeSettingsString(key) != null) {
|
||||||
|
@ -272,7 +276,7 @@ class SettingsActivity : BaseActivity() {
|
||||||
localDnsPort?.isEnabled = vpn
|
localDnsPort?.isEnabled = vpn
|
||||||
vpnDns?.isEnabled = vpn
|
vpnDns?.isEnabled = vpn
|
||||||
vpnBypassLan?.isEnabled = vpn
|
vpnBypassLan?.isEnabled = vpn
|
||||||
vpn
|
vpnInterfaceAddress?.isEnabled = vpn
|
||||||
if (vpn) {
|
if (vpn) {
|
||||||
updateLocalDns(
|
updateLocalDns(
|
||||||
MmkvManager.decodeSettingsBool(
|
MmkvManager.decodeSettingsBool(
|
||||||
|
@ -363,6 +367,6 @@ class SettingsActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onModeHelpClicked(view: View) {
|
fun onModeHelpClicked(view: View) {
|
||||||
Utils.openUri(this, AppConfig.v2rayNGWikiMode)
|
Utils.openUri(this, AppConfig.APP_WIKI_MODE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,12 @@ import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -18,8 +20,8 @@ import kotlinx.coroutines.launch
|
||||||
class SubEditActivity : BaseActivity() {
|
class SubEditActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
private var del_config: MenuItem? = null
|
||||||
var save_config: MenuItem? = null
|
private var save_config: MenuItem? = null
|
||||||
|
|
||||||
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
|
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
|
||||||
|
|
||||||
|
@ -37,7 +39,7 @@ class SubEditActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bingding seleced server config
|
* binding selected server config
|
||||||
*/
|
*/
|
||||||
private fun bindingServer(subItem: SubscriptionItem): Boolean {
|
private fun bindingServer(subItem: SubscriptionItem): Boolean {
|
||||||
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
|
||||||
|
@ -45,6 +47,7 @@ class SubEditActivity : BaseActivity() {
|
||||||
binding.etFilter.text = Utils.getEditable(subItem.filter)
|
binding.etFilter.text = Utils.getEditable(subItem.filter)
|
||||||
binding.chkEnable.isChecked = subItem.enabled
|
binding.chkEnable.isChecked = subItem.enabled
|
||||||
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
|
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
|
||||||
|
binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
|
||||||
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
|
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
|
||||||
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
|
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
|
||||||
return true
|
return true
|
||||||
|
@ -76,6 +79,7 @@ class SubEditActivity : BaseActivity() {
|
||||||
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
||||||
subItem.prevProfile = binding.etPreProfile.text.toString()
|
subItem.prevProfile = binding.etPreProfile.text.toString()
|
||||||
subItem.nextProfile = binding.etNextProfile.text.toString()
|
subItem.nextProfile = binding.etNextProfile.text.toString()
|
||||||
|
subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked
|
||||||
|
|
||||||
if (TextUtils.isEmpty(subItem.remarks)) {
|
if (TextUtils.isEmpty(subItem.remarks)) {
|
||||||
toast(R.string.sub_setting_remarks)
|
toast(R.string.sub_setting_remarks)
|
||||||
|
@ -89,12 +93,14 @@ class SubEditActivity : BaseActivity() {
|
||||||
|
|
||||||
if (!Utils.isValidSubUrl(subItem.url)) {
|
if (!Utils.isValidSubUrl(subItem.url)) {
|
||||||
toast(R.string.toast_insecure_url_protocol)
|
toast(R.string.toast_insecure_url_protocol)
|
||||||
//return false
|
if (!subItem.allowInsecureUrl) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MmkvManager.encodeSubscription(editSubId, subItem)
|
MmkvManager.encodeSubscription(editSubId, subItem)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -104,19 +110,28 @@ class SubEditActivity : BaseActivity() {
|
||||||
*/
|
*/
|
||||||
private fun deleteServer(): Boolean {
|
private fun deleteServer(): Boolean {
|
||||||
if (editSubId.isNotEmpty()) {
|
if (editSubId.isNotEmpty()) {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
MmkvManager.removeSubscription(editSubId)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
launch(Dispatchers.Main) {
|
MmkvManager.removeSubscription(editSubId)
|
||||||
finish()
|
launch(Dispatchers.Main) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
MmkvManager.removeSubscription(editSubId)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
}
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||||
import com.v2ray.ang.databinding.LayoutProgressBinding
|
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
|
@ -35,6 +35,7 @@ class SubSettingActivity : BaseActivity() {
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
|
@ -58,21 +59,18 @@ class SubSettingActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.sub_update -> {
|
R.id.sub_update -> {
|
||||||
val dialog = AlertDialog.Builder(this)
|
binding.pbWaiting.show()
|
||||||
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
|
||||||
.setCancelable(false)
|
|
||||||
.show()
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val count = AngConfigManager.updateConfigViaSubAll()
|
val count = AngConfigManager.updateConfigViaSubAll()
|
||||||
delay(500L)
|
delay(500L)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +81,7 @@ class SubSettingActivity : BaseActivity() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun refreshData() {
|
fun refreshData() {
|
||||||
subscriptions = MmkvManager.decodeSubscriptions()
|
subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
|
|
@ -3,11 +3,14 @@ package com.v2ray.ang.ui
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
|
||||||
|
@ -18,6 +21,8 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
||||||
|
|
||||||
|
@ -44,6 +49,10 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
|
||||||
|
removeSubscription(subId, position)
|
||||||
|
}
|
||||||
|
|
||||||
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
if (!it.isPressed) return@setOnCheckedChangeListener
|
if (!it.isPressed) return@setOnCheckedChangeListener
|
||||||
subItem.enabled = isChecked
|
subItem.enabled = isChecked
|
||||||
|
@ -52,8 +61,13 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(subItem.url)) {
|
if (TextUtils.isEmpty(subItem.url)) {
|
||||||
|
holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE
|
||||||
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
|
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
|
||||||
|
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
|
||||||
} else {
|
} else {
|
||||||
|
holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
|
||||||
|
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
|
||||||
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
|
||||||
AlertDialog.Builder(mActivity)
|
AlertDialog.Builder(mActivity)
|
||||||
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
.setItems(share_method.asList().toTypedArray()) { _, i ->
|
||||||
|
@ -78,13 +92,39 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
|
||||||
else -> mActivity.toast("else")
|
else -> mActivity.toast("else")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Share subscription failed", e)
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeSubscription(subId: String, position: Int) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
|
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
removeSubscriptionSub(subId, position)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
removeSubscriptionSub(subId, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeSubscriptionSub(subId: String, position: Int) {
|
||||||
|
mActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
MmkvManager.removeSubscription(subId)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
notifyItemRangeChanged(position, mActivity.subscriptions.size)
|
||||||
|
mActivity.refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||||
return MainViewHolder(
|
return MainViewHolder(
|
||||||
ItemRecyclerSubSettingBinding.inflate(
|
ItemRecyclerSubSettingBinding.inflate(
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.v2ray.ang.ui
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -28,7 +29,7 @@ class TaskerActivity : BaseActivity() {
|
||||||
lstData.add("Default")
|
lstData.add("Default")
|
||||||
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
|
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
|
||||||
|
|
||||||
MmkvManager.decodeServerList()?.forEach { key ->
|
MmkvManager.decodeServerList().forEach { key ->
|
||||||
MmkvManager.decodeServerConfig(key)?.let { config ->
|
MmkvManager.decodeServerConfig(key)?.let { config ->
|
||||||
lstData.add(config.remarks)
|
lstData.add(config.remarks)
|
||||||
lstGuid.add(key)
|
lstGuid.add(key)
|
||||||
|
@ -60,7 +61,7 @@ class TaskerActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,16 @@ 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.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
import com.v2ray.ang.databinding.ActivityLogcatBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
class UrlSchemeActivity : BaseActivity() {
|
class UrlSchemeActivity : BaseActivity() {
|
||||||
|
@ -40,7 +46,7 @@ class UrlSchemeActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
toast(R.string.toast_failure)
|
toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +55,7 @@ class UrlSchemeActivity : BaseActivity() {
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Error processing URL scheme", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +63,7 @@ class UrlSchemeActivity : BaseActivity() {
|
||||||
if (uriString.isNullOrEmpty()) {
|
if (uriString.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d("UrlScheme", uriString)
|
Log.i(AppConfig.TAG, uriString)
|
||||||
|
|
||||||
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
|
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
|
||||||
val uri = Uri.parse(decodedUrl)
|
val uri = Uri.parse(decodedUrl)
|
||||||
|
@ -65,12 +71,16 @@ class UrlSchemeActivity : BaseActivity() {
|
||||||
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
|
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
|
||||||
decodedUrl += "#${fragment}"
|
decodedUrl += "#${fragment}"
|
||||||
}
|
}
|
||||||
Log.d("UrlScheme-decodedUrl", decodedUrl)
|
Log.i(AppConfig.TAG, decodedUrl)
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if (count + countSub > 0) {
|
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
|
||||||
toast(R.string.import_subscription_success)
|
withContext(Dispatchers.Main) {
|
||||||
} else {
|
if (count + countSub > 0) {
|
||||||
toast(R.string.import_subscription_failure)
|
toast(R.string.import_subscription_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.import_subscription_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,19 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.LOOPBACK
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivityUserAssetBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||||
import com.v2ray.ang.databinding.LayoutProgressBinding
|
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
|
import com.v2ray.ang.extension.concatUrl
|
||||||
import com.v2ray.ang.extension.toTrafficString
|
import com.v2ray.ang.extension.toTrafficString
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -38,18 +39,47 @@ import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.Proxy
|
|
||||||
import java.net.URL
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class UserAssetActivity : BaseActivity() {
|
class UserAssetActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
|
||||||
|
|
||||||
|
private val requestStoragePermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted: Boolean ->
|
||||||
|
if (isGranted) {
|
||||||
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
|
intent.type = "*/*"
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
|
try {
|
||||||
|
chooseFile.launch(
|
||||||
|
Intent.createChooser(
|
||||||
|
intent,
|
||||||
|
getString(R.string.title_file_chooser)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (ex: android.content.ActivityNotFoundException) {
|
||||||
|
toast(R.string.toast_require_file_manager)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requestCameraPermissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted: Boolean ->
|
||||||
|
if (isGranted) {
|
||||||
|
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -58,12 +88,18 @@ class UserAssetActivity : BaseActivity() {
|
||||||
|
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
binding.recyclerView.adapter = UserAssetAdapter()
|
binding.recyclerView.adapter = UserAssetAdapter()
|
||||||
|
|
||||||
|
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
|
||||||
|
binding.layoutGeoFilesSources.setOnClickListener {
|
||||||
|
setGeoFilesSources()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
@ -80,36 +116,32 @@ class UserAssetActivity : BaseActivity() {
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getGeoFilesSources(): String {
|
||||||
|
return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setGeoFilesSources() {
|
||||||
|
AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
val value = AppConfig.GEO_FILES_SOURCES[i]
|
||||||
|
MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
|
||||||
|
binding.tvGeoFilesSourcesSummary.text = value
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to set geo files sources", e)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showFileChooser() {
|
private fun showFileChooser() {
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
} else {
|
} else {
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
RxPermissions(this)
|
requestStoragePermissionLauncher.launch(permission)
|
||||||
.request(permission)
|
|
||||||
.subscribe {
|
|
||||||
if (it) {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
|
||||||
intent.type = "*/*"
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
|
|
||||||
try {
|
|
||||||
chooseFile.launch(
|
|
||||||
Intent.createChooser(
|
|
||||||
intent,
|
|
||||||
getString(R.string.title_file_chooser)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (ex: android.content.ActivityNotFoundException) {
|
|
||||||
toast(R.string.toast_require_file_manager)
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val uri = result.data?.data
|
val uri = result.data?.data
|
||||||
if (result.resultCode == RESULT_OK && uri != null) {
|
if (result.resultCode == RESULT_OK && uri != null) {
|
||||||
val assetId = Utils.getUuid()
|
val assetId = Utils.getUuid()
|
||||||
|
@ -127,7 +159,7 @@ class UserAssetActivity : BaseActivity() {
|
||||||
copyFile(uri)
|
copyFile(uri)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
toast(R.string.toast_asset_copy_failed)
|
toastError(R.string.toast_asset_copy_failed)
|
||||||
MmkvManager.removeAssetUrl(assetId)
|
MmkvManager.removeAssetUrl(assetId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,8 +170,8 @@ class UserAssetActivity : BaseActivity() {
|
||||||
contentResolver.openInputStream(uri).use { inputStream ->
|
contentResolver.openInputStream(uri).use { inputStream ->
|
||||||
targetFile.outputStream().use { fileOut ->
|
targetFile.outputStream().use { fileOut ->
|
||||||
inputStream?.copyTo(fileOut)
|
inputStream?.copyTo(fileOut)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return targetFile.path
|
return targetFile.path
|
||||||
|
@ -153,19 +185,12 @@ class UserAssetActivity : BaseActivity() {
|
||||||
}.also { cursor.close() }
|
}.also { cursor.close() }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to get cursor name", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun importAssetFromQRcode(): Boolean {
|
private fun importAssetFromQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe {
|
|
||||||
if (it)
|
|
||||||
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
|
|
||||||
else
|
|
||||||
toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,65 +207,58 @@ class UserAssetActivity : BaseActivity() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Send URL to UserAssetUrlActivity for Processing
|
// Send URL to UserAssetUrlActivity for Processing
|
||||||
startActivity(Intent(this, UserAssetUrlActivity::class.java)
|
startActivity(
|
||||||
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url))
|
Intent(this, UserAssetUrlActivity::class.java)
|
||||||
|
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to import asset from URL", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadGeoFiles() {
|
private fun downloadGeoFiles() {
|
||||||
val dialog = AlertDialog.Builder(this)
|
binding.pbWaiting.show()
|
||||||
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
|
|
||||||
.setCancelable(false)
|
|
||||||
.show()
|
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
|
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
var assets = MmkvManager.decodeAssetUrls()
|
||||||
assets = addBuiltInGeoItems(assets)
|
assets = addBuiltInGeoItems(assets)
|
||||||
|
|
||||||
assets.forEach {
|
var resultCount = 0
|
||||||
//toast(getString(R.string.msg_downloading_content) + it)
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
assets.forEach {
|
||||||
var result = downloadGeo(it.second, 60000, httpPort)
|
try {
|
||||||
if (!result) {
|
var result = downloadGeo(it.second, 15000, httpPort)
|
||||||
result = downloadGeo(it.second, 60000, 0)
|
if (!result) {
|
||||||
}
|
result = downloadGeo(it.second, 15000, 0)
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (result) {
|
|
||||||
toast(getString(R.string.toast_success) + " " + it.second.remarks)
|
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
|
||||||
} else {
|
|
||||||
toast(getString(R.string.toast_failure) + " " + it.second.remarks)
|
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
if (result)
|
||||||
|
resultCount++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (resultCount > 0) {
|
||||||
|
toast(getString(R.string.title_update_config_count, resultCount))
|
||||||
|
refreshData()
|
||||||
|
} else {
|
||||||
|
toast(getString(R.string.toast_failure))
|
||||||
|
}
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
|
||||||
val targetTemp = File(extDir, item.remarks + "_temp")
|
val targetTemp = File(extDir, item.remarks + "_temp")
|
||||||
val target = File(extDir, item.remarks)
|
val target = File(extDir, item.remarks)
|
||||||
var conn: HttpURLConnection? = null
|
Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
|
||||||
//Log.d(AppConfig.ANG_PACKAGE, url)
|
|
||||||
|
|
||||||
|
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
|
||||||
try {
|
try {
|
||||||
conn = if (httpPort == 0) {
|
|
||||||
URL(item.url).openConnection() as HttpURLConnection
|
|
||||||
} else {
|
|
||||||
URL(item.url).openConnection(
|
|
||||||
Proxy(
|
|
||||||
Proxy.Type.HTTP,
|
|
||||||
InetSocketAddress(LOOPBACK, httpPort)
|
|
||||||
)
|
|
||||||
) as HttpURLConnection
|
|
||||||
}
|
|
||||||
conn.connectTimeout = timeout
|
|
||||||
conn.readTimeout = timeout
|
|
||||||
val inputStream = conn.inputStream
|
val inputStream = conn.inputStream
|
||||||
val responseCode = conn.responseCode
|
val responseCode = conn.responseCode
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
@ -252,10 +270,10 @@ class UserAssetActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, Log.getStackTraceString(e))
|
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
conn?.disconnect()
|
conn.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,7 +285,8 @@ class UserAssetActivity : BaseActivity() {
|
||||||
list.add(
|
list.add(
|
||||||
Utils.getUuid() to AssetUrlItem(
|
Utils.getUuid() to AssetUrlItem(
|
||||||
it,
|
it,
|
||||||
AppConfig.GeoUrl + it
|
String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
|
||||||
|
locked = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -279,11 +298,16 @@ class UserAssetActivity : BaseActivity() {
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.recyclerView.adapter?.notifyDataSetChanged()
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun refreshData() {
|
||||||
|
binding.recyclerView.adapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
|
||||||
return UserAssetViewHolder(
|
return UserAssetViewHolder(
|
||||||
|
@ -313,7 +337,7 @@ class UserAssetActivity : BaseActivity() {
|
||||||
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
|
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
|
if (item.second.locked == true) {
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
||||||
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
||||||
} else {
|
} else {
|
||||||
|
@ -349,4 +373,4 @@ class UserAssetActivity : BaseActivity() {
|
||||||
|
|
||||||
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
|
||||||
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
RecyclerView.ViewHolder(itemUserAssetBinding.root)
|
||||||
}
|
}
|
|
@ -2,13 +2,16 @@ package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
|
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -21,10 +24,10 @@ class UserAssetUrlActivity : BaseActivity() {
|
||||||
|
|
||||||
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
private var del_config: MenuItem? = null
|
||||||
var save_config: MenuItem? = null
|
private var save_config: MenuItem? = null
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
private val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
|
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -41,6 +44,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
||||||
binding.etRemarks.setText(assetNameQrcode)
|
binding.etRemarks.setText(assetNameQrcode)
|
||||||
binding.etUrl.setText(assetUrlQrcode)
|
binding.etUrl.setText(assetUrlQrcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> clearAsset()
|
else -> clearAsset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +77,11 @@ class UserAssetUrlActivity : BaseActivity() {
|
||||||
// remove file associated with the asset
|
// remove file associated with the asset
|
||||||
val file = extDir.resolve(assetItem.remarks)
|
val file = extDir.resolve(assetItem.remarks)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
file.delete()
|
try {
|
||||||
|
file.delete()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assetId = Utils.getUuid()
|
assetId = Utils.getUuid()
|
||||||
|
@ -101,7 +109,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
MmkvManager.encodeAsset(assetId, assetItem)
|
MmkvManager.encodeAsset(assetId, assetItem)
|
||||||
toast(R.string.toast_success)
|
toastSuccess(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,36 +4,37 @@ import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
object AppManagerUtil {
|
object AppManagerUtil {
|
||||||
private fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
|
/**
|
||||||
val packageManager = ctx.packageManager
|
* Load the list of network applications.
|
||||||
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
*
|
||||||
val apps = ArrayList<AppInfo>()
|
* @param context The context to use.
|
||||||
|
* @return A list of AppInfo objects representing the network applications.
|
||||||
|
*/
|
||||||
|
suspend fun loadNetworkAppList(context: Context): ArrayList<AppInfo> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||||
|
val apps = ArrayList<AppInfo>()
|
||||||
|
|
||||||
for (pkg in packages) {
|
for (pkg in packages) {
|
||||||
val applicationInfo = pkg.applicationInfo ?: continue
|
val applicationInfo = pkg.applicationInfo ?: continue
|
||||||
|
|
||||||
val appName = applicationInfo.loadLabel(packageManager).toString()
|
val appName = applicationInfo.loadLabel(packageManager).toString()
|
||||||
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
|
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
|
||||||
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
|
val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0
|
||||||
|
|
||||||
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
|
||||||
apps.add(appInfo)
|
apps.add(appInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext apps
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps
|
fun getLastUpdateTime(context: Context): Long =
|
||||||
}
|
context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
|
||||||
|
|
||||||
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
|
|
||||||
Observable.unsafeCreate {
|
|
||||||
it.onNext(loadNetworkAppList(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
// val PackageInfo.hasInternetPermission: Boolean
|
|
||||||
// get() {
|
|
||||||
// val permissions = requestedPermissions
|
|
||||||
// return permissions?.any { it == Manifest.permission.INTERNET } ?: false
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
223
V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
Normal file
223
V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
|
import com.v2ray.ang.util.Utils.encode
|
||||||
|
import com.v2ray.ang.util.Utils.urlDecode
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.IDN
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
object HttpUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the domain part of a URL string to its IDN (Punycode, ASCII Compatible Encoding) format.
|
||||||
|
*
|
||||||
|
* For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path".
|
||||||
|
*
|
||||||
|
* @param str The URL string to convert (can contain non-ASCII characters in the domain).
|
||||||
|
* @return The URL string with the domain part converted to ASCII-compatible (Punycode) format.
|
||||||
|
*/
|
||||||
|
fun toIdnUrl(str: String): String {
|
||||||
|
val url = URL(str)
|
||||||
|
val host = url.host
|
||||||
|
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
|
||||||
|
if (host != asciiHost) {
|
||||||
|
return str.replace(host, asciiHost)
|
||||||
|
} else {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Unicode domain name to its IDN (Punycode, ASCII Compatible Encoding) format.
|
||||||
|
* If the input is an IP address or already an ASCII domain, returns the original string.
|
||||||
|
*
|
||||||
|
* @param domain The domain string to convert (can include non-ASCII internationalized characters).
|
||||||
|
* @return The domain in ASCII-compatible (Punycode) format, or the original string if input is an IP or already ASCII.
|
||||||
|
*/
|
||||||
|
fun toIdnDomain(domain: String): String {
|
||||||
|
// Return as is if it's a pure IP address (IPv4 or IPv6)
|
||||||
|
if (Utils.isPureIpAddress(domain)) {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as is if already ASCII (English domain or already punycode)
|
||||||
|
if (domain.all { it.code < 128 }) {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, convert to ASCII using IDN
|
||||||
|
return IDN.toASCII(domain, IDN.ALLOW_UNASSIGNED)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a hostname to an IP address, returns original input if it's already an IP
|
||||||
|
*
|
||||||
|
* @param host The hostname or IP address to resolve
|
||||||
|
* @param ipv6Preferred Whether to prefer IPv6 addresses, defaults to false
|
||||||
|
* @return The resolved IP address or the original input (if it's already an IP or resolution fails)
|
||||||
|
*/
|
||||||
|
fun resolveHostToIP(host: String, ipv6Preferred: Boolean = false): List<String>? {
|
||||||
|
try {
|
||||||
|
// If it's already an IP address, return it as a list
|
||||||
|
if (Utils.isPureIpAddress(host)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all IP addresses
|
||||||
|
val addresses = InetAddress.getAllByName(host)
|
||||||
|
if (addresses.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort addresses based on preference
|
||||||
|
val sortedAddresses = if (ipv6Preferred) {
|
||||||
|
addresses.sortedWith(compareByDescending { it is Inet6Address })
|
||||||
|
} else {
|
||||||
|
addresses.sortedWith(compareBy { it is Inet6Address })
|
||||||
|
}
|
||||||
|
|
||||||
|
val ipList = sortedAddresses.mapNotNull { it.hostAddress }
|
||||||
|
|
||||||
|
Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}")
|
||||||
|
|
||||||
|
return ipList
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to resolve host to IP", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the content of a URL as a string.
|
||||||
|
*
|
||||||
|
* @param url The URL to fetch content from.
|
||||||
|
* @param timeout The timeout value in milliseconds.
|
||||||
|
* @param httpPort The HTTP port to use.
|
||||||
|
* @return The content of the URL as a string.
|
||||||
|
*/
|
||||||
|
fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? {
|
||||||
|
val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null
|
||||||
|
try {
|
||||||
|
return conn.inputStream.bufferedReader().readText()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the content of a URL as a string with a custom User-Agent header.
|
||||||
|
*
|
||||||
|
* @param url The URL to fetch content from.
|
||||||
|
* @param timeout The timeout value in milliseconds.
|
||||||
|
* @param httpPort The HTTP port to use.
|
||||||
|
* @return The content of the URL as a string.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getUrlContentWithUserAgent(url: String?, timeout: Int = 15000, httpPort: Int = 0): String {
|
||||||
|
var currentUrl = url
|
||||||
|
var redirects = 0
|
||||||
|
val maxRedirects = 3
|
||||||
|
|
||||||
|
while (redirects++ < maxRedirects) {
|
||||||
|
if (currentUrl == null) continue
|
||||||
|
val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue
|
||||||
|
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
||||||
|
conn.connect()
|
||||||
|
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
when (responseCode) {
|
||||||
|
in 300..399 -> {
|
||||||
|
val location = conn.getHeaderField("Location")
|
||||||
|
conn.disconnect()
|
||||||
|
if (location.isNullOrEmpty()) {
|
||||||
|
throw IOException("Redirect location not found")
|
||||||
|
}
|
||||||
|
currentUrl = location
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> try {
|
||||||
|
return conn.inputStream.use { it.bufferedReader().readText() }
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IOException("Too many redirects")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an HttpURLConnection object connected through a proxy.
|
||||||
|
*
|
||||||
|
* @param urlStr The target URL address.
|
||||||
|
* @param port The port of the proxy server.
|
||||||
|
* @param connectTimeout The connection timeout in milliseconds (default is 15000 ms).
|
||||||
|
* @param readTimeout The read timeout in milliseconds (default is 15000 ms).
|
||||||
|
* @param needStream Whether the connection needs to support streaming.
|
||||||
|
* @return Returns a configured HttpURLConnection object, or null if it fails.
|
||||||
|
*/
|
||||||
|
fun createProxyConnection(
|
||||||
|
urlStr: String,
|
||||||
|
port: Int,
|
||||||
|
connectTimeout: Int = 15000,
|
||||||
|
readTimeout: Int = 15000,
|
||||||
|
needStream: Boolean = false
|
||||||
|
): HttpURLConnection? {
|
||||||
|
|
||||||
|
var conn: HttpURLConnection? = null
|
||||||
|
try {
|
||||||
|
val url = URL(urlStr)
|
||||||
|
// Create a connection
|
||||||
|
conn = if (port == 0) {
|
||||||
|
url.openConnection()
|
||||||
|
} else {
|
||||||
|
url.openConnection(
|
||||||
|
Proxy(
|
||||||
|
Proxy.Type.HTTP,
|
||||||
|
InetSocketAddress(LOOPBACK, port)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} as HttpURLConnection
|
||||||
|
|
||||||
|
// Set connection and read timeouts
|
||||||
|
conn.connectTimeout = connectTimeout
|
||||||
|
conn.readTimeout = readTimeout
|
||||||
|
if (!needStream) {
|
||||||
|
// Set request headers
|
||||||
|
conn.setRequestProperty("Connection", "close")
|
||||||
|
// Disable automatic redirects
|
||||||
|
conn.instanceFollowRedirects = false
|
||||||
|
// Disable caching
|
||||||
|
conn.useCaches = false
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add Basic Authorization
|
||||||
|
url.userInfo?.let {
|
||||||
|
conn.setRequestProperty(
|
||||||
|
"Authorization",
|
||||||
|
"Basic ${encode(urlDecode(it))}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to create proxy connection", e)
|
||||||
|
// If an exception occurs, close the connection and return null
|
||||||
|
conn?.disconnect()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
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.JsonObject
|
||||||
|
@ -8,19 +9,39 @@ 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
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
object JsonUtil {
|
object JsonUtil {
|
||||||
private var gson = Gson()
|
private var gson = Gson()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an object to its JSON representation.
|
||||||
|
*
|
||||||
|
* @param src The object to convert.
|
||||||
|
* @return The JSON representation of the object.
|
||||||
|
*/
|
||||||
fun toJson(src: Any?): String {
|
fun toJson(src: Any?): String {
|
||||||
return gson.toJson(src)
|
return gson.toJson(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a JSON string into an object of the specified class.
|
||||||
|
*
|
||||||
|
* @param src The JSON string to parse.
|
||||||
|
* @param cls The class of the object to parse into.
|
||||||
|
* @return The parsed object.
|
||||||
|
*/
|
||||||
fun <T> fromJson(src: String, cls: Class<T>): T {
|
fun <T> fromJson(src: String, cls: Class<T>): T {
|
||||||
return gson.fromJson(src, cls)
|
return gson.fromJson(src, cls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an object to its pretty-printed JSON representation.
|
||||||
|
*
|
||||||
|
* @param src The object to convert.
|
||||||
|
* @return The pretty-printed JSON representation of the object, or null if the object is null.
|
||||||
|
*/
|
||||||
fun toJsonPretty(src: Any?): String? {
|
fun toJsonPretty(src: Any?): String? {
|
||||||
if (src == null)
|
if (src == null)
|
||||||
return null
|
return null
|
||||||
|
@ -39,13 +60,19 @@ object JsonUtil {
|
||||||
return gsonPre.toJson(src)
|
return gsonPre.toJson(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a JSON string into a JsonObject.
|
||||||
|
*
|
||||||
|
* @param src The JSON string to parse.
|
||||||
|
* @return The parsed JsonObject, or null if parsing fails.
|
||||||
|
*/
|
||||||
fun parseString(src: String?): JsonObject? {
|
fun parseString(src: String?): JsonObject? {
|
||||||
if (src == null)
|
if (src == null)
|
||||||
return null
|
return null
|
||||||
try {
|
try {
|
||||||
return JsonParser.parseString(src).getAsJsonObject()
|
return JsonParser.parseString(src).getAsJsonObject()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to parse JSON string", e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,43 @@ package com.v2ray.ang.util
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.service.V2RayTestService
|
import com.v2ray.ang.service.V2RayTestService
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
|
|
||||||
object MessageUtil {
|
object MessageUtil {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the service.
|
||||||
|
*
|
||||||
|
* @param ctx The context.
|
||||||
|
* @param what The message identifier.
|
||||||
|
* @param content The message content.
|
||||||
|
*/
|
||||||
fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) {
|
fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) {
|
||||||
sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
|
sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the UI.
|
||||||
|
*
|
||||||
|
* @param ctx The context.
|
||||||
|
* @param what The message identifier.
|
||||||
|
* @param content The message content.
|
||||||
|
*/
|
||||||
fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
|
fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
|
||||||
sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
|
sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the test service.
|
||||||
|
*
|
||||||
|
* @param ctx The context.
|
||||||
|
* @param what The message identifier.
|
||||||
|
* @param content The message content.
|
||||||
|
*/
|
||||||
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
|
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
|
||||||
try {
|
try {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
|
@ -26,10 +48,18 @@ object MessageUtil {
|
||||||
intent.putExtra("content", content)
|
intent.putExtra("content", content)
|
||||||
ctx.startService(intent)
|
ctx.startService(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to send message to test service", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message with the specified action.
|
||||||
|
*
|
||||||
|
* @param ctx The context.
|
||||||
|
* @param action The action string.
|
||||||
|
* @param what The message identifier.
|
||||||
|
* @param content The message content.
|
||||||
|
*/
|
||||||
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
|
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
|
||||||
try {
|
try {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
|
@ -39,7 +69,7 @@ object MessageUtil {
|
||||||
intent.putExtra("content", content)
|
intent.putExtra("content", content)
|
||||||
ctx.sendBroadcast(intent)
|
ctx.sendBroadcast(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to send message with action: $action", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,18 @@ import java.util.Locale
|
||||||
|
|
||||||
open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
|
open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Wraps the context with a new locale.
|
||||||
|
*
|
||||||
|
* @param context The original context.
|
||||||
|
* @param newLocale The new locale to set.
|
||||||
|
* @return A ContextWrapper with the new locale.
|
||||||
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun wrap(context: Context, newLocale: Locale?): ContextWrapper {
|
fun wrap(context: Context, newLocale: Locale?): ContextWrapper {
|
||||||
var mContext = context
|
var mContext = context
|
||||||
val res: Resources = mContext.resources
|
val res: Resources = mContext.resources
|
||||||
val configuration: Configuration = res.configuration
|
val configuration: Configuration = res.configuration
|
||||||
//注意 Android 7.0 前后的不同处理方法
|
|
||||||
mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
configuration.setLocale(newLocale)
|
configuration.setLocale(newLocale)
|
||||||
val localeList = LocaleList(newLocale)
|
val localeList = LocaleList(newLocale)
|
||||||
|
|
|
@ -3,53 +3,80 @@ package com.v2ray.ang.util
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.fmt.Hysteria2Fmt
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.service.ProcessService
|
import com.v2ray.ang.service.ProcessService
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object PluginUtil {
|
object PluginUtil {
|
||||||
//private const val HYSTERIA2 = "hysteria2-plugin"
|
|
||||||
private const val HYSTERIA2 = "libhysteria2.so"
|
private const val HYSTERIA2 = "libhysteria2.so"
|
||||||
private const val TAG = ANG_PACKAGE
|
|
||||||
private val procService: ProcessService by lazy {
|
private val procService: ProcessService by lazy {
|
||||||
ProcessService()
|
ProcessService()
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun initPlugin(name: String): PluginManager.InitResult {
|
/**
|
||||||
// return PluginManager.init(name)!!
|
* Run the plugin based on the provided configuration.
|
||||||
// }
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param config The profile configuration.
|
||||||
|
* @param socksPort The port information.
|
||||||
|
*/
|
||||||
|
fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
|
||||||
|
Log.i(AppConfig.TAG, "Starting plugin execution")
|
||||||
|
|
||||||
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
|
if (config == null) {
|
||||||
Log.d(TAG, "runPlugin")
|
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
try {
|
||||||
val configFile = genConfigHy2(context, config, domainPort) ?: return
|
if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
val cmd = genCmdHy2(context, configFile)
|
if (socksPort == null) {
|
||||||
|
Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
|
||||||
|
val configFile = genConfigHy2(context, config, socksPort) ?: return
|
||||||
|
val cmd = genCmdHy2(context, configFile)
|
||||||
|
|
||||||
procService.runProcess(context, cmd)
|
procService.runProcess(context, cmd)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error running plugin", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the running plugin.
|
||||||
|
*/
|
||||||
fun stopPlugin() {
|
fun stopPlugin() {
|
||||||
stopHy2()
|
stopHy2()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a real ping using Hysteria2.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param config The profile configuration.
|
||||||
|
* @return The ping delay in milliseconds, or -1 if it fails.
|
||||||
|
*/
|
||||||
fun realPingHy2(context: Context, config: ProfileItem?): Long {
|
fun realPingHy2(context: Context, config: ProfileItem?): Long {
|
||||||
Log.d(TAG, "realPingHy2")
|
Log.i(AppConfig.TAG, "realPingHy2")
|
||||||
val retFailure = -1L
|
val retFailure = -1L
|
||||||
|
|
||||||
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
||||||
val socksPort = Utils.findFreePort(listOf(0))
|
val socksPort = Utils.findFreePort(listOf(0))
|
||||||
val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure
|
val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure
|
||||||
val cmd = genCmdHy2(context, configFile)
|
val cmd = genCmdHy2(context, configFile)
|
||||||
|
|
||||||
val proc = ProcessService()
|
val proc = ProcessService()
|
||||||
proc.runProcess(context, cmd)
|
proc.runProcess(context, cmd)
|
||||||
Thread.sleep(1000L)
|
Thread.sleep(1000L)
|
||||||
val delay = SpeedtestUtil.testConnection(context, socksPort)
|
val delay = SpeedtestManager.testConnection(context, socksPort)
|
||||||
proc.stopProcess()
|
proc.stopProcess()
|
||||||
|
|
||||||
return delay.first
|
return delay.first
|
||||||
|
@ -57,27 +84,39 @@ object PluginUtil {
|
||||||
return retFailure
|
return retFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
|
/**
|
||||||
Log.d(TAG, "runPlugin $HYSTERIA2")
|
* Generate the configuration file for Hysteria2.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param config The profile configuration.
|
||||||
|
* @param socksPort The port information.
|
||||||
|
* @return The generated configuration file.
|
||||||
|
*/
|
||||||
|
private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? {
|
||||||
|
Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
|
||||||
|
|
||||||
val socksPort = domainPort?.split(":")?.last()
|
|
||||||
.let { if (it.isNullOrEmpty()) return null else it.toInt() }
|
|
||||||
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
|
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
|
||||||
|
|
||||||
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
|
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
|
||||||
Log.d(TAG, "runPlugin ${configFile.absolutePath}")
|
Log.i(AppConfig.TAG, "runPlugin ${configFile.absolutePath}")
|
||||||
|
|
||||||
configFile.parentFile?.mkdirs()
|
configFile.parentFile?.mkdirs()
|
||||||
configFile.writeText(JsonUtil.toJson(hy2Config))
|
configFile.writeText(JsonUtil.toJson(hy2Config))
|
||||||
Log.d(TAG, JsonUtil.toJson(hy2Config))
|
Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config))
|
||||||
|
|
||||||
return configFile
|
return configFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the command to run Hysteria2.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param configFile The configuration file.
|
||||||
|
* @return The command to run Hysteria2.
|
||||||
|
*/
|
||||||
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
|
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
|
||||||
return mutableListOf(
|
return mutableListOf(
|
||||||
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
|
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
|
||||||
//initPlugin(HYSTERIA2).path,
|
|
||||||
"--disable-update-check",
|
"--disable-update-check",
|
||||||
"--config",
|
"--config",
|
||||||
configFile.absolutePath,
|
configFile.absolutePath,
|
||||||
|
@ -87,12 +126,15 @@ object PluginUtil {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the Hysteria2 process.
|
||||||
|
*/
|
||||||
private fun stopHy2() {
|
private fun stopHy2() {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "$HYSTERIA2 destroy")
|
Log.i(AppConfig.TAG, "$HYSTERIA2 destroy")
|
||||||
procService?.stopProcess()
|
procService?.stopProcess()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(TAG, e.toString())
|
Log.e(AppConfig.TAG, "Failed to stop Hysteria2 process", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,17 @@ import com.google.zxing.qrcode.QRCodeWriter
|
||||||
import java.util.EnumMap
|
import java.util.EnumMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 描述:解析二维码图片
|
* QR code decoder utility.
|
||||||
*/
|
*/
|
||||||
object QRCodeDecoder {
|
object QRCodeDecoder {
|
||||||
val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java)
|
val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create qrcode using zxing
|
* Creates a QR code bitmap from the given text.
|
||||||
|
*
|
||||||
|
* @param text The text to encode in the QR code.
|
||||||
|
* @param size The size of the QR code bitmap.
|
||||||
|
* @return The generated QR code bitmap, or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
fun createQRCode(text: String, size: Int = 800): Bitmap? {
|
fun createQRCode(text: String, size: Int = 800): Bitmap? {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
|
@ -35,22 +39,21 @@ object QRCodeDecoder {
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
* Decodes a QR code from a local image file. This method is time-consuming and should be called in a background thread.
|
||||||
*
|
*
|
||||||
* @param picturePath 要解析的二维码图片本地路径
|
* @param picturePath The local path of the image file to decode.
|
||||||
* @return 返回二维码图片里的内容 或 null
|
* @return The content of the QR code, or null if decoding fails.
|
||||||
*/
|
*/
|
||||||
fun syncDecodeQRCode(picturePath: String): String? {
|
fun syncDecodeQRCode(picturePath: String): String? {
|
||||||
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath))
|
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。
|
* Decodes a QR code from a bitmap. This method is time-consuming and should be called in a background thread.
|
||||||
*
|
*
|
||||||
* @param bitmap 要解析的二维码图片
|
* @param bitmap The bitmap to decode.
|
||||||
* @return 返回二维码图片里的内容 或 null
|
* @return The content of the QR code, or null if decoding fails.
|
||||||
*/
|
*/
|
||||||
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
||||||
return bitmap?.let {
|
return bitmap?.let {
|
||||||
|
@ -70,12 +73,11 @@ object QRCodeDecoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
|
* Converts a local image file to a bitmap that can be decoded as a QR code. The image is compressed to avoid being too large.
|
||||||
*
|
*
|
||||||
* @param picturePath 本地图片文件路径
|
* @param picturePath The local path of the image file.
|
||||||
* @return
|
* @return The decoded bitmap, or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
private fun getDecodeAbleBitmap(picturePath: String): Bitmap? {
|
private fun getDecodeAbleBitmap(picturePath: String): Bitmap? {
|
||||||
return try {
|
return try {
|
||||||
|
|
|
@ -4,10 +4,8 @@ import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.LocaleList
|
import android.os.LocaleList
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
@ -16,72 +14,79 @@ import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.AppConfig.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.dto.Language
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.*
|
import java.net.InetAddress
|
||||||
import java.util.*
|
import java.net.ServerSocket
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
object Utils {
|
object Utils {
|
||||||
|
|
||||||
|
private val IPV4_REGEX =
|
||||||
|
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
||||||
|
private val IPV6_REGEX = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert string to editalbe for kotlin
|
* Convert string to editable for Kotlin.
|
||||||
*
|
*
|
||||||
* @param text
|
* @param text The string to convert.
|
||||||
* @return
|
* @return An Editable instance containing the text.
|
||||||
*/
|
*/
|
||||||
fun getEditable(text: String?): Editable {
|
fun getEditable(text: String?): Editable {
|
||||||
return Editable.Factory.getInstance().newEditable(text.orEmpty())
|
return Editable.Factory.getInstance().newEditable(text.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* find value in array position
|
* Find the position of a value in an array.
|
||||||
|
*
|
||||||
|
* @param array The array to search.
|
||||||
|
* @param value The value to find.
|
||||||
|
* @return The index of the value in the array, or -1 if not found.
|
||||||
*/
|
*/
|
||||||
fun arrayFind(array: Array<out String>, value: String): Int {
|
fun arrayFind(array: Array<out String>, value: String): Int {
|
||||||
for (i in array.indices) {
|
return array.indexOf(value)
|
||||||
if (array[i] == value) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* parseInt
|
* Parse a string to an integer with a default value.
|
||||||
|
*
|
||||||
|
* @param str The string to parse.
|
||||||
|
* @param default The default value if parsing fails.
|
||||||
|
* @return The parsed integer, or the default value if parsing fails.
|
||||||
*/
|
*/
|
||||||
fun parseInt(str: String): Int {
|
fun parseInt(str: String?, default: Int = 0): Int {
|
||||||
return parseInt(str, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseInt(str: String?, default: Int): Int {
|
|
||||||
return str?.toIntOrNull() ?: default
|
return str?.toIntOrNull() ?: default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get text from clipboard
|
* Get text from the clipboard.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @return The text from the clipboard, or an empty string if an error occurs.
|
||||||
*/
|
*/
|
||||||
fun getClipboard(context: Context): String {
|
fun getClipboard(context: Context): String {
|
||||||
return try {
|
return try {
|
||||||
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
cmb.primaryClip?.getItemAt(0)?.text.toString()
|
cmb.primaryClip?.getItemAt(0)?.text.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set text to clipboard
|
* Set text to the clipboard.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param content The text to set to the clipboard.
|
||||||
*/
|
*/
|
||||||
fun setClipboard(context: Context, content: String) {
|
fun setClipboard(context: Context, content: String) {
|
||||||
try {
|
try {
|
||||||
|
@ -89,385 +94,368 @@ object Utils {
|
||||||
val clipData = ClipData.newPlainText(null, content)
|
val clipData = ClipData.newPlainText(null, content)
|
||||||
cmb.setPrimaryClip(clipData)
|
cmb.setPrimaryClip(clipData)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to set clipboard content", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* base64 decode
|
* Decode a base64 encoded string.
|
||||||
|
*
|
||||||
|
* @param text The base64 encoded string.
|
||||||
|
* @return The decoded string, or an empty string if decoding fails.
|
||||||
*/
|
*/
|
||||||
fun decode(text: String?): String {
|
fun decode(text: String?): String {
|
||||||
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
|
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to decode a base64 encoded string.
|
||||||
|
*
|
||||||
|
* @param text The base64 encoded string.
|
||||||
|
* @return The decoded string, or null if decoding fails.
|
||||||
|
*/
|
||||||
fun tryDecodeBase64(text: String?): String? {
|
fun tryDecodeBase64(text: String?): String? {
|
||||||
|
if (text.isNullOrEmpty()) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.i(ANG_PACKAGE, "Parse base64 standard failed $e")
|
Log.e(AppConfig.TAG, "Failed to decode standard base64", e)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
|
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e")
|
Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* base64 encode
|
* Encode a string to base64.
|
||||||
|
*
|
||||||
|
* @param text The string to encode.
|
||||||
|
* @return The base64 encoded string, or an empty string if encoding fails.
|
||||||
*/
|
*/
|
||||||
fun encode(text: String): String {
|
fun encode(text: String): String {
|
||||||
return try {
|
return try {
|
||||||
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to encode text to base64", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get remote dns servers from preference
|
* Check if a string is a valid IP address.
|
||||||
*/
|
*
|
||||||
fun getRemoteDnsServers(): List<String> {
|
* @param value The string to check.
|
||||||
val remoteDns =
|
* @return True if the string is a valid IP address, false otherwise.
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
|
||||||
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
|
||||||
if (ret.isEmpty()) {
|
|
||||||
return listOf(AppConfig.DNS_PROXY)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getVpnDnsServers(): List<String> {
|
|
||||||
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
|
|
||||||
return vpnDns.split(",").filter { isPureIpAddress(it) }
|
|
||||||
// allow empty, in that case dns will use system default
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get remote dns servers from preference
|
|
||||||
*/
|
|
||||||
fun getDomesticDnsServers(): List<String> {
|
|
||||||
val domesticDns =
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
|
|
||||||
val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
|
||||||
if (ret.isEmpty()) {
|
|
||||||
return listOf(AppConfig.DNS_DIRECT)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is ip address
|
|
||||||
*/
|
*/
|
||||||
fun isIpAddress(value: String?): Boolean {
|
fun isIpAddress(value: String?): Boolean {
|
||||||
|
if (value.isNullOrEmpty()) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (value.isNullOrEmpty()) {
|
var addr = value.trim()
|
||||||
return false
|
if (addr.isEmpty()) return false
|
||||||
}
|
|
||||||
var addr = value
|
|
||||||
if (addr.isEmpty() || addr.isBlank()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
//CIDR
|
//CIDR
|
||||||
if (addr.indexOf("/") > 0) {
|
if (addr.contains("/")) {
|
||||||
val arr = addr.split("/")
|
val arr = addr.split("/")
|
||||||
if (arr.count() == 2 && Integer.parseInt(arr[1]) > -1) {
|
if (arr.size == 2 && arr[1].toIntOrNull() != null && arr[1].toInt() > -1) {
|
||||||
addr = arr[0]
|
addr = arr[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "::ffff:192.168.173.22"
|
// Handle IPv4-mapped IPv6 addresses
|
||||||
// "[::ffff:192.168.173.22]:80"
|
|
||||||
if (addr.startsWith("::ffff:") && '.' in addr) {
|
if (addr.startsWith("::ffff:") && '.' in addr) {
|
||||||
addr = addr.drop(7)
|
addr = addr.drop(7)
|
||||||
} else if (addr.startsWith("[::ffff:") && '.' in addr) {
|
} else if (addr.startsWith("[::ffff:") && '.' in addr) {
|
||||||
addr = addr.drop(8).replace("]", "")
|
addr = addr.drop(8).replace("]", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// addr = addr.toLowerCase()
|
val octets = addr.split('.')
|
||||||
val octets = addr.split('.').toTypedArray()
|
|
||||||
if (octets.size == 4) {
|
if (octets.size == 4) {
|
||||||
if (octets[3].indexOf(":") > 0) {
|
if (octets[3].contains(":")) {
|
||||||
addr = addr.substring(0, addr.indexOf(":"))
|
addr = addr.substring(0, addr.indexOf(":"))
|
||||||
}
|
}
|
||||||
return isIpv4Address(addr)
|
return isIpv4Address(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ipv6addr [2001:abc::123]:8080
|
|
||||||
return isIpv6Address(addr)
|
return isIpv6Address(addr)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to validate IP address", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a pure IP address (IPv4 or IPv6).
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a pure IP address, false otherwise.
|
||||||
|
*/
|
||||||
fun isPureIpAddress(value: String): Boolean {
|
fun isPureIpAddress(value: String): Boolean {
|
||||||
return isIpv4Address(value) || isIpv6Address(value)
|
return isIpv4Address(value) || isIpv6Address(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isIpv4Address(value: String): Boolean {
|
/**
|
||||||
val regV4 =
|
* Check if a string is a valid domain name.
|
||||||
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
*
|
||||||
return regV4.matches(value)
|
* A valid domain name must not be an IP address and must be a valid URL format.
|
||||||
}
|
*
|
||||||
|
* @param input The string to check.
|
||||||
|
* @return True if the string is a valid domain name, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isDomainName(input: String?): Boolean {
|
||||||
|
if (input.isNullOrEmpty()) return false
|
||||||
|
|
||||||
fun isIpv6Address(value: String): Boolean {
|
// Must not be an IP address and must be a valid URL format
|
||||||
var addr = value
|
return !isPureIpAddress(input) && isValidUrl(input)
|
||||||
if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) {
|
|
||||||
addr = addr.drop(1)
|
|
||||||
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
|
|
||||||
}
|
|
||||||
val regV6 =
|
|
||||||
Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
|
|
||||||
return regV6.matches(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isCoreDNSAddress(s: String): Boolean {
|
|
||||||
return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") || s == "localhost"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* is valid url
|
* Check if a string is a valid IPv4 address.
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a valid IPv4 address, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isIpv4Address(value: String): Boolean {
|
||||||
|
return IPV4_REGEX.matches(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid IPv6 address.
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a valid IPv6 address, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isIpv6Address(value: String): Boolean {
|
||||||
|
var addr = value
|
||||||
|
if (addr.startsWith("[") && addr.endsWith("]")) {
|
||||||
|
addr = addr.drop(1).dropLast(1)
|
||||||
|
}
|
||||||
|
return IPV6_REGEX.matches(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a CoreDNS address.
|
||||||
|
*
|
||||||
|
* @param s The string to check.
|
||||||
|
* @return True if the string is a CoreDNS address, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isCoreDNSAddress(s: String): Boolean {
|
||||||
|
return s.startsWith("https") ||
|
||||||
|
s.startsWith("tcp") ||
|
||||||
|
s.startsWith("quic") ||
|
||||||
|
s == "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid URL.
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a valid URL, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun isValidUrl(value: String?): Boolean {
|
fun isValidUrl(value: String?): Boolean {
|
||||||
try {
|
if (value.isNullOrEmpty()) return false
|
||||||
if (value.isNullOrEmpty()) {
|
|
||||||
return false
|
return try {
|
||||||
}
|
Patterns.WEB_URL.matcher(value).matches() ||
|
||||||
if (Patterns.WEB_URL.matcher(value).matches()
|
Patterns.DOMAIN_NAME.matcher(value).matches() ||
|
||||||
|| Patterns.DOMAIN_NAME.matcher(value).matches()
|
URLUtil.isValidUrl(value)
|
||||||
|| URLUtil.isValidUrl(value)
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to validate URL", e)
|
||||||
return false
|
false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startVServiceFromToggle(context: Context): Boolean {
|
|
||||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
|
||||||
context.toast(R.string.app_tile_first_use)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
V2RayServiceManager.startV2Ray(context)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* stopVService
|
* Open a URI in a browser.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param uriString The URI string to open.
|
||||||
*/
|
*/
|
||||||
fun stopVService(context: Context) {
|
|
||||||
context.toast(R.string.toast_services_stop)
|
|
||||||
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openUri(context: Context, uriString: String) {
|
fun openUri(context: Context, uriString: String) {
|
||||||
val uri = Uri.parse(uriString)
|
try {
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
val uri = uriString.toUri()
|
||||||
|
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to open URI", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* uuid
|
* Generate a UUID.
|
||||||
|
*
|
||||||
|
* @return A UUID string without dashes.
|
||||||
*/
|
*/
|
||||||
fun getUuid(): String {
|
fun getUuid(): String {
|
||||||
return try {
|
return try {
|
||||||
UUID.randomUUID().toString().replace("-", "")
|
UUID.randomUUID().toString().replace("-", "")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to generate UUID", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a URL-encoded string.
|
||||||
|
*
|
||||||
|
* @param url The URL-encoded string.
|
||||||
|
* @return The decoded string, or the original string if decoding fails.
|
||||||
|
*/
|
||||||
fun urlDecode(url: String): String {
|
fun urlDecode(url: String): String {
|
||||||
return try {
|
return try {
|
||||||
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
URLDecoder.decode(url, Charsets.UTF_8.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to decode URL", e)
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a string to URL-encoded format.
|
||||||
|
*
|
||||||
|
* @param url The string to encode.
|
||||||
|
* @return The URL-encoded string, or the original string if encoding fails.
|
||||||
|
*/
|
||||||
fun urlEncode(url: String): String {
|
fun urlEncode(url: String): String {
|
||||||
return try {
|
return try {
|
||||||
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
|
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to encode URL", e)
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* readTextFromAssets
|
* Read text from an asset file.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @param fileName The name of the asset file.
|
||||||
|
* @return The content of the asset file as a string.
|
||||||
*/
|
*/
|
||||||
fun readTextFromAssets(context: Context?, fileName: String): String {
|
fun readTextFromAssets(context: Context?, fileName: String): String {
|
||||||
if (context == null) {
|
if (context == null) return ""
|
||||||
return ""
|
|
||||||
}
|
|
||||||
val content = context.assets.open(fileName).bufferedReader().use {
|
|
||||||
it.readText()
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
fun userAssetPath(context: Context?): String {
|
return try {
|
||||||
if (context == null)
|
context.assets.open(fileName).use { inputStream ->
|
||||||
return ""
|
inputStream.bufferedReader().use { reader ->
|
||||||
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
|
reader.readText()
|
||||||
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
|
}
|
||||||
return extDir.absolutePath
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun backupPath(context: Context?): String {
|
|
||||||
if (context == null)
|
|
||||||
return ""
|
|
||||||
val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS)
|
|
||||||
?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
|
|
||||||
return extDir.absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDeviceIdForXUDPBaseKey(): String {
|
|
||||||
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
|
||||||
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUrlContext(url: String, timeout: Int): String {
|
|
||||||
var result: String
|
|
||||||
var conn: HttpURLConnection? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
conn = URL(url).openConnection() as HttpURLConnection
|
|
||||||
conn.connectTimeout = timeout
|
|
||||||
conn.readTimeout = timeout
|
|
||||||
conn.setRequestProperty("Connection", "close")
|
|
||||||
conn.instanceFollowRedirects = false
|
|
||||||
conn.useCaches = false
|
|
||||||
//val code = conn.responseCode
|
|
||||||
result = conn.inputStream.bufferedReader().readText()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
result = ""
|
Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
|
||||||
} finally {
|
""
|
||||||
conn?.disconnect()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getUrlContentWithCustomUserAgent(
|
|
||||||
urlStr: String?,
|
|
||||||
timeout: Int = 30000,
|
|
||||||
httpPort: Int = 0
|
|
||||||
): String {
|
|
||||||
val url = URL(urlStr)
|
|
||||||
val conn = if (httpPort == 0) {
|
|
||||||
url.openConnection()
|
|
||||||
} else {
|
|
||||||
url.openConnection(
|
|
||||||
Proxy(
|
|
||||||
Proxy.Type.HTTP,
|
|
||||||
InetSocketAddress(LOOPBACK, httpPort)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
conn.connectTimeout = timeout
|
|
||||||
conn.readTimeout = timeout
|
|
||||||
conn.setRequestProperty("Connection", "close")
|
|
||||||
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
|
||||||
url.userInfo?.let {
|
|
||||||
conn.setRequestProperty(
|
|
||||||
"Authorization",
|
|
||||||
"Basic ${encode(urlDecode(it))}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
conn.useCaches = false
|
|
||||||
return conn.inputStream.use {
|
|
||||||
it.bufferedReader().readText()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the user asset directory.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @return The path to the user asset directory.
|
||||||
|
*/
|
||||||
|
fun userAssetPath(context: Context?): String {
|
||||||
|
if (context == null) return ""
|
||||||
|
|
||||||
|
return try {
|
||||||
|
context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
|
||||||
|
?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to get user asset path", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the backup directory.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @return The path to the backup directory.
|
||||||
|
*/
|
||||||
|
fun backupPath(context: Context?): String {
|
||||||
|
if (context == null) return ""
|
||||||
|
|
||||||
|
return try {
|
||||||
|
context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
|
||||||
|
?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to get backup path", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device ID for XUDP base key.
|
||||||
|
*
|
||||||
|
* @return The device ID for XUDP base key.
|
||||||
|
*/
|
||||||
|
fun getDeviceIdForXUDPBaseKey(): String {
|
||||||
|
return try {
|
||||||
|
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
|
||||||
|
Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to generate device ID", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dark mode status.
|
||||||
|
*
|
||||||
|
* @param context The context to use.
|
||||||
|
* @return True if dark mode is enabled, false otherwise.
|
||||||
|
*/
|
||||||
fun getDarkModeStatus(context: Context): Boolean {
|
fun getDarkModeStatus(context: Context): Boolean {
|
||||||
return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
|
return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
fun setNightMode() {
|
* Get the IPv6 address in a formatted string.
|
||||||
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
*
|
||||||
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
* @param address The IPv6 address.
|
||||||
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
* @return The formatted IPv6 address, or the original address if not valid.
|
||||||
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
*/
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIpv6Address(address: String?): String {
|
fun getIpv6Address(address: String?): String {
|
||||||
if (address == null) {
|
if (address.isNullOrEmpty()) return ""
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
|
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
|
||||||
String.format("[%s]", address)
|
"[$address]"
|
||||||
} else {
|
} else {
|
||||||
address
|
address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocale(): Locale {
|
/**
|
||||||
val langCode =
|
* Get the system locale.
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
|
*
|
||||||
val language = Language.fromCode(langCode)
|
* @return The system locale.
|
||||||
|
*/
|
||||||
return when (language) {
|
fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
Language.AUTO -> getSysLocale()
|
|
||||||
Language.ENGLISH -> Locale.ENGLISH
|
|
||||||
Language.CHINA -> Locale.CHINA
|
|
||||||
Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
|
|
||||||
Language.VIETNAMESE -> Locale("vi")
|
|
||||||
Language.RUSSIAN -> Locale("ru")
|
|
||||||
Language.PERSIAN -> Locale("fa")
|
|
||||||
Language.BANGLA -> Locale("bn")
|
|
||||||
Language.BAKHTIARI -> Locale("bqi", "IR")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
LocaleList.getDefault()[0]
|
LocaleList.getDefault()[0]
|
||||||
} else {
|
} else {
|
||||||
Locale.getDefault()
|
Locale.getDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix illegal characters in a URL.
|
||||||
|
*
|
||||||
|
* @param str The URL string.
|
||||||
|
* @return The URL string with illegal characters replaced.
|
||||||
|
*/
|
||||||
fun fixIllegalUrl(str: String): String {
|
fun fixIllegalUrl(str: String): String {
|
||||||
return str
|
return str.replace(" ", "%20")
|
||||||
.replace(" ", "%20")
|
|
||||||
.replace("|", "%7C")
|
.replace("|", "%7C")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeWhiteSpace(str: String?): String? {
|
/**
|
||||||
return str?.replace(" ", "")
|
* Find a free port from a list of ports.
|
||||||
}
|
*
|
||||||
|
* @param ports The list of ports to check.
|
||||||
fun idnToASCII(str: String): String {
|
* @return The first free port found.
|
||||||
val url = URL(str)
|
* @throws IOException If no free port is found.
|
||||||
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file)
|
*/
|
||||||
.toExternalForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTv(context: Context): Boolean =
|
|
||||||
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
|
||||||
|
|
||||||
fun getDelayTestUrl(second: Boolean = false): String {
|
|
||||||
return if (second) {
|
|
||||||
AppConfig.DelayTestUrl2
|
|
||||||
} else {
|
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
|
|
||||||
?: AppConfig.DelayTestUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findFreePort(ports: List<Int>): Int {
|
fun findFreePort(ports: List<Int>): Int {
|
||||||
for (port in ports) {
|
for (port in ports) {
|
||||||
try {
|
try {
|
||||||
|
@ -481,24 +469,102 @@ object Utils {
|
||||||
throw IOException("no free port found")
|
throw IOException("no free port found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid subscription URL.
|
||||||
|
*
|
||||||
|
* @param value The string to check.
|
||||||
|
* @return True if the string is a valid subscription URL, false otherwise.
|
||||||
|
*/
|
||||||
fun isValidSubUrl(value: String?): Boolean {
|
fun isValidSubUrl(value: String?): Boolean {
|
||||||
|
if (value.isNullOrEmpty()) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (value.isNullOrEmpty()) return false
|
|
||||||
if (URLUtil.isHttpsUrl(value)) return true
|
if (URLUtil.isHttpsUrl(value)) return true
|
||||||
if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true
|
if (URLUtil.isHttpUrl(value)) {
|
||||||
|
if (value.contains(LOOPBACK)) return true
|
||||||
|
|
||||||
|
//Check private ip address
|
||||||
|
val uri = URI(fixIllegalUrl(value))
|
||||||
|
if (isIpAddress(uri.host)) {
|
||||||
|
AppConfig.PRIVATE_IP_LIST.forEach {
|
||||||
|
if (isIpInCidr(uri.host, it)) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the receiver flags based on the Android version.
|
||||||
|
*
|
||||||
|
* @return The receiver flags.
|
||||||
|
*/
|
||||||
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
ContextCompat.RECEIVER_EXPORTED
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
} else {
|
} else {
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isXray(): Boolean = (ANG_PACKAGE.startsWith("com.v2ray.ang"))
|
/**
|
||||||
|
* Check if the package is Xray.
|
||||||
|
*
|
||||||
|
* @return True if the package is Xray, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isXray(): Boolean = BuildConfig.APPLICATION_ID.startsWith("com.v2ray.ang")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if it is the Google Play version.
|
||||||
|
*
|
||||||
|
* @return True if the package is Google Play, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isGoogleFlavor(): Boolean = BuildConfig.FLAVOR == "playstore"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an InetAddress to its long representation
|
||||||
|
*
|
||||||
|
* @param ip The InetAddress to convert
|
||||||
|
* @return The long representation of the IP address
|
||||||
|
*/
|
||||||
|
private fun inetAddressToLong(ip: InetAddress): Long {
|
||||||
|
val bytes = ip.address
|
||||||
|
var result: Long = 0
|
||||||
|
for (i in bytes.indices) {
|
||||||
|
result = result shl 8 or (bytes[i].toInt() and 0xff).toLong()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP address is within a CIDR range
|
||||||
|
*
|
||||||
|
* @param ip The IP address to check
|
||||||
|
* @param cidr The CIDR notation range (e.g., "192.168.1.0/24")
|
||||||
|
* @return True if the IP is within the CIDR range, false otherwise
|
||||||
|
*/
|
||||||
|
fun isIpInCidr(ip: String, cidr: String): Boolean {
|
||||||
|
try {
|
||||||
|
if (!isIpAddress(ip)) return false
|
||||||
|
|
||||||
|
// Parse CIDR (e.g., "192.168.1.0/24")
|
||||||
|
val (cidrIp, prefixLen) = cidr.split("/")
|
||||||
|
val prefixLength = prefixLen.toInt()
|
||||||
|
|
||||||
|
// Convert IP and CIDR's IP portion to Long
|
||||||
|
val ipLong = inetAddressToLong(InetAddress.getByName(ip))
|
||||||
|
val cidrIpLong = inetAddressToLong(InetAddress.getByName(cidrIp))
|
||||||
|
|
||||||
|
// Calculate subnet mask (e.g., /24 → 0xFFFFFF00)
|
||||||
|
val mask = if (prefixLength == 0) 0L else (-1L shl (32 - prefixLength))
|
||||||
|
|
||||||
|
// Check if they're in the same subnet
|
||||||
|
return (ipLong and mask) == (cidrIpLong and mask)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
@ -13,6 +15,14 @@ import java.util.zip.ZipOutputStream
|
||||||
object ZipUtil {
|
object ZipUtil {
|
||||||
private const val BUFFER_SIZE = 4096
|
private const val BUFFER_SIZE = 4096
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip the contents of a folder.
|
||||||
|
*
|
||||||
|
* @param folderPath The path to the folder to zip.
|
||||||
|
* @param outputZipFilePath The path to the output zip file.
|
||||||
|
* @return True if the operation is successful, false otherwise.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean {
|
fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean {
|
||||||
val buffer = ByteArray(BUFFER_SIZE)
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
|
@ -53,12 +63,20 @@ object ZipUtil {
|
||||||
zos.closeEntry()
|
zos.closeEntry()
|
||||||
zos.close()
|
zos.close()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to zip folder", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unzip the contents of a zip file to a folder.
|
||||||
|
*
|
||||||
|
* @param zipFile The zip file to unzip.
|
||||||
|
* @param destDirectory The destination directory.
|
||||||
|
* @return True if the operation is successful, false otherwise.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun unzipToFolder(zipFile: File, destDirectory: String): Boolean {
|
fun unzipToFolder(zipFile: File, destDirectory: String): Boolean {
|
||||||
File(destDirectory).run {
|
File(destDirectory).run {
|
||||||
|
@ -72,10 +90,8 @@ object ZipUtil {
|
||||||
zip.getInputStream(entry).use { input ->
|
zip.getInputStream(entry).use { input ->
|
||||||
val filePath = destDirectory + File.separator + entry.name
|
val filePath = destDirectory + File.separator + entry.name
|
||||||
if (!entry.isDirectory) {
|
if (!entry.isDirectory) {
|
||||||
// if the entry is a file, extracts it
|
|
||||||
extractFile(input, filePath)
|
extractFile(input, filePath)
|
||||||
} else {
|
} else {
|
||||||
// if the entry is a directory, make the directory
|
|
||||||
val dir = File(filePath)
|
val dir = File(filePath)
|
||||||
dir.mkdir()
|
dir.mkdir()
|
||||||
}
|
}
|
||||||
|
@ -83,12 +99,19 @@ object ZipUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.TAG, "Failed to unzip file", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a file from an input stream.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream to read from.
|
||||||
|
* @param destFilePath The destination file path.
|
||||||
|
* @throws IOException If an I/O error occurs.
|
||||||
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun extractFile(inputStream: InputStream, destFilePath: String) {
|
private fun extractFile(inputStream: InputStream, destFilePath: String) {
|
||||||
val bos = BufferedOutputStream(FileOutputStream(destFilePath))
|
val bos = BufferedOutputStream(FileOutputStream(destFilePath))
|
||||||
|
|
|
@ -13,18 +13,17 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.ServersCache
|
import com.v2ray.ang.dto.ServersCache
|
||||||
import com.v2ray.ang.extension.serializable
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -49,7 +48,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startListenBroadcast() {
|
fun startListenBroadcast() {
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||||
|
@ -57,20 +55,30 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the ViewModel is cleared.
|
||||||
|
*/
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
|
||||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
SpeedtestUtil.closeAllTcpSockets()
|
SpeedtestManager.closeAllTcpSockets()
|
||||||
Log.i(ANG_PACKAGE, "Main ViewModel is cleared")
|
Log.i(AppConfig.TAG, "Main ViewModel is cleared")
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the server list.
|
||||||
|
*/
|
||||||
fun reloadServerList() {
|
fun reloadServerList() {
|
||||||
serverList = MmkvManager.decodeServerList()
|
serverList = MmkvManager.decodeServerList()
|
||||||
updateCache()
|
updateCache()
|
||||||
updateListAction.value = -1
|
updateListAction.value = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a server by its GUID.
|
||||||
|
* @param guid The GUID of the server to remove.
|
||||||
|
*/
|
||||||
fun removeServer(guid: String) {
|
fun removeServer(guid: String) {
|
||||||
serverList.remove(guid)
|
serverList.remove(guid)
|
||||||
MmkvManager.removeServer(guid)
|
MmkvManager.removeServer(guid)
|
||||||
|
@ -80,33 +88,43 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun appendCustomConfigServer(server: String): Boolean {
|
// /**
|
||||||
if (server.contains("inbounds")
|
// * Appends a custom configuration server.
|
||||||
&& server.contains("outbounds")
|
// * @param server The server configuration to append.
|
||||||
&& server.contains("routing")
|
// * @return True if the server was successfully appended, false otherwise.
|
||||||
) {
|
// */
|
||||||
try {
|
// fun appendCustomConfigServer(server: String): Boolean {
|
||||||
val config = CustomFmt.parse(server) ?: return false
|
// if (server.contains("inbounds")
|
||||||
config.subscriptionId = subscriptionId
|
// && server.contains("outbounds")
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
// && server.contains("routing")
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
// ) {
|
||||||
serverList.add(0, key)
|
// try {
|
||||||
// val profile = ProfileLiteItem(
|
// val config = CustomFmt.parse(server) ?: return false
|
||||||
// configType = config.configType,
|
// config.subscriptionId = subscriptionId
|
||||||
// subscriptionId = config.subscriptionId,
|
// val key = MmkvManager.encodeServerConfig("", config)
|
||||||
// remarks = config.remarks,
|
// MmkvManager.encodeServerRaw(key, server)
|
||||||
// server = config.getProxyOutbound()?.getServerAddress(),
|
// serverList.add(0, key)
|
||||||
// serverPort = config.getProxyOutbound()?.getServerPort(),
|
//// val profile = ProfileLiteItem(
|
||||||
// )
|
//// configType = config.configType,
|
||||||
serversCache.add(0, ServersCache(key, config))
|
//// subscriptionId = config.subscriptionId,
|
||||||
return true
|
//// remarks = config.remarks,
|
||||||
} catch (e: Exception) {
|
//// server = config.getProxyOutbound()?.getServerAddress(),
|
||||||
e.printStackTrace()
|
//// serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||||
}
|
//// )
|
||||||
}
|
// serversCache.add(0, ServersCache(key, config))
|
||||||
return false
|
// return true
|
||||||
}
|
// } catch (e: Exception) {
|
||||||
|
// e.printStackTrace()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps the positions of two servers.
|
||||||
|
* @param fromPosition The initial position of the server.
|
||||||
|
* @param toPosition The target position of the server.
|
||||||
|
*/
|
||||||
fun swapServer(fromPosition: Int, toPosition: Int) {
|
fun swapServer(fromPosition: Int, toPosition: Int) {
|
||||||
if (subscriptionId.isEmpty()) {
|
if (subscriptionId.isEmpty()) {
|
||||||
Collections.swap(serverList, fromPosition, toPosition)
|
Collections.swap(serverList, fromPosition, toPosition)
|
||||||
|
@ -119,6 +137,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
MmkvManager.encodeServerList(serverList)
|
MmkvManager.encodeServerList(serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the cache of servers.
|
||||||
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun updateCache() {
|
fun updateCache() {
|
||||||
serversCache.clear()
|
serversCache.clear()
|
||||||
|
@ -141,12 +162,16 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keywordFilter.isEmpty() || profile.remarks.contains(keywordFilter)) {
|
if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) {
|
||||||
serversCache.add(ServersCache(guid, profile))
|
serversCache.add(ServersCache(guid, profile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the configuration via subscription for all servers.
|
||||||
|
* @return The number of updated configurations.
|
||||||
|
*/
|
||||||
fun updateConfigViaSubAll(): Int {
|
fun updateConfigViaSubAll(): Int {
|
||||||
if (subscriptionId.isEmpty()) {
|
if (subscriptionId.isEmpty()) {
|
||||||
return AngConfigManager.updateConfigViaSubAll()
|
return AngConfigManager.updateConfigViaSubAll()
|
||||||
|
@ -156,6 +181,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all servers.
|
||||||
|
* @return The number of exported servers.
|
||||||
|
*/
|
||||||
fun exportAllServer(): Int {
|
fun exportAllServer(): Int {
|
||||||
val serverListCopy =
|
val serverListCopy =
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
|
@ -171,21 +200,22 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the TCP ping for all servers.
|
||||||
|
*/
|
||||||
fun testAllTcping() {
|
fun testAllTcping() {
|
||||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
SpeedtestUtil.closeAllTcpSockets()
|
SpeedtestManager.closeAllTcpSockets()
|
||||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||||
//updateListAction.value = -1 // update all
|
|
||||||
|
|
||||||
val serversCopy = serversCache.toList() // Create a copy of the list
|
val serversCopy = serversCache.toList()
|
||||||
for (item in serversCopy) {
|
for (item in serversCopy) {
|
||||||
item.profile.let { outbound ->
|
item.profile.let { outbound ->
|
||||||
val serverAddress = outbound.server
|
val serverAddress = outbound.server
|
||||||
val serverPort = outbound.serverPort
|
val serverPort = outbound.serverPort
|
||||||
if (serverAddress != null && serverPort != null) {
|
if (serverAddress != null && serverPort != null) {
|
||||||
tcpingTestScope.launch {
|
tcpingTestScope.launch {
|
||||||
val testResult = SpeedtestUtil.tcping(serverAddress, serverPort.toInt())
|
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
||||||
updateListAction.value = getPosition(item.guid)
|
updateListAction.value = getPosition(item.guid)
|
||||||
|
@ -196,23 +226,33 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the real ping for all servers.
|
||||||
|
*/
|
||||||
fun testAllRealPing() {
|
fun testAllRealPing() {
|
||||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
|
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
|
||||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||||
updateListAction.value = -1 // update all
|
updateListAction.value = -1
|
||||||
|
|
||||||
val serversCopy = serversCache.toList() // Create a copy of the list
|
val serversCopy = serversCache.toList()
|
||||||
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
for (item in serversCopy) {
|
for (item in serversCopy) {
|
||||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
|
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the real ping for the current server.
|
||||||
|
*/
|
||||||
fun testCurrentServerRealPing() {
|
fun testCurrentServerRealPing() {
|
||||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
|
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_MEASURE_DELAY, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the subscription ID.
|
||||||
|
* @param id The new subscription ID.
|
||||||
|
*/
|
||||||
fun subscriptionIdChanged(id: String) {
|
fun subscriptionIdChanged(id: String) {
|
||||||
if (subscriptionId != id) {
|
if (subscriptionId != id) {
|
||||||
subscriptionId = id
|
subscriptionId = id
|
||||||
|
@ -221,6 +261,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the subscriptions.
|
||||||
|
* @param context The context.
|
||||||
|
* @return A pair of lists containing the subscription IDs and remarks.
|
||||||
|
*/
|
||||||
fun getSubscriptions(context: Context): Pair<MutableList<String>?, MutableList<String>?> {
|
fun getSubscriptions(context: Context): Pair<MutableList<String>?, MutableList<String>?> {
|
||||||
val subscriptions = MmkvManager.decodeSubscriptions()
|
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
if (subscriptionId.isNotEmpty()
|
if (subscriptionId.isNotEmpty()
|
||||||
|
@ -239,6 +284,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
return listId to listRemarks
|
return listId to listRemarks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the position of a server by its GUID.
|
||||||
|
* @param guid The GUID of the server.
|
||||||
|
* @return The position of the server.
|
||||||
|
*/
|
||||||
fun getPosition(guid: String): Int {
|
fun getPosition(guid: String): Int {
|
||||||
serversCache.forEachIndexed { index, it ->
|
serversCache.forEachIndexed { index, it ->
|
||||||
if (it.guid == guid)
|
if (it.guid == guid)
|
||||||
|
@ -247,6 +297,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes duplicate servers.
|
||||||
|
* @return The number of removed servers.
|
||||||
|
*/
|
||||||
fun removeDuplicateServer(): Int {
|
fun removeDuplicateServer(): Int {
|
||||||
val serversCacheCopy = mutableListOf<Pair<String, ProfileItem>>()
|
val serversCacheCopy = mutableListOf<Pair<String, ProfileItem>>()
|
||||||
for (it in serversCache) {
|
for (it in serversCache) {
|
||||||
|
@ -273,6 +327,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
return deleteServer.count()
|
return deleteServer.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all servers.
|
||||||
|
* @return The number of removed servers.
|
||||||
|
*/
|
||||||
fun removeAllServer(): Int {
|
fun removeAllServer(): Int {
|
||||||
val count =
|
val count =
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
|
@ -287,6 +345,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes invalid servers.
|
||||||
|
* @return The number of removed servers.
|
||||||
|
*/
|
||||||
fun removeInvalidServer(): Int {
|
fun removeInvalidServer(): Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
|
@ -300,6 +362,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts servers by their test results.
|
||||||
|
*/
|
||||||
fun sortByTestResults() {
|
fun sortByTestResults() {
|
||||||
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
||||||
|
|
||||||
|
@ -319,12 +384,20 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
MmkvManager.encodeServerList(serverList)
|
MmkvManager.encodeServerList(serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes assets.
|
||||||
|
* @param assets The asset manager.
|
||||||
|
*/
|
||||||
fun initAssets(assets: AssetManager) {
|
fun initAssets(assets: AssetManager) {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
SettingsManager.initAssets(getApplication<AngApplication>(), assets)
|
SettingsManager.initAssets(getApplication<AngApplication>(), assets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the configuration by a keyword.
|
||||||
|
* @param keyword The keyword to filter by.
|
||||||
|
*/
|
||||||
fun filterConfig(keyword: String) {
|
fun filterConfig(keyword: String) {
|
||||||
if (keyword == keywordFilter) {
|
if (keyword == keywordFilter) {
|
||||||
return
|
return
|
||||||
|
@ -346,12 +419,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START_SUCCESS -> {
|
AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||||
getApplication<AngApplication>().toast(R.string.toast_services_success)
|
getApplication<AngApplication>().toastSuccess(R.string.toast_services_success)
|
||||||
isRunning.value = true
|
isRunning.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START_FAILURE -> {
|
AppConfig.MSG_STATE_START_FAILURE -> {
|
||||||
getApplication<AngApplication>().toast(R.string.toast_services_failure)
|
getApplication<AngApplication>().toastError(R.string.toast_services_failure)
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue