Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cd5fefdca | ||
|
|
25c42c475f | ||
|
|
96e66da071 | ||
|
|
6914b9ee1b | ||
|
|
cfc6546c97 | ||
|
|
0880313659 | ||
|
|
875ca02126 | ||
|
|
c2d5925053 | ||
|
|
547bbf8e95 | ||
|
|
2218251b03 | ||
|
|
4da3a23162 | ||
|
|
28a90baf88 | ||
|
|
7bbdda2f2f | ||
|
|
884b444a41 | ||
|
|
e1ff2df36e | ||
|
|
61c0111778 | ||
|
|
bfc9a64e07 | ||
|
|
2ba92045cc | ||
|
|
aeca9f51c8 | ||
|
|
b107c0ac1d | ||
|
|
65a04b4784 | ||
|
|
eab9f50cfd | ||
|
|
b8bb83b524 | ||
|
|
93eb9fe3b9 | ||
|
|
5f167512f5 | ||
|
|
a727b81263 | ||
|
|
f27c9192d1 | ||
|
|
90153fa17f | ||
|
|
cdfaa01852 | ||
|
|
33d2c3b00d | ||
|
|
b7f992cdc0 | ||
|
|
0a6a24e309 | ||
|
|
153b4cffef | ||
|
|
f09a413232 | ||
|
|
cba58f6ae2 | ||
|
|
2bf4b91488 | ||
|
|
b60b7f4307 | ||
|
|
e4ca04a096 | ||
|
|
d0f7ecec44 | ||
|
|
f488811f01 | ||
|
|
d212cda1e1 | ||
|
|
ba760eac59 | ||
|
|
8549b5ea46 | ||
|
|
0b3c106c6f | ||
|
|
84e7ee4ef3 | ||
|
|
e5d498ea6e | ||
|
|
6da835a2ca | ||
|
|
1f9a71e6ac | ||
|
|
9c92fdc257 | ||
|
|
da219228fa | ||
|
|
c0a6455d08 | ||
|
|
709e2a9ed4 | ||
|
|
c3ac9f01d2 | ||
|
|
65eba3795f | ||
|
|
341cdb5dbe | ||
|
|
4f43c2ce45 | ||
|
|
2ec691fc6b | ||
|
|
81ed321654 | ||
|
|
6ad37c70f1 | ||
|
|
c7ffd6d82d | ||
|
|
ae4b0fd8d3 | ||
|
|
beceaba44d | ||
|
|
f5987d9767 | ||
|
|
616712b338 | ||
|
|
076a968476 | ||
|
|
e60eb703c6 | ||
|
|
cdede639f2 | ||
|
|
23264d71f0 | ||
|
|
9caafc4303 | ||
|
|
48a3690a39 | ||
|
|
b66a8ca44d | ||
|
|
82087e1187 | ||
|
|
fdfd0438c3 | ||
|
|
28027b5288 | ||
|
|
ba54005753 | ||
|
|
c6758b11b5 | ||
|
|
6c29e5e9a4 | ||
|
|
17e0db2ffc | ||
|
|
490ea59499 | ||
|
|
f4db6bcf63 | ||
|
|
90f89de957 | ||
|
|
9612b868f2 | ||
|
|
5f8ea93f36 | ||
|
|
fc132f7282 | ||
|
|
6f9bb6caa7 | ||
|
|
0e5b88de8f | ||
|
|
5b4f51981e | ||
|
|
3dcee45e9f | ||
|
|
3573a3bec3 | ||
|
|
796bad1c1c | ||
|
|
77042f6fae | ||
|
|
013ac308f7 | ||
|
|
d703582f19 | ||
|
|
1db80f740d | ||
|
|
a0d2740280 | ||
|
|
297083f3c4 | ||
|
|
15a4ad978a | ||
|
|
63f4cfac83 | ||
|
|
ca849fb19e | ||
|
|
3de3070ab7 | ||
|
|
cbea4bab7c | ||
|
|
fe8b825c34 | ||
|
|
daa0394960 | ||
|
|
e5aba5d99b | ||
|
|
c4847eb3de | ||
|
|
5a5f911453 | ||
|
|
22cef29c27 | ||
|
|
ef41641680 | ||
|
|
69135e8707 | ||
|
|
5daef71147 | ||
|
|
5ffc5ec502 | ||
|
|
35063db3e6 | ||
|
|
868c24bb8b | ||
|
|
7367baffb8 | ||
|
|
819ff2995a | ||
|
|
3b5d04b717 | ||
|
|
b673cd73ac | ||
|
|
649c1a022b | ||
|
|
034e58bc9d | ||
|
|
a95f280102 | ||
|
|
df8da05f32 | ||
|
|
635581719b | ||
|
|
77d5e203e8 | ||
|
|
370d002b25 | ||
|
|
18f0fe47ff | ||
|
|
cccd6139fc | ||
|
|
1fadca8524 | ||
|
|
af01e2ac06 | ||
|
|
de22e16cd4 | ||
|
|
e073b19343 | ||
|
|
a81a05cd45 | ||
|
|
fc67281d2a | ||
|
|
ece35b9a1c | ||
|
|
ed8fb7fa82 | ||
|
|
3688dd4634 | ||
|
|
7ff445ef55 | ||
|
|
e4847b4c76 | ||
|
|
5f5d8f1d2f | ||
|
|
a45aa489e8 | ||
|
|
9b86ba9e35 | ||
|
|
f5b74ecd78 | ||
|
|
2d0ab355d6 | ||
|
|
e41235f76b | ||
|
|
3045f7000e | ||
|
|
c0c2dfb657 | ||
|
|
622cafbfd6 | ||
|
|
d8a1f66af9 | ||
|
|
c34cce63b0 | ||
|
|
f8f17b5d38 | ||
|
|
1d3a194d89 | ||
|
|
056384fdab | ||
|
|
7c40d074f3 | ||
|
|
c61595eb0d | ||
|
|
7192c970fa | ||
|
|
cfa709c651 | ||
|
|
97c467af41 | ||
|
|
6039426bac | ||
|
|
862fb90de9 | ||
|
|
842c32f29f | ||
|
|
0501de1658 | ||
|
|
dbe26847c7 | ||
|
|
806290f0a5 | ||
|
|
325c643314 | ||
|
|
4adc0affbe | ||
|
|
b5026095a0 | ||
|
|
fa7da3be10 | ||
|
|
35b44f1955 | ||
|
|
4708ee8823 | ||
|
|
52471f2ace | ||
|
|
0b065c745d | ||
|
|
9ce8244065 | ||
|
|
b7fafa1bf9 | ||
|
|
114c974ce5 | ||
|
|
e035925d25 | ||
|
|
c0fda6fcba | ||
|
|
75c90e3c45 | ||
|
|
9960f49698 | ||
|
|
1a2c4cc9a1 | ||
|
|
ee4f05b07d | ||
|
|
17ef476ede | ||
|
|
141b98631c | ||
|
|
845562bca3 | ||
|
|
105a41eeea |
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
@@ -21,23 +21,28 @@ jobs:
|
|||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
|
|
||||||
- name: Setup Golang
|
- name: Setup Golang
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.22.2'
|
go-version: '1.23.2'
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Patch Go use 600296
|
||||||
|
#https://go-review.googlesource.com/c/go/+/600296
|
||||||
|
run: |
|
||||||
|
cd "$(go env GOROOT)"
|
||||||
|
curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1
|
||||||
|
|
||||||
- name: Install gomobile
|
- name: Install gomobile
|
||||||
run: |
|
run: |
|
||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240806205939-81131f6468ab
|
||||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
|
||||||
- name: Setup Android environment
|
- name: Setup Android environment
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
|
||||||
- name: Build dependencies
|
- name: Build dependencies
|
||||||
run: |
|
run: |
|
||||||
mkdir ${{ github.workspace }}/build
|
mkdir ${{ github.workspace }}/build
|
||||||
@@ -56,8 +61,33 @@ jobs:
|
|||||||
chmod 755 gradlew
|
chmod 755 gradlew
|
||||||
./gradlew assembleDebug
|
./gradlew assembleDebug
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload arm64-v8a APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ success() }}
|
||||||
|
with:
|
||||||
|
name: arm64-v8a
|
||||||
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
|
||||||
|
|
||||||
|
- name: Upload armeabi-v7a APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ success() }}
|
||||||
|
with:
|
||||||
|
name: armeabi-v7a
|
||||||
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
|
||||||
|
|
||||||
|
- name: Upload x86 APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ success() }}
|
||||||
|
with:
|
||||||
|
name: x86-apk
|
||||||
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
|
||||||
|
|
||||||
|
- name: Upload Other APKs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: apk
|
name: others-apk
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/
|
path: |
|
||||||
|
${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug
|
||||||
|
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
|
||||||
|
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
|
||||||
|
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||||
|
|
||||||
[](https://developer.android.com/about/versions/lollipop)
|
[](https://developer.android.com/about/versions/lollipop)
|
||||||
[](https://kotlinlang.org)
|
[](https://kotlinlang.org)
|
||||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||||
[](https://github.com/2dust/v2rayNG/releases)
|
[](https://github.com/2dust/v2rayNG/releases)
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 585
|
versionCode = 611
|
||||||
versionName = "1.8.39"
|
versionName = "1.9.15"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
@@ -91,6 +91,8 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.org.mockito.mockito.inline)
|
||||||
|
testImplementation(libs.mockito.kotlin)
|
||||||
|
|
||||||
implementation(libs.flexbox)
|
implementation(libs.flexbox)
|
||||||
// Androidx
|
// Androidx
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
android:smallScreens="true"
|
|
||||||
android:normalScreens="true"
|
|
||||||
android:largeScreens="true"
|
android:largeScreens="true"
|
||||||
|
android:normalScreens="true"
|
||||||
|
android:smallScreens="true"
|
||||||
android:xlargeScreens="true" />
|
android:xlargeScreens="true" />
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
@@ -44,13 +44,13 @@
|
|||||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AngApplication"
|
android:name=".AngApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppThemeDayNight"
|
android:theme="@style/AppThemeDayNight"
|
||||||
@@ -77,55 +77,57 @@
|
|||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
|
||||||
android:name=".ui.ServerActivity"
|
android:name=".ui.ServerActivity"
|
||||||
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
|
||||||
android:name=".ui.ServerCustomConfigActivity"
|
android:name=".ui.ServerCustomConfigActivity"
|
||||||
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.SettingsActivity"
|
||||||
android:name=".ui.SettingsActivity" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.PerAppProxyActivity"
|
||||||
android:name=".ui.PerAppProxyActivity" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.ScannerActivity"
|
||||||
android:name=".ui.ScannerActivity" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.LogcatActivity"
|
||||||
android:name=".ui.LogcatActivity" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.RoutingSettingActivity"
|
||||||
android:name=".ui.RoutingSettingsActivity"
|
android:exported="false" />
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.RoutingEditActivity"
|
||||||
android:name=".ui.SubSettingActivity" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.SubSettingActivity"
|
||||||
android:name=".ui.UserAssetActivity" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.UserAssetActivity"
|
||||||
android:name=".ui.UserAssetUrlActivity" />
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.UserAssetUrlActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.SubEditActivity"
|
||||||
android:name=".ui.SubEditActivity" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.ScScannerActivity"
|
||||||
android:name=".ui.ScScannerActivity" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
|
||||||
android:name=".ui.ScSwitchActivity"
|
android:name=".ui.ScSwitchActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
android:theme="@style/AppTheme.NoActionBar.Translucent" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:exported="true"
|
android:name=".ui.UrlSchemeActivity"
|
||||||
android:name=".ui.UrlSchemeActivity">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -143,16 +145,16 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
android:name=".ui.AboutActivity"
|
||||||
android:name=".ui.AboutActivity" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.V2RayVpnService"
|
android:name=".service.V2RayVpnService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService" />
|
<action android:name="android.net.VpnService" />
|
||||||
@@ -168,8 +170,8 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".service.V2RayProxyOnlyService"
|
android:name=".service.V2RayProxyOnlyService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:label="@string/app_name"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<property
|
<property
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
@@ -179,12 +181,11 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".service.V2RayTestService"
|
android:name=".service.V2RayTestService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:process=":RunSoLibV2RayDaemon"
|
android:process=":RunSoLibV2RayDaemon" />
|
||||||
/>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:exported="true"
|
|
||||||
android:name=".receiver.WidgetProvider"
|
android:name=".receiver.WidgetProvider"
|
||||||
|
android:exported="true"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
@@ -195,13 +196,21 @@
|
|||||||
<action android:name="com.v2ray.ang.action.activity" />
|
<action android:name="com.v2ray.ang.action.activity" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.BootReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="BootReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:exported="true"
|
|
||||||
android:name=".service.QSTileService"
|
android:name=".service.QSTileService"
|
||||||
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:icon="@drawable/ic_stat_name"
|
android:icon="@drawable/ic_stat_name"
|
||||||
android:label="@string/app_tile_name"
|
android:label="@string/app_tile_name"
|
||||||
android:foregroundServiceType="specialUse"
|
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -213,19 +222,19 @@
|
|||||||
</service>
|
</service>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<activity
|
<activity
|
||||||
android:exported="true"
|
|
||||||
android:name=".ui.TaskerActivity"
|
android:name=".ui.TaskerActivity"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:exported="true"
|
||||||
android:label="@string/app_name">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:exported="true"
|
|
||||||
android:name=".receiver.TaskerReceiver"
|
android:name=".receiver.TaskerReceiver"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:exported="true"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
tools:ignore="ExportedReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
148
V2rayNG/app/src/main/assets/custom_routing_black
Normal file
148
V2rayNG/app/src/main/assets/custom_routing_black
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "绕过bittorrent",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"protocol": [
|
||||||
|
"bittorrent"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Google cn",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"domain:googleapis.cn",
|
||||||
|
"domain:gstatic.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "阻断udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "阻断广告",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"domain": [
|
||||||
|
"geosite:category-ads-all"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理海外公共DNSIP",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"ip": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"1.0.0.1",
|
||||||
|
"2606:4700:4700::1111",
|
||||||
|
"2606:4700:4700::1001",
|
||||||
|
"1.1.1.2",
|
||||||
|
"1.0.0.2",
|
||||||
|
"2606:4700:4700::1112",
|
||||||
|
"2606:4700:4700::1002",
|
||||||
|
"1.1.1.3",
|
||||||
|
"1.0.0.3",
|
||||||
|
"2606:4700:4700::1113",
|
||||||
|
"2606:4700:4700::1003",
|
||||||
|
"8.8.8.8",
|
||||||
|
"8.8.4.4",
|
||||||
|
"2001:4860:4860::8888",
|
||||||
|
"2001:4860:4860::8844",
|
||||||
|
"94.140.14.14",
|
||||||
|
"94.140.15.15",
|
||||||
|
"2a10:50c0::ad1:ff",
|
||||||
|
"2a10:50c0::ad2:ff",
|
||||||
|
"94.140.14.15",
|
||||||
|
"94.140.15.16",
|
||||||
|
"2a10:50c0::bad1:ff",
|
||||||
|
"2a10:50c0::bad2:ff",
|
||||||
|
"94.140.14.140",
|
||||||
|
"94.140.14.141",
|
||||||
|
"2a10:50c0::1:ff",
|
||||||
|
"2a10:50c0::2:ff",
|
||||||
|
"208.67.222.222",
|
||||||
|
"208.67.220.220",
|
||||||
|
"2620:119:35::35",
|
||||||
|
"2620:119:53::53",
|
||||||
|
"208.67.222.123",
|
||||||
|
"208.67.220.123",
|
||||||
|
"2620:119:35::123",
|
||||||
|
"2620:119:53::123",
|
||||||
|
"9.9.9.9",
|
||||||
|
"149.112.112.112",
|
||||||
|
"2620:fe::9",
|
||||||
|
"2620:fe::fe",
|
||||||
|
"9.9.9.11",
|
||||||
|
"149.112.112.11",
|
||||||
|
"2620:fe::11",
|
||||||
|
"2620:fe::fe:11",
|
||||||
|
"9.9.9.10",
|
||||||
|
"149.112.112.10",
|
||||||
|
"2620:fe::10",
|
||||||
|
"2620:fe::fe:10",
|
||||||
|
"77.88.8.8",
|
||||||
|
"77.88.8.1",
|
||||||
|
"2a02:6b8::feed:0ff",
|
||||||
|
"2a02:6b8:0:1::feed:0ff",
|
||||||
|
"77.88.8.88",
|
||||||
|
"77.88.8.2",
|
||||||
|
"2a02:6b8::feed:bad",
|
||||||
|
"2a02:6b8:0:1::feed:bad",
|
||||||
|
"77.88.8.7",
|
||||||
|
"77.88.8.3",
|
||||||
|
"2a02:6b8::feed:a11",
|
||||||
|
"2a02:6b8:0:1::feed:a11"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理海外公共DNS域名",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"domain:cloudflare-dns.com",
|
||||||
|
"domain:one.one.one.one",
|
||||||
|
"domain:dns.google",
|
||||||
|
"domain:adguard-dns.com",
|
||||||
|
"domain:opendns.com",
|
||||||
|
"domain:quad9.net",
|
||||||
|
"domain:dns.yandex.ru"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理IP",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"ip": [
|
||||||
|
"geoip:facebook",
|
||||||
|
"geoip:fastly",
|
||||||
|
"geoip:google",
|
||||||
|
"geoip:netflix",
|
||||||
|
"geoip:telegram",
|
||||||
|
"geoip:twitter"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理GFW",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"geosite:gfw",
|
||||||
|
"geosite:greatfire"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "最终直连",
|
||||||
|
"port": "0-65535",
|
||||||
|
"outboundTag": "direct"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1 +0,0 @@
|
|||||||
geosite:category-ads-all,
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
geosite:cn
|
|
||||||
34
V2rayNG/app/src/main/assets/custom_routing_global
Normal file
34
V2rayNG/app/src/main/assets/custom_routing_global
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "阻断udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "阻断广告",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"domain": [
|
||||||
|
"geosite:category-ads-all"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "最终代理",
|
||||||
|
"port": "0-65535",
|
||||||
|
"outboundTag": "proxy"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1 +0,0 @@
|
|||||||
geosite:geolocation-!cn
|
|
||||||
106
V2rayNG/app/src/main/assets/custom_routing_white
Normal file
106
V2rayNG/app/src/main/assets/custom_routing_white
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "Google cn",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"domain:googleapis.cn",
|
||||||
|
"domain:gstatic.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "阻断udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "阻断广告",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"domain": [
|
||||||
|
"geosite:category-ads-all"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国公共DNSIP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"223.5.5.5",
|
||||||
|
"223.6.6.6",
|
||||||
|
"2400:3200::1",
|
||||||
|
"2400:3200:baba::1",
|
||||||
|
"119.29.29.29",
|
||||||
|
"1.12.12.12",
|
||||||
|
"120.53.53.53",
|
||||||
|
"2402:4e00::",
|
||||||
|
"2402:4e00:1::",
|
||||||
|
"180.76.76.76",
|
||||||
|
"2400:da00::6666",
|
||||||
|
"114.114.114.114",
|
||||||
|
"114.114.115.115",
|
||||||
|
"114.114.114.119",
|
||||||
|
"114.114.115.119",
|
||||||
|
"114.114.114.110",
|
||||||
|
"114.114.115.110",
|
||||||
|
"180.184.1.1",
|
||||||
|
"180.184.2.2",
|
||||||
|
"101.226.4.6",
|
||||||
|
"218.30.118.6",
|
||||||
|
"123.125.81.6",
|
||||||
|
"140.207.198.6",
|
||||||
|
"1.2.4.8",
|
||||||
|
"210.2.4.8",
|
||||||
|
"117.50.11.11",
|
||||||
|
"52.80.66.66",
|
||||||
|
"2400:7fc0:849e:200::4",
|
||||||
|
"2404:c2c0:85d8:901::4",
|
||||||
|
"117.50.10.10",
|
||||||
|
"52.80.52.52",
|
||||||
|
"2400:7fc0:849e:200::8",
|
||||||
|
"2404:c2c0:85d8:901::8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国公共DNS域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"domain:alidns.com",
|
||||||
|
"domain:doh.pub",
|
||||||
|
"domain:dot.pub",
|
||||||
|
"domain:360.cn",
|
||||||
|
"domain:onedns.net"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:cn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:cn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "最终代理",
|
||||||
|
"port": "0-65535",
|
||||||
|
"outboundTag": "proxy"
|
||||||
|
}
|
||||||
|
]
|
||||||
49
V2rayNG/app/src/main/assets/custom_routing_white_iran
Normal file
49
V2rayNG/app/src/main/assets/custom_routing_white_iran
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "Block udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Block ads and trackers",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"domain": [
|
||||||
|
"geosite:category-ads-all"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Direct LAN IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Direct LAN domains",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Bypass Iran domains",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"domain:ir",
|
||||||
|
"geosite:category-ir"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Bypass Iran IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:ir"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Final Agent",
|
||||||
|
"port": "0-65535",
|
||||||
|
"outboundTag": "proxy"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 Paul Burke
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.v2ray.ang.helper;
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for manual initiation of a drag.
|
|
||||||
*/
|
|
||||||
public interface OnStartDragListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a view is requesting a start of a drag.
|
|
||||||
*
|
|
||||||
* @param viewHolder The holder of the view to drag.
|
|
||||||
*/
|
|
||||||
void onStartDrag(RecyclerView.ViewHolder viewHolder);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -16,14 +16,15 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
|
import android.view.animation.DecelerateInterpolator;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
|
||||||
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
|
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
|
||||||
@@ -36,9 +37,12 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
*/
|
*/
|
||||||
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||||
|
|
||||||
public static final float ALPHA_FULL = 1.0f;
|
private static final float ALPHA_FULL = 1.0f;
|
||||||
|
private static final float SWIPE_THRESHOLD = 0.25f;
|
||||||
|
private static final long ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
private final ItemTouchHelperAdapter mAdapter;
|
private final ItemTouchHelperAdapter mAdapter;
|
||||||
|
private ValueAnimator mReturnAnimator;
|
||||||
|
|
||||||
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
|
||||||
mAdapter = adapter;
|
mAdapter = adapter;
|
||||||
@@ -51,15 +55,14 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isItemViewSwipeEnabled() {
|
public boolean isItemViewSwipeEnabled() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
// Set movement flags based on the layout manager
|
|
||||||
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
|
||||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
||||||
final int swipeFlags = 0;
|
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
|
||||||
return makeMovementFlags(dragFlags, swipeFlags);
|
return makeMovementFlags(dragFlags, swipeFlags);
|
||||||
} else {
|
} else {
|
||||||
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
|
||||||
@@ -69,61 +72,89 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) {
|
||||||
if (source.getItemViewType() != target.getItemViewType()) {
|
if (source.getItemViewType() != target.getItemViewType()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the adapter of the move
|
|
||||||
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
|
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||||
// Notify the adapter of the dismissal
|
// 不执行删除操作,仅返回项目到原位
|
||||||
mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
|
returnViewToOriginalPosition(viewHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX,
|
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
|
||||||
float dY, int actionState, boolean isCurrentlyActive) {
|
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||||
|
float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
|
||||||
// Fade out the view as it is swiped out of the parent's bounds
|
float maxSwipeDistance = viewHolder.itemView.getWidth() * SWIPE_THRESHOLD;
|
||||||
final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
|
float swipeAmount = Math.abs(dX);
|
||||||
|
float direction = Math.signum(dX);
|
||||||
|
|
||||||
|
// 限制最大滑动距离
|
||||||
|
float translationX = Math.min(swipeAmount, maxSwipeDistance) * direction;
|
||||||
|
float alpha = ALPHA_FULL - Math.min(swipeAmount, maxSwipeDistance) / maxSwipeDistance;
|
||||||
|
|
||||||
|
viewHolder.itemView.setTranslationX(translationX);
|
||||||
viewHolder.itemView.setAlpha(alpha);
|
viewHolder.itemView.setAlpha(alpha);
|
||||||
viewHolder.itemView.setTranslationX(dX);
|
|
||||||
|
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
|
||||||
|
returnViewToOriginalPosition(viewHolder);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void returnViewToOriginalPosition(RecyclerView.ViewHolder viewHolder) {
|
||||||
|
if (mReturnAnimator != null && mReturnAnimator.isRunning()) {
|
||||||
|
mReturnAnimator.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.getTranslationX(), 0f);
|
||||||
|
mReturnAnimator.addUpdateListener(animation -> {
|
||||||
|
float value = (float) animation.getAnimatedValue();
|
||||||
|
viewHolder.itemView.setTranslationX(value);
|
||||||
|
viewHolder.itemView.setAlpha(1f - Math.abs(value) / (viewHolder.itemView.getWidth() * SWIPE_THRESHOLD));
|
||||||
|
});
|
||||||
|
mReturnAnimator.setInterpolator(new DecelerateInterpolator());
|
||||||
|
mReturnAnimator.setDuration(ANIMATION_DURATION);
|
||||||
|
mReturnAnimator.start();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
|
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||||
// We only want the active item to change
|
|
||||||
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
|
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||||
// Let the view holder know that this item is being moved or dragged
|
|
||||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||||
itemViewHolder.onItemSelected();
|
itemViewHolder.onItemSelected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onSelectedChanged(viewHolder, actionState);
|
super.onSelectedChanged(viewHolder, actionState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
|
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
super.clearView(recyclerView, viewHolder);
|
super.clearView(recyclerView, viewHolder);
|
||||||
|
|
||||||
mAdapter.onItemMoveCompleted();
|
|
||||||
|
|
||||||
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
viewHolder.itemView.setAlpha(ALPHA_FULL);
|
||||||
|
|
||||||
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
if (viewHolder instanceof ItemTouchHelperViewHolder) {
|
||||||
// Tell the view holder it's time to restore the idle state
|
|
||||||
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
|
||||||
itemViewHolder.onItemClear();
|
itemViewHolder.onItemClear();
|
||||||
}
|
}
|
||||||
|
mAdapter.onItemMoveCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
return 1.1f; // 设置一个大于1的值,确保不会触发默认的滑动删除操作
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getSwipeEscapeVelocity(float defaultValue) {
|
||||||
|
return defaultValue * 10; // 增加滑动逃逸速度,使得更难触发滑动
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
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.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication() {
|
class AngApplication : MultiDexApplication() {
|
||||||
@@ -32,11 +35,18 @@ class AngApplication : MultiDexApplication() {
|
|||||||
// if (firstRun)
|
// if (firstRun)
|
||||||
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||||
|
|
||||||
//Logger.init().logLevel(if (BuildConfig.DEBUG) LogLevel.FULL else LogLevel.NONE)
|
|
||||||
MMKV.initialize(this)
|
MMKV.initialize(this)
|
||||||
|
|
||||||
Utils.setNightMode(application)
|
Utils.setNightMode(application)
|
||||||
// Initialize WorkManager with the custom configuration
|
// Initialize WorkManager with the custom configuration
|
||||||
WorkManager.initialize(this, workManagerConfiguration)
|
WorkManager.initialize(this, workManagerConfiguration)
|
||||||
|
|
||||||
|
SettingsManager.initRoutingRulesets(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(
|
||||||
|
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
|
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
|
||||||
|
)!!
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,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_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||||
const val PREF_ROUTING_MODE = "pref_routing_mode"
|
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
|
||||||
const val PREF_V2RAY_ROUTING_AGENT = "pref_v2ray_routing_agent"
|
|
||||||
const val PREF_V2RAY_ROUTING_DIRECT = "pref_v2ray_routing_direct"
|
|
||||||
const val PREF_V2RAY_ROUTING_BLOCKED = "pref_v2ray_routing_blocked"
|
|
||||||
const val PREF_ROUTING_CUSTOM = "pref_routing_custom"
|
|
||||||
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
||||||
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
|
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
|
||||||
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
|
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
|
||||||
@@ -52,20 +48,18 @@ object AppConfig {
|
|||||||
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||||
const val PREF_SOCKS_PORT = "pref_socks_port"
|
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||||
const val PREF_HTTP_PORT = "pref_http_port"
|
const val PREF_HTTP_PORT = "pref_http_port"
|
||||||
|
|
||||||
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
||||||
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
||||||
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
|
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
|
||||||
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||||
const val PREF_MODE = "pref_mode"
|
const val PREF_MODE = "pref_mode"
|
||||||
|
const val PREF_IS_BOOTED = "pref_is_booted"
|
||||||
|
|
||||||
/** 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_HTTP: String = "http://"
|
|
||||||
const val PROTOCOL_HTTPS: String = "https://"
|
|
||||||
const val PROTOCOL_FREEDOM: String = "freedom"
|
const val PROTOCOL_FREEDOM: String = "freedom"
|
||||||
|
|
||||||
/** Broadcast actions. */
|
/** Broadcast actions. */
|
||||||
@@ -109,6 +103,10 @@ object AppConfig {
|
|||||||
const val DNS_PROXY = "1.1.1.1"
|
const val DNS_PROXY = "1.1.1.1"
|
||||||
const val DNS_DIRECT = "223.5.5.5"
|
const val DNS_DIRECT = "223.5.5.5"
|
||||||
const val DNS_VPN = "1.1.1.1"
|
const val DNS_VPN = "1.1.1.1"
|
||||||
|
const val GEOSITE_PRIVATE = "geosite:private"
|
||||||
|
const val GEOSITE_CN = "geosite:cn"
|
||||||
|
const val GEOIP_PRIVATE = "geoip:private"
|
||||||
|
const val GEOIP_CN = "geoip:cn"
|
||||||
|
|
||||||
/** Ports and addresses for various services. */
|
/** Ports and addresses for various services. */
|
||||||
const val PORT_LOCAL_DNS = "10853"
|
const val PORT_LOCAL_DNS = "10853"
|
||||||
@@ -117,6 +115,7 @@ object AppConfig {
|
|||||||
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
||||||
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
||||||
const val WIREGUARD_LOCAL_MTU = "1420"
|
const val WIREGUARD_LOCAL_MTU = "1420"
|
||||||
|
const val LOOPBACK = "127.0.0.1"
|
||||||
|
|
||||||
/** Message constants for communication. */
|
/** Message constants for communication. */
|
||||||
const val MSG_REGISTER_CLIENT = 1
|
const val MSG_REGISTER_CLIENT = 1
|
||||||
@@ -137,7 +136,7 @@ object AppConfig {
|
|||||||
|
|
||||||
/** Notification channel IDs and names. */
|
/** Notification channel IDs and names. */
|
||||||
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
||||||
const val RAY_NG_CHANNEL_NAME = "V2rayNG Background Service"
|
const val RAY_NG_CHANNEL_NAME = "v2rayNG Background Service"
|
||||||
const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
|
const val SUBSCRIPTION_UPDATE_CHANNEL = "subscription_update_channel"
|
||||||
const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
|
const val SUBSCRIPTION_UPDATE_CHANNEL_NAME = "Subscription Update Service"
|
||||||
|
|
||||||
@@ -146,7 +145,38 @@ object AppConfig {
|
|||||||
const val CUSTOM = ""
|
const val CUSTOM = ""
|
||||||
const val SHADOWSOCKS = "ss://"
|
const val SHADOWSOCKS = "ss://"
|
||||||
const val SOCKS = "socks://"
|
const val SOCKS = "socks://"
|
||||||
|
const val HTTP = "http://"
|
||||||
const val VLESS = "vless://"
|
const val VLESS = "vless://"
|
||||||
const val TROJAN = "trojan://"
|
const val TROJAN = "trojan://"
|
||||||
const val WIREGUARD = "wireguard://"
|
const val WIREGUARD = "wireguard://"
|
||||||
|
const val TUIC = "tuic://"
|
||||||
|
const val HYSTERIA2 = "hysteria2://"
|
||||||
|
const val HY2 = "hy2://"
|
||||||
|
|
||||||
|
/** Give a good name to this, IDK*/
|
||||||
|
const val VPN = "VPN"
|
||||||
|
|
||||||
|
// Google API rule constants
|
||||||
|
const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
|
||||||
|
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
||||||
|
|
||||||
|
// Android Private DNS constants
|
||||||
|
const val DNS_PUB_DOMAIN = "dns.pub"
|
||||||
|
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
||||||
|
const val DNS_ONE_ONE_DOMAIN = "one.one.one.one"
|
||||||
|
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
||||||
|
|
||||||
|
val DNS_PUB_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||||
|
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||||
|
val DNS_ONE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||||
|
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
||||||
|
|
||||||
|
const val DEFAULT_PORT = 443
|
||||||
|
const val DEFAULT_SECURITY = "auto"
|
||||||
|
const val DEFAULT_LEVEL = 8
|
||||||
|
const val DEFAULT_NETWORK = "tcp"
|
||||||
|
const val TLS = "tls"
|
||||||
|
const val REALITY = "reality"
|
||||||
|
const val HEADER_TYPE_HTTP = "http"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class ConfigResult(
|
||||||
|
var status: Boolean,
|
||||||
|
var guid: String? = null,
|
||||||
|
var content: String = "",
|
||||||
|
var domainPort: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
@@ -10,9 +10,12 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
|
|||||||
SOCKS(4, AppConfig.SOCKS),
|
SOCKS(4, AppConfig.SOCKS),
|
||||||
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),
|
||||||
|
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
||||||
|
HTTP(10, AppConfig.HTTP);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int) = values().firstOrNull { it.value == value }
|
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.v2ray.ang.dto
|
|
||||||
|
|
||||||
enum class ERoutingMode(val value: String) {
|
|
||||||
GLOBAL_PROXY("0"),
|
|
||||||
BYPASS_LAN("1"),
|
|
||||||
BYPASS_MAINLAND("2"),
|
|
||||||
BYPASS_LAN_MAINLAND("3"),
|
|
||||||
GLOBAL_DIRECT("4");
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class Hysteria2Bean(
|
||||||
|
val server: String?,
|
||||||
|
val auth: String?,
|
||||||
|
val lazy: Boolean? = true,
|
||||||
|
val obfs: ObfsBean? = null,
|
||||||
|
val socks5: Socks5Bean? = null,
|
||||||
|
val http: Socks5Bean? = null,
|
||||||
|
val tls: TlsBean? = null,
|
||||||
|
val transport: TransportBean? = null,
|
||||||
|
) {
|
||||||
|
data class ObfsBean(
|
||||||
|
val type: String?,
|
||||||
|
val salamander: SalamanderBean?
|
||||||
|
) {
|
||||||
|
data class SalamanderBean(
|
||||||
|
val password: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Socks5Bean(
|
||||||
|
val listen: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TlsBean(
|
||||||
|
val sni: String?,
|
||||||
|
val insecure: Boolean?,
|
||||||
|
val pinSHA256: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TransportBean(
|
||||||
|
val type: String?,
|
||||||
|
val udp: TransportUdpBean?
|
||||||
|
) {
|
||||||
|
data class TransportUdpBean(
|
||||||
|
val hopInterval: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/Language.kt
Normal file
18
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/Language.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
enum class Language(val code: String) {
|
||||||
|
AUTO("auto"),
|
||||||
|
ENGLISH("en"),
|
||||||
|
CHINA("zh-rCN"),
|
||||||
|
TRADITIONAL_CHINESE("zh-rTW"),
|
||||||
|
VIETNAMESE("vi"),
|
||||||
|
RUSSIAN("ru"),
|
||||||
|
PERSIAN("fa"),
|
||||||
|
BANGLA("bn");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromCode(code: String): Language {
|
||||||
|
return entries.find { it.code == code } ?: AUTO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/NetworkType.kt
Normal file
17
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/NetworkType.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
enum class NetworkType(val type: String) {
|
||||||
|
TCP("tcp"),
|
||||||
|
KCP("kcp"),
|
||||||
|
WS("ws"),
|
||||||
|
HTTP_UPGRADE("httpupgrade"),
|
||||||
|
SPLIT_HTTP("splithttp"),
|
||||||
|
HTTP("http"),
|
||||||
|
H2("h2"),
|
||||||
|
QUIC("quic"),
|
||||||
|
GRPC("grpc");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,75 @@
|
|||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
data class ProfileItem(
|
data class ProfileItem(
|
||||||
|
val configVersion: Int = 4,
|
||||||
val configType: EConfigType,
|
val configType: EConfigType,
|
||||||
var subscriptionId: String = "",
|
var subscriptionId: String = "",
|
||||||
|
var addedTime: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
var remarks: String = "",
|
var remarks: String = "",
|
||||||
var server: String?,
|
var server: String? = null,
|
||||||
var serverPort: Int?,
|
var serverPort: String? = null,
|
||||||
)
|
|
||||||
|
var password: String? = null,
|
||||||
|
var method: String? = null,
|
||||||
|
var flow: String? = null,
|
||||||
|
var username: String? = null,
|
||||||
|
|
||||||
|
var network: String? = null,
|
||||||
|
var headerType: String? = null,
|
||||||
|
var host: String? = null,
|
||||||
|
var path: String? = null,
|
||||||
|
var seed: String? = null,
|
||||||
|
var quicSecurity: String? = null,
|
||||||
|
var quicKey: String? = null,
|
||||||
|
var mode: String? = null,
|
||||||
|
var serviceName: String? = null,
|
||||||
|
var authority: String? = null,
|
||||||
|
|
||||||
|
var security: String? = null,
|
||||||
|
var sni: String? = null,
|
||||||
|
var alpn: String? = null,
|
||||||
|
var fingerPrint: String? = null,
|
||||||
|
var insecure: Boolean? = null,
|
||||||
|
|
||||||
|
var publicKey: String? = null,
|
||||||
|
var shortId: String? = null,
|
||||||
|
var spiderX: String? = null,
|
||||||
|
|
||||||
|
var secretKey: String? = null,
|
||||||
|
var localAddress: String? = null,
|
||||||
|
var reserved: String? = null,
|
||||||
|
var mtu: Int? = null,
|
||||||
|
|
||||||
|
var obfsPassword: String? = null,
|
||||||
|
var portHopping: String? = null,
|
||||||
|
var portHoppingInterval: String? = null,
|
||||||
|
var pinSHA256: String? = null,
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(configType: EConfigType): ProfileItem {
|
||||||
|
return ProfileItem(configType = configType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllOutboundTags(): MutableList<String> {
|
||||||
|
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerAddressAndPort(): String {
|
||||||
|
return Utils.getIpv6Address(server) + ":" + serverPort
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKeyProperty(): ProfileItem {
|
||||||
|
val copy = this.copy()
|
||||||
|
copy.subscriptionId = ""
|
||||||
|
copy.addedTime = 0L
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class ProfileLiteItem(
|
||||||
|
val configType: EConfigType,
|
||||||
|
var subscriptionId: String = "",
|
||||||
|
var remarks: String = "",
|
||||||
|
var server: String?,
|
||||||
|
var serverPort: Int?,
|
||||||
|
)
|
||||||
20
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/RoutingType.kt
Normal file
20
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/RoutingType.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
enum class RoutingType(val fileName: String) {
|
||||||
|
WHITE("custom_routing_white"),
|
||||||
|
BLACK("custom_routing_black"),
|
||||||
|
GLOBAL("custom_routing_global"),
|
||||||
|
WHITE_IRAN("custom_routing_white_iran");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromIndex(index: Int): RoutingType {
|
||||||
|
return when (index) {
|
||||||
|
0 -> WHITE
|
||||||
|
1 -> BLACK
|
||||||
|
2 -> GLOBAL
|
||||||
|
3 -> WHITE_IRAN
|
||||||
|
else -> WHITE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/RulesetItem.kt
Normal file
13
V2rayNG/app/src/main/kotlin/com/v2ray/ang/dto/RulesetItem.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class RulesetItem(
|
||||||
|
var remarks: String? = "",
|
||||||
|
var ip: List<String>? = null,
|
||||||
|
var domain: List<String>? = null,
|
||||||
|
var outboundTag: String = "",
|
||||||
|
var port: String? = null,
|
||||||
|
var network: String? = null,
|
||||||
|
var protocol: List<String>? = null,
|
||||||
|
var enabled: Boolean = true,
|
||||||
|
var looked: Boolean? = false,
|
||||||
|
)
|
||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.dto
|
|||||||
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
data class ServerConfig(
|
data class ServerConfig(
|
||||||
val configVersion: Int = 3,
|
val configVersion: Int = 3,
|
||||||
@@ -17,7 +16,8 @@ data class ServerConfig(
|
|||||||
companion object {
|
companion object {
|
||||||
fun create(configType: EConfigType): ServerConfig {
|
fun create(configType: EConfigType): ServerConfig {
|
||||||
when (configType) {
|
when (configType) {
|
||||||
EConfigType.VMESS, EConfigType.VLESS ->
|
EConfigType.VMESS,
|
||||||
|
EConfigType.VLESS ->
|
||||||
return ServerConfig(
|
return ServerConfig(
|
||||||
configType = configType,
|
configType = configType,
|
||||||
outboundBean = V2rayConfig.OutboundBean(
|
outboundBean = V2rayConfig.OutboundBean(
|
||||||
@@ -36,7 +36,11 @@ data class ServerConfig(
|
|||||||
EConfigType.CUSTOM ->
|
EConfigType.CUSTOM ->
|
||||||
return ServerConfig(configType = configType)
|
return ServerConfig(configType = configType)
|
||||||
|
|
||||||
EConfigType.SHADOWSOCKS, EConfigType.SOCKS, EConfigType.TROJAN ->
|
EConfigType.SHADOWSOCKS,
|
||||||
|
EConfigType.SOCKS,
|
||||||
|
EConfigType.HTTP,
|
||||||
|
EConfigType.TROJAN,
|
||||||
|
EConfigType.HYSTERIA2 ->
|
||||||
return ServerConfig(
|
return ServerConfig(
|
||||||
configType = configType,
|
configType = configType,
|
||||||
outboundBean = V2rayConfig.OutboundBean(
|
outboundBean = V2rayConfig.OutboundBean(
|
||||||
@@ -79,10 +83,4 @@ data class ServerConfig(
|
|||||||
}
|
}
|
||||||
return mutableListOf()
|
return mutableListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getV2rayPointDomainAndPort(): String {
|
|
||||||
val address = getProxyOutbound()?.getServerAddress().orEmpty()
|
|
||||||
val port = getProxyOutbound()?.getServerPort()
|
|
||||||
return Utils.getIpv6Address(address) + ":" + port
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,8 @@ data class SubscriptionItem(
|
|||||||
var lastUpdated: Long = -1,
|
var lastUpdated: Long = -1,
|
||||||
var autoUpdate: Boolean = false,
|
var autoUpdate: Boolean = false,
|
||||||
val updateInterval: Int? = null,
|
val updateInterval: Int? = null,
|
||||||
|
var prevProfile: String? = null,
|
||||||
|
var nextProfile: String? = null,
|
||||||
|
var filter: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import com.google.gson.JsonSerializationContext
|
|||||||
import com.google.gson.JsonSerializer
|
import com.google.gson.JsonSerializer
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.*
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.*
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
data class V2rayConfig(
|
data class V2rayConfig(
|
||||||
@@ -26,16 +30,6 @@ data class V2rayConfig(
|
|||||||
var observatory: Any? = null,
|
var observatory: Any? = null,
|
||||||
var burstObservatory: Any? = null
|
var burstObservatory: Any? = null
|
||||||
) {
|
) {
|
||||||
companion object {
|
|
||||||
const val DEFAULT_PORT = 443
|
|
||||||
const val DEFAULT_SECURITY = "auto"
|
|
||||||
const val DEFAULT_LEVEL = 8
|
|
||||||
const val DEFAULT_NETWORK = "tcp"
|
|
||||||
|
|
||||||
const val TLS = "tls"
|
|
||||||
const val REALITY = "reality"
|
|
||||||
const val HTTP = "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
data class LogBean(
|
data class LogBean(
|
||||||
val access: String,
|
val access: String,
|
||||||
@@ -81,11 +75,54 @@ 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,
|
||||||
var noise: NoiseBean? = null,
|
var noises: List<NoiseBean>? = null,
|
||||||
var servers: List<ServersBean>? = null,
|
var servers: List<ServersBean>? = null,
|
||||||
/*Blackhole*/
|
/*Blackhole*/
|
||||||
var response: Response? = null,
|
var response: Response? = null,
|
||||||
@@ -103,22 +140,23 @@ data class V2rayConfig(
|
|||||||
var secretKey: String? = null,
|
var secretKey: String? = null,
|
||||||
val peers: List<WireGuardBean>? = null,
|
val peers: List<WireGuardBean>? = null,
|
||||||
var reserved: List<Int>? = null,
|
var reserved: List<Int>? = null,
|
||||||
var mtu: Int? = null
|
var mtu: Int? = null,
|
||||||
|
var obfsPassword: String? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class VnextBean(
|
data class VnextBean(
|
||||||
var address: String = "",
|
var address: String = "",
|
||||||
var port: Int = DEFAULT_PORT,
|
var port: Int = AppConfig.DEFAULT_PORT,
|
||||||
var users: List<UsersBean>
|
var users: List<UsersBean>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class UsersBean(
|
data class UsersBean(
|
||||||
var id: String = "",
|
var id: String = "",
|
||||||
var alterId: Int? = null,
|
var alterId: Int? = null,
|
||||||
var security: String = DEFAULT_SECURITY,
|
var security: String? = null,
|
||||||
var level: Int = DEFAULT_LEVEL,
|
var level: Int = AppConfig.DEFAULT_LEVEL,
|
||||||
var encryption: String = "",
|
var encryption: String? = null,
|
||||||
var flow: String = ""
|
var flow: String? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,28 +167,27 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class NoiseBean(
|
data class NoiseBean(
|
||||||
|
var type: String? = null,
|
||||||
var packet: String? = null,
|
var packet: String? = null,
|
||||||
var delay: String? = null
|
var delay: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ServersBean(
|
data class ServersBean(
|
||||||
var address: String = "",
|
var address: String = "",
|
||||||
var method: String = "chacha20-poly1305",
|
var method: String? = null,
|
||||||
var ota: Boolean = false,
|
var ota: Boolean = false,
|
||||||
var password: String = "",
|
var password: String? = null,
|
||||||
var port: Int = DEFAULT_PORT,
|
var port: Int = AppConfig.DEFAULT_PORT,
|
||||||
var level: Int = DEFAULT_LEVEL,
|
var level: Int = AppConfig.DEFAULT_LEVEL,
|
||||||
val email: String? = null,
|
val email: String? = null,
|
||||||
var flow: String? = null,
|
var flow: String? = null,
|
||||||
val ivCheck: Boolean? = null,
|
val ivCheck: Boolean? = null,
|
||||||
var users: List<SocksUsersBean>? = null
|
var users: List<SocksUsersBean>? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
data class SocksUsersBean(
|
data class SocksUsersBean(
|
||||||
var user: String = "",
|
var user: String = "",
|
||||||
var pass: String = "",
|
var pass: String = "",
|
||||||
var level: Int = DEFAULT_LEVEL
|
var level: Int = AppConfig.DEFAULT_LEVEL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +200,7 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class StreamSettingsBean(
|
data class StreamSettingsBean(
|
||||||
var network: String = DEFAULT_NETWORK,
|
var network: String = AppConfig.DEFAULT_NETWORK,
|
||||||
var security: String = "",
|
var security: String = "",
|
||||||
var tcpSettings: TcpSettingsBean? = null,
|
var tcpSettings: TcpSettingsBean? = null,
|
||||||
var kcpSettings: KcpSettingsBean? = null,
|
var kcpSettings: KcpSettingsBean? = null,
|
||||||
@@ -175,6 +212,7 @@ data class V2rayConfig(
|
|||||||
var quicSettings: QuicSettingBean? = null,
|
var quicSettings: QuicSettingBean? = null,
|
||||||
var realitySettings: TlsSettingsBean? = null,
|
var realitySettings: TlsSettingsBean? = null,
|
||||||
var grpcSettings: GrpcSettingsBean? = null,
|
var grpcSettings: GrpcSettingsBean? = null,
|
||||||
|
var hy2steriaSettings: Hy2steriaSettingsBean? = null,
|
||||||
val dsSettings: Any? = null,
|
val dsSettings: Any? = null,
|
||||||
var sockopt: SockoptBean? = null
|
var sockopt: SockoptBean? = null
|
||||||
) {
|
) {
|
||||||
@@ -222,7 +260,7 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class WsSettingsBean(
|
data class WsSettingsBean(
|
||||||
var path: String = "",
|
var path: String? = null,
|
||||||
var headers: HeadersBean = HeadersBean(),
|
var headers: HeadersBean = HeadersBean(),
|
||||||
val maxEarlyData: Int? = null,
|
val maxEarlyData: Int? = null,
|
||||||
val useBrowserForwarding: Boolean? = null,
|
val useBrowserForwarding: Boolean? = null,
|
||||||
@@ -232,21 +270,21 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class HttpupgradeSettingsBean(
|
data class HttpupgradeSettingsBean(
|
||||||
var path: String = "",
|
var path: String? = null,
|
||||||
var host: String = "",
|
var host: String? = null,
|
||||||
val acceptProxyProtocol: Boolean? = null
|
val acceptProxyProtocol: Boolean? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SplithttpSettingsBean(
|
data class SplithttpSettingsBean(
|
||||||
var path: String = "",
|
var path: String? = null,
|
||||||
var host: String = "",
|
var host: String? = null,
|
||||||
val maxUploadSize: Int? = null,
|
val maxUploadSize: Int? = null,
|
||||||
val maxConcurrentUploads: Int? = null
|
val maxConcurrentUploads: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HttpSettingsBean(
|
data class HttpSettingsBean(
|
||||||
var host: List<String> = ArrayList(),
|
var host: List<String> = ArrayList(),
|
||||||
var path: String = ""
|
var path: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SockoptBean(
|
data class SockoptBean(
|
||||||
@@ -260,7 +298,7 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class TlsSettingsBean(
|
data class TlsSettingsBean(
|
||||||
var allowInsecure: Boolean = false,
|
var allowInsecure: Boolean = false,
|
||||||
var serverName: String = "",
|
var serverName: String? = null,
|
||||||
val alpn: List<String>? = null,
|
val alpn: List<String>? = null,
|
||||||
val minVersion: String? = null,
|
val minVersion: String? = null,
|
||||||
val maxVersion: String? = null,
|
val maxVersion: String? = null,
|
||||||
@@ -293,22 +331,34 @@ data class V2rayConfig(
|
|||||||
var health_check_timeout: Int? = null
|
var health_check_timeout: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class Hy2steriaSettingsBean(
|
||||||
|
var password: String? = null,
|
||||||
|
var use_udp_extension: Boolean? = true,
|
||||||
|
var congestion: Hy2CongestionBean? = null
|
||||||
|
) {
|
||||||
|
data class Hy2CongestionBean(
|
||||||
|
var type: String? = "bbr",
|
||||||
|
var up_mbps: Int? = null,
|
||||||
|
var down_mbps: Int? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun populateTransportSettings(
|
fun populateTransportSettings(
|
||||||
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
||||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
||||||
authority: String?
|
authority: String?
|
||||||
): String {
|
): String? {
|
||||||
var sni = ""
|
var sni: String? = null
|
||||||
network = transport
|
network = transport
|
||||||
when (network) {
|
when (network) {
|
||||||
"tcp" -> {
|
"tcp" -> {
|
||||||
val tcpSetting = TcpSettingsBean()
|
val tcpSetting = TcpSettingsBean()
|
||||||
if (headerType == HTTP) {
|
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
||||||
tcpSetting.header.type = HTTP
|
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
||||||
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
|
||||||
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
|
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
|
||||||
requestObj.headers.Host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
requestObj.path = (path.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
tcpSetting.header.request = requestObj
|
tcpSetting.header.request = requestObj
|
||||||
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
|
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
|
||||||
}
|
}
|
||||||
@@ -357,7 +407,7 @@ data class V2rayConfig(
|
|||||||
"h2", "http" -> {
|
"h2", "http" -> {
|
||||||
network = "h2"
|
network = "h2"
|
||||||
val h2Setting = HttpSettingsBean()
|
val h2Setting = HttpSettingsBean()
|
||||||
h2Setting.host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
sni = h2Setting.host.getOrNull(0) ?: sni
|
sni = h2Setting.host.getOrNull(0) ?: sni
|
||||||
h2Setting.path = path ?: "/"
|
h2Setting.path = path ?: "/"
|
||||||
httpSettings = h2Setting
|
httpSettings = h2Setting
|
||||||
@@ -386,7 +436,7 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun populateTlsSettings(
|
fun populateTlsSettings(
|
||||||
streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
|
streamSecurity: String, allowInsecure: Boolean, sni: String?, fingerprint: String?, alpns: String?,
|
||||||
publicKey: String?, shortId: String?, spiderX: String?
|
publicKey: String?, shortId: String?, spiderX: String?
|
||||||
) {
|
) {
|
||||||
security = streamSecurity
|
security = streamSecurity
|
||||||
@@ -399,10 +449,10 @@ data class V2rayConfig(
|
|||||||
shortId = shortId,
|
shortId = shortId,
|
||||||
spiderX = spiderX
|
spiderX = spiderX
|
||||||
)
|
)
|
||||||
if (security == TLS) {
|
if (security == AppConfig.TLS) {
|
||||||
tlsSettings = tlsSetting
|
tlsSettings = tlsSetting
|
||||||
realitySettings = null
|
realitySettings = null
|
||||||
} else if (security == REALITY) {
|
} else if (security == AppConfig.REALITY) {
|
||||||
tlsSettings = null
|
tlsSettings = null
|
||||||
realitySettings = tlsSetting
|
realitySettings = tlsSetting
|
||||||
}
|
}
|
||||||
@@ -420,14 +470,16 @@ data class V2rayConfig(
|
|||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.vnext?.get(0)?.address
|
return settings?.vnext?.first()?.address
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.get(0)?.address
|
return settings?.servers?.first()?.address
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
|
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -436,29 +488,40 @@ data class V2rayConfig(
|
|||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.vnext?.get(0)?.port
|
return settings?.vnext?.first()?.port
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.get(0)?.port
|
return settings?.servers?.first()?.port
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
|
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getServerAddressAndPort(): String {
|
||||||
|
val address = getServerAddress().orEmpty()
|
||||||
|
val port = getServerPort()
|
||||||
|
return Utils.getIpv6Address(address) + ":" + port
|
||||||
|
}
|
||||||
|
|
||||||
fun getPassword(): String? {
|
fun getPassword(): String? {
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.vnext?.get(0)?.users?.get(0)?.id
|
return settings?.vnext?.first()?.users?.first()?.id
|
||||||
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
) {
|
) {
|
||||||
return settings?.servers?.get(0)?.password
|
return settings?.servers?.first()?.password
|
||||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)) {
|
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|
) {
|
||||||
|
return settings?.servers?.first()?.users?.first()?.pass
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.secretKey
|
return settings?.secretKey
|
||||||
}
|
}
|
||||||
@@ -467,14 +530,14 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
fun getSecurityEncryption(): String? {
|
fun getSecurityEncryption(): String? {
|
||||||
return when {
|
return when {
|
||||||
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security
|
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security
|
||||||
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption
|
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption
|
||||||
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method
|
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTransportSettingDetails(): List<String>? {
|
fun getTransportSettingDetails(): List<String?>? {
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
@@ -486,8 +549,8 @@ data class V2rayConfig(
|
|||||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
tcpSetting.header.type,
|
tcpSetting.header.type,
|
||||||
tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(),
|
tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(),
|
||||||
tcpSetting.header.request?.path?.joinToString().orEmpty()
|
tcpSetting.header.request?.path?.joinToString(",").orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +594,7 @@ data class V2rayConfig(
|
|||||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
val h2Setting = streamSettings?.httpSettings ?: return null
|
||||||
listOf(
|
listOf(
|
||||||
"",
|
"",
|
||||||
h2Setting.host.joinToString(),
|
h2Setting.host.joinToString(","),
|
||||||
h2Setting.path
|
h2Setting.path
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -574,7 +637,8 @@ data class V2rayConfig(
|
|||||||
var port: Int? = null,
|
var port: Int? = null,
|
||||||
var domains: List<String>? = null,
|
var domains: List<String>? = null,
|
||||||
var expectIPs: List<String>? = null,
|
var expectIPs: List<String>? = null,
|
||||||
val clientIp: String? = null
|
val clientIp: String? = null,
|
||||||
|
val skipFallback: Boolean? = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,6 +650,7 @@ data class V2rayConfig(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
data class RulesBean(
|
data class RulesBean(
|
||||||
|
var type: String = "field",
|
||||||
var ip: ArrayList<String>? = null,
|
var ip: ArrayList<String>? = null,
|
||||||
var domain: ArrayList<String>? = null,
|
var domain: ArrayList<String>? = null,
|
||||||
var outboundTag: String = "",
|
var outboundTag: String = "",
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package com.v2ray.ang.extension
|
package com.v2ray.ang.extension
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
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 me.drakeet.support.toast.ToastCompat
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.io.Serializable
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
|
|
||||||
@@ -54,4 +59,40 @@ 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(), "")
|
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
|
||||||
|
|
||||||
|
fun String.toLongEx(): Long = toLongOrNull() ?: 0
|
||||||
|
|
||||||
|
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
callback()
|
||||||
|
if (onetime) context.unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
registerReceiver(this, IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addDataScheme("package")
|
||||||
|
}, Context.RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
|
registerReceiver(this, IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addDataScheme("package")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
|
||||||
|
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
|
||||||
|
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
|
||||||
21
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/CustomFmt.kt
Normal file
21
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/CustomFmt.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
|
||||||
|
object CustomFmt : FmtBase() {
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.CUSTOM)
|
||||||
|
|
||||||
|
val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
|
||||||
|
val outbound = fullConfig.getProxyOutbound()
|
||||||
|
|
||||||
|
config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
||||||
|
config.server = outbound?.getServerAddress()
|
||||||
|
config.serverPort = outbound?.getServerPort().toString()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
}
|
||||||
85
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/FmtBase.kt
Normal file
85
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/FmtBase.kt
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.NetworkType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
open class FmtBase {
|
||||||
|
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
|
||||||
|
val query = if (dicQuery != null)
|
||||||
|
("?" + dicQuery.toList().joinToString(
|
||||||
|
separator = "&",
|
||||||
|
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
|
||||||
|
else ""
|
||||||
|
|
||||||
|
val url = String.format(
|
||||||
|
"%s@%s:%s",
|
||||||
|
Utils.urlEncode(userInfo ?: ""),
|
||||||
|
Utils.getIpv6Address(config.server),
|
||||||
|
config.serverPort
|
||||||
|
)
|
||||||
|
|
||||||
|
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getQueryParam(uri: URI): Map<String, String> {
|
||||||
|
return uri.rawQuery.split("&")
|
||||||
|
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
|
||||||
|
val dicQuery = HashMap<String, String>()
|
||||||
|
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
|
||||||
|
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
||||||
|
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
||||||
|
config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() }
|
||||||
|
config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
|
||||||
|
config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
|
||||||
|
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
|
||||||
|
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
|
||||||
|
|
||||||
|
val networkType = NetworkType.fromString(config.network)
|
||||||
|
dicQuery["type"] = networkType.type
|
||||||
|
|
||||||
|
when (networkType) {
|
||||||
|
NetworkType.TCP -> {
|
||||||
|
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||||
|
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.KCP -> {
|
||||||
|
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||||
|
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.WS, NetworkType.HTTP_UPGRADE, NetworkType.SPLIT_HTTP -> {
|
||||||
|
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||||
|
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.HTTP, NetworkType.H2 -> {
|
||||||
|
dicQuery["type"] = "http"
|
||||||
|
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
|
||||||
|
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.QUIC -> {
|
||||||
|
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
|
||||||
|
config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
|
||||||
|
config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.GRPC -> {
|
||||||
|
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
|
||||||
|
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
|
||||||
|
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dicQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/HttpFmt.kt
Normal file
28
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/HttpFmt.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import kotlin.text.orEmpty
|
||||||
|
|
||||||
|
object HttpFmt : FmtBase() {
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.HTTP)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = profileItem.server.orEmpty()
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
|
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
socksUsersBean.user = profileItem.username.orEmpty()
|
||||||
|
socksUsersBean.pass = profileItem.password.orEmpty()
|
||||||
|
server.users = listOf(socksUsersBean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
120
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/Hysteria2Fmt.kt
Normal file
120
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/Hysteria2Fmt.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.Hysteria2Bean
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object Hysteria2Fmt : FmtBase() {
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
config.server = uri.idnHost
|
||||||
|
config.serverPort = uri.port.toString()
|
||||||
|
config.password = uri.userInfo
|
||||||
|
config.security = AppConfig.TLS
|
||||||
|
|
||||||
|
if (!uri.rawQuery.isNullOrEmpty()) {
|
||||||
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
|
config.security = queryParam["security"] ?: AppConfig.TLS
|
||||||
|
config.insecure = if (queryParam["insecure"].isNullOrEmpty()) {
|
||||||
|
allowInsecure
|
||||||
|
} else {
|
||||||
|
queryParam["insecure"].orEmpty() == "1"
|
||||||
|
}
|
||||||
|
config.sni = queryParam["sni"]
|
||||||
|
config.alpn = queryParam["alpn"]
|
||||||
|
|
||||||
|
config.obfsPassword = queryParam["obfs-password"]
|
||||||
|
config.portHopping = queryParam["mport"]
|
||||||
|
config.pinSHA256 = queryParam["pinSHA256"]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ProfileItem): String {
|
||||||
|
val dicQuery = HashMap<String, String>()
|
||||||
|
|
||||||
|
config.security.let { if (it != null) dicQuery["security"] = it }
|
||||||
|
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
|
||||||
|
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
|
||||||
|
config.insecure.let { dicQuery["insecure"] = if (it == true) "1" else "0" }
|
||||||
|
|
||||||
|
if (config.obfsPassword.isNotNullEmpty()) {
|
||||||
|
dicQuery["obfs"] = "salamander"
|
||||||
|
dicQuery["obfs-password"] = config.obfsPassword.orEmpty()
|
||||||
|
}
|
||||||
|
if (config.portHopping.isNotNullEmpty()) {
|
||||||
|
dicQuery["mport"] = config.portHopping.orEmpty()
|
||||||
|
}
|
||||||
|
if (config.pinSHA256.isNotNullEmpty()) {
|
||||||
|
dicQuery["pinSHA256"] = config.pinSHA256.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
return toUri(config, config.password, dicQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
|
||||||
|
|
||||||
|
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
|
||||||
|
Hysteria2Bean.ObfsBean(
|
||||||
|
type = "salamander",
|
||||||
|
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
|
||||||
|
password = config.obfsPassword
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val transport = if (config.portHopping.isNullOrEmpty()) null else
|
||||||
|
Hysteria2Bean.TransportBean(
|
||||||
|
type = "udp",
|
||||||
|
udp = Hysteria2Bean.TransportBean.TransportUdpBean(
|
||||||
|
hopInterval = (config.portHoppingInterval ?: "30") + "s"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val server =
|
||||||
|
if (config.portHopping.isNullOrEmpty())
|
||||||
|
config.getServerAddressAndPort()
|
||||||
|
else
|
||||||
|
Utils.getIpv6Address(config.server) + ":" + config.portHopping
|
||||||
|
|
||||||
|
val bean = Hysteria2Bean(
|
||||||
|
server = server,
|
||||||
|
auth = config.password,
|
||||||
|
obfs = obfs,
|
||||||
|
transport = transport,
|
||||||
|
socks5 = Hysteria2Bean.Socks5Bean(
|
||||||
|
listen = "$LOOPBACK:${socksPort}",
|
||||||
|
),
|
||||||
|
http = Hysteria2Bean.Socks5Bean(
|
||||||
|
listen = "$LOOPBACK:${socksPort}",
|
||||||
|
),
|
||||||
|
tls = Hysteria2Bean.TlsBean(
|
||||||
|
sni = config.sni ?: config.server,
|
||||||
|
insecure = config.insecure,
|
||||||
|
pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return bean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
108
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/ShadowsocksFmt.kt
Normal file
108
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/ShadowsocksFmt.kt
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object ShadowsocksFmt : FmtBase() {
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
return parseSip002(str) ?: parseLegacy(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseSip002(str: String): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.idnHost.isEmpty()) return null
|
||||||
|
if (uri.port <= 0) return null
|
||||||
|
if (uri.userInfo.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
config.server = uri.idnHost
|
||||||
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
val result = if (uri.userInfo.contains(":")) {
|
||||||
|
uri.userInfo.split(":", limit = 2)
|
||||||
|
} else {
|
||||||
|
Utils.decode(uri.userInfo).split(":", limit = 2)
|
||||||
|
}
|
||||||
|
if (result.count() == 2) {
|
||||||
|
config.method = result.first()
|
||||||
|
config.password = result.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uri.rawQuery.isNullOrEmpty()) {
|
||||||
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
|
if (queryParam["plugin"] == "obfs-local" && queryParam["obfs"] == "http") {
|
||||||
|
config.network = "tcp"
|
||||||
|
config.headerType = "http"
|
||||||
|
config.host = queryParam["obfs-host"]
|
||||||
|
config.path = queryParam["path"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseLegacy(str: String): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
|
||||||
|
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||||
|
val indexSplit = result.indexOf("#")
|
||||||
|
if (indexSplit > 0) {
|
||||||
|
try {
|
||||||
|
config.remarks =
|
||||||
|
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.substring(0, indexSplit)
|
||||||
|
}
|
||||||
|
|
||||||
|
//part decode
|
||||||
|
val indexS = result.indexOf("@")
|
||||||
|
result = if (indexS > 0) {
|
||||||
|
Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||||
|
indexS,
|
||||||
|
result.length
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Utils.decode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
|
||||||
|
val match = legacyPattern.matchEntire(result) ?: return null
|
||||||
|
|
||||||
|
config.server = match.groupValues[3].removeSurrounding("[", "]")
|
||||||
|
config.serverPort = match.groupValues[4]
|
||||||
|
config.password = match.groupValues[2]
|
||||||
|
config.method = match.groupValues[1].lowercase()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ProfileItem): String {
|
||||||
|
val pw = "${config.method}:${config.password}"
|
||||||
|
|
||||||
|
return toUri(config, Utils.encode(pw), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = profileItem.server.orEmpty()
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
server.password = profileItem.password
|
||||||
|
server.method = profileItem.method
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
62
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/SocksFmt.kt
Normal file
62
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/SocksFmt.kt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
import kotlin.text.orEmpty
|
||||||
|
|
||||||
|
object SocksFmt : FmtBase() {
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.idnHost.isEmpty()) return null
|
||||||
|
if (uri.port <= 0) return null
|
||||||
|
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
config.server = uri.idnHost
|
||||||
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
if (uri.userInfo?.isEmpty() == false) {
|
||||||
|
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
|
||||||
|
if (result.count() == 2) {
|
||||||
|
config.username = result.first()
|
||||||
|
config.password = result.last()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ProfileItem): String {
|
||||||
|
val pw =
|
||||||
|
if (config.username.isNotNullEmpty())
|
||||||
|
"${config.username}:${config.password}"
|
||||||
|
else
|
||||||
|
":"
|
||||||
|
|
||||||
|
return toUri(config, Utils.encode(pw), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = profileItem.server.orEmpty()
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
|
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
socksUsersBean.user = profileItem.username.orEmpty()
|
||||||
|
socksUsersBean.pass = profileItem.password.orEmpty()
|
||||||
|
server.users = listOf(socksUsersBean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
103
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/TrojanFmt.kt
Normal file
103
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/TrojanFmt.kt
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
import kotlin.text.orEmpty
|
||||||
|
|
||||||
|
object TrojanFmt : FmtBase() {
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
|
val config = ProfileItem.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
config.server = uri.idnHost
|
||||||
|
config.serverPort = uri.port.toString()
|
||||||
|
config.password = uri.userInfo
|
||||||
|
|
||||||
|
if (uri.rawQuery.isNullOrEmpty()) {
|
||||||
|
config.security = AppConfig.TLS
|
||||||
|
config.insecure = allowInsecure
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
|
config.network = queryParam["type"] ?: "tcp"
|
||||||
|
config.headerType = queryParam["headerType"]
|
||||||
|
config.host = queryParam["host"]
|
||||||
|
config.path = queryParam["path"]
|
||||||
|
|
||||||
|
config.seed = queryParam["seed"]
|
||||||
|
config.quicSecurity = queryParam["quicSecurity"]
|
||||||
|
config.quicKey = queryParam["key"]
|
||||||
|
config.mode = queryParam["mode"]
|
||||||
|
config.serviceName = queryParam["serviceName"]
|
||||||
|
config.authority = queryParam["authority"]
|
||||||
|
|
||||||
|
config.security = queryParam["security"]
|
||||||
|
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
||||||
|
allowInsecure
|
||||||
|
} else {
|
||||||
|
queryParam["allowInsecure"].orEmpty() == "1"
|
||||||
|
}
|
||||||
|
config.sni = queryParam["sni"]
|
||||||
|
config.fingerPrint = queryParam["fp"]
|
||||||
|
config.alpn = queryParam["alpn"]
|
||||||
|
config.publicKey = queryParam["pbk"]
|
||||||
|
config.shortId = queryParam["sid"]
|
||||||
|
config.spiderX = queryParam["spx"]
|
||||||
|
config.flow = queryParam["flow"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ProfileItem): String {
|
||||||
|
val dicQuery = getQueryDic(config)
|
||||||
|
|
||||||
|
return toUri(config, config.password, dicQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = profileItem.server.orEmpty()
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
server.password = profileItem.password
|
||||||
|
server.flow = profileItem.flow
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
|
profileItem.network.orEmpty(),
|
||||||
|
profileItem.headerType,
|
||||||
|
profileItem.host,
|
||||||
|
profileItem.path,
|
||||||
|
profileItem.seed,
|
||||||
|
profileItem.quicSecurity,
|
||||||
|
profileItem.quicKey,
|
||||||
|
profileItem.mode,
|
||||||
|
profileItem.serviceName,
|
||||||
|
profileItem.authority,
|
||||||
|
)
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
|
profileItem.security.orEmpty(),
|
||||||
|
profileItem.insecure == true,
|
||||||
|
profileItem.sni,
|
||||||
|
profileItem.fingerPrint,
|
||||||
|
profileItem.alpn,
|
||||||
|
profileItem.publicKey,
|
||||||
|
profileItem.shortId,
|
||||||
|
profileItem.spiderX,
|
||||||
|
)
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
}
|
||||||
104
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/VlessFmt.kt
Normal file
104
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/VlessFmt.kt
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object VlessFmt : FmtBase() {
|
||||||
|
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
|
val config = ProfileItem.create(EConfigType.VLESS)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
config.server = uri.idnHost
|
||||||
|
config.serverPort = uri.port.toString()
|
||||||
|
config.password = uri.userInfo
|
||||||
|
config.method = queryParam["encryption"] ?: "none"
|
||||||
|
|
||||||
|
config.network = queryParam["type"] ?: "tcp"
|
||||||
|
config.headerType = queryParam["headerType"]
|
||||||
|
config.host = queryParam["host"]
|
||||||
|
config.path = queryParam["path"]
|
||||||
|
|
||||||
|
config.seed = queryParam["seed"]
|
||||||
|
config.quicSecurity = queryParam["quicSecurity"]
|
||||||
|
config.quicKey = queryParam["key"]
|
||||||
|
config.mode = queryParam["mode"]
|
||||||
|
config.serviceName = queryParam["serviceName"]
|
||||||
|
config.authority = queryParam["authority"]
|
||||||
|
|
||||||
|
config.security = queryParam["security"]
|
||||||
|
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
|
||||||
|
allowInsecure
|
||||||
|
} else {
|
||||||
|
queryParam["allowInsecure"].orEmpty() == "1"
|
||||||
|
}
|
||||||
|
config.sni = queryParam["sni"]
|
||||||
|
config.fingerPrint = queryParam["fp"]
|
||||||
|
config.alpn = queryParam["alpn"]
|
||||||
|
config.publicKey = queryParam["pbk"]
|
||||||
|
config.shortId = queryParam["sid"]
|
||||||
|
config.spiderX = queryParam["spx"]
|
||||||
|
config.flow = queryParam["flow"]
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ProfileItem): String {
|
||||||
|
val dicQuery = getQueryDic(config)
|
||||||
|
dicQuery["encryption"] = config.method ?: "none"
|
||||||
|
|
||||||
|
return toUri(config, config.password, dicQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.VLESS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
|
vnext.address = profileItem.server.orEmpty()
|
||||||
|
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
vnext.users[0].id = profileItem.password.orEmpty()
|
||||||
|
vnext.users[0].encryption = profileItem.method
|
||||||
|
vnext.users[0].flow = profileItem.flow
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
|
profileItem.network.orEmpty(),
|
||||||
|
profileItem.headerType,
|
||||||
|
profileItem.host,
|
||||||
|
profileItem.path,
|
||||||
|
profileItem.seed,
|
||||||
|
profileItem.quicSecurity,
|
||||||
|
profileItem.quicKey,
|
||||||
|
profileItem.mode,
|
||||||
|
profileItem.serviceName,
|
||||||
|
profileItem.authority,
|
||||||
|
)
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
|
profileItem.security.orEmpty(),
|
||||||
|
profileItem.insecure == true,
|
||||||
|
profileItem.sni,
|
||||||
|
profileItem.fingerPrint,
|
||||||
|
profileItem.alpn,
|
||||||
|
profileItem.publicKey,
|
||||||
|
profileItem.shortId,
|
||||||
|
profileItem.spiderX,
|
||||||
|
)
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
199
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/VmessFmt.kt
Normal file
199
V2rayNG/app/src/main/kotlin/com/v2ray/ang/fmt/VmessFmt.kt
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.NetworkType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.dto.VmessQRCode
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
import kotlin.text.orEmpty
|
||||||
|
|
||||||
|
object VmessFmt : FmtBase() {
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
||||||
|
return parseVmessStd(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
|
val config = ProfileItem.create(EConfigType.VMESS)
|
||||||
|
|
||||||
|
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
||||||
|
result = Utils.decode(result)
|
||||||
|
if (TextUtils.isEmpty(result)) {
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
|
||||||
|
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
||||||
|
if (TextUtils.isEmpty(vmessQRCode.add)
|
||||||
|
|| TextUtils.isEmpty(vmessQRCode.port)
|
||||||
|
|| TextUtils.isEmpty(vmessQRCode.id)
|
||||||
|
|| TextUtils.isEmpty(vmessQRCode.net)
|
||||||
|
) {
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
config.remarks = vmessQRCode.ps
|
||||||
|
config.server = vmessQRCode.add
|
||||||
|
config.serverPort = vmessQRCode.port
|
||||||
|
config.password = vmessQRCode.id
|
||||||
|
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
||||||
|
|
||||||
|
config.network = vmessQRCode.net ?: "tcp"
|
||||||
|
config.headerType = vmessQRCode.type
|
||||||
|
config.host = vmessQRCode.host
|
||||||
|
config.path = vmessQRCode.path
|
||||||
|
|
||||||
|
when (NetworkType.fromString(config.network)) {
|
||||||
|
NetworkType.KCP -> {
|
||||||
|
config.seed = vmessQRCode.path
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.QUIC -> {
|
||||||
|
config.quicSecurity = vmessQRCode.host
|
||||||
|
config.quicKey = vmessQRCode.path
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.GRPC -> {
|
||||||
|
config.mode = vmessQRCode.type
|
||||||
|
config.serviceName = vmessQRCode.path
|
||||||
|
config.authority = vmessQRCode.host
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.security = vmessQRCode.tls
|
||||||
|
config.insecure = allowInsecure
|
||||||
|
config.sni = vmessQRCode.sni
|
||||||
|
config.fingerPrint = vmessQRCode.fp
|
||||||
|
config.alpn = vmessQRCode.alpn
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUri(config: ProfileItem): String {
|
||||||
|
val vmessQRCode = VmessQRCode()
|
||||||
|
|
||||||
|
vmessQRCode.v = "2"
|
||||||
|
vmessQRCode.ps = config.remarks
|
||||||
|
vmessQRCode.add = config.server.orEmpty()
|
||||||
|
vmessQRCode.port = config.serverPort.orEmpty()
|
||||||
|
vmessQRCode.id = config.password.orEmpty()
|
||||||
|
vmessQRCode.scy = config.method.orEmpty()
|
||||||
|
vmessQRCode.aid = "0"
|
||||||
|
|
||||||
|
vmessQRCode.net = config.network.orEmpty()
|
||||||
|
vmessQRCode.type = config.headerType.orEmpty()
|
||||||
|
when (NetworkType.fromString(config.network)) {
|
||||||
|
NetworkType.KCP -> {
|
||||||
|
vmessQRCode.path = config.seed.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.QUIC -> {
|
||||||
|
vmessQRCode.host = config.quicSecurity.orEmpty()
|
||||||
|
vmessQRCode.path = config.quicKey.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.GRPC -> {
|
||||||
|
vmessQRCode.type = config.mode.orEmpty()
|
||||||
|
vmessQRCode.path = config.serviceName.orEmpty()
|
||||||
|
vmessQRCode.host = config.authority.orEmpty()
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
|
||||||
|
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
|
||||||
|
|
||||||
|
vmessQRCode.tls = config.security.orEmpty()
|
||||||
|
vmessQRCode.sni = config.sni.orEmpty()
|
||||||
|
vmessQRCode.fp = config.fingerPrint.orEmpty()
|
||||||
|
vmessQRCode.alpn = config.alpn.orEmpty()
|
||||||
|
|
||||||
|
val json = JsonUtil.toJson(vmessQRCode)
|
||||||
|
return Utils.encode(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseVmessStd(str: String): ProfileItem? {
|
||||||
|
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
|
||||||
|
val config = ProfileItem.create(EConfigType.VMESS)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
config.server = uri.idnHost
|
||||||
|
config.serverPort = uri.port.toString()
|
||||||
|
config.password = uri.userInfo
|
||||||
|
config.method = AppConfig.DEFAULT_SECURITY
|
||||||
|
|
||||||
|
config.network = NetworkType.fromString(queryParam["type"]).name
|
||||||
|
config.headerType = queryParam["headerType"]
|
||||||
|
config.host = queryParam["host"]
|
||||||
|
config.path = queryParam["path"]
|
||||||
|
|
||||||
|
config.seed = queryParam["seed"]
|
||||||
|
config.quicSecurity = queryParam["quicSecurity"]
|
||||||
|
config.quicKey = queryParam["key"]
|
||||||
|
config.mode = queryParam["mode"]
|
||||||
|
config.serviceName = queryParam["serviceName"]
|
||||||
|
config.authority = queryParam["authority"]
|
||||||
|
|
||||||
|
config.security = queryParam["security"]
|
||||||
|
config.insecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
||||||
|
config.sni = queryParam["sni"]
|
||||||
|
config.fingerPrint = queryParam["fp"]
|
||||||
|
config.alpn = queryParam["alpn"]
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.VMESS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
|
vnext.address = profileItem.server.orEmpty()
|
||||||
|
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
vnext.users[0].id = profileItem.password.orEmpty()
|
||||||
|
vnext.users[0].security = profileItem.method
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.populateTransportSettings(
|
||||||
|
profileItem.network.orEmpty(),
|
||||||
|
profileItem.headerType,
|
||||||
|
profileItem.host,
|
||||||
|
profileItem.path,
|
||||||
|
profileItem.seed,
|
||||||
|
profileItem.quicSecurity,
|
||||||
|
profileItem.quicKey,
|
||||||
|
profileItem.mode,
|
||||||
|
profileItem.serviceName,
|
||||||
|
profileItem.authority,
|
||||||
|
)
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
|
profileItem.security.orEmpty(),
|
||||||
|
profileItem.insecure == true,
|
||||||
|
profileItem.sni,
|
||||||
|
profileItem.fingerPrint,
|
||||||
|
profileItem.alpn,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
import kotlin.text.orEmpty
|
||||||
|
|
||||||
|
object WireguardFmt : FmtBase() {
|
||||||
|
fun parse(str: String): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||||
|
val queryParam = getQueryParam(uri)
|
||||||
|
|
||||||
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
config.server = uri.idnHost
|
||||||
|
config.serverPort = uri.port.toString()
|
||||||
|
|
||||||
|
config.secretKey = uri.userInfo
|
||||||
|
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
||||||
|
config.publicKey = queryParam["publickey"].orEmpty()
|
||||||
|
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
|
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseWireguardConfFile(str: String): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
val queryParam: MutableMap<String, String> = mutableMapOf()
|
||||||
|
|
||||||
|
var currentSection: String? = null
|
||||||
|
|
||||||
|
str.lines().forEach { line ->
|
||||||
|
val trimmedLine = line.trim()
|
||||||
|
|
||||||
|
when {
|
||||||
|
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
|
||||||
|
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
|
||||||
|
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
|
||||||
|
currentSection != null -> {
|
||||||
|
val (key, value) = trimmedLine.split("=").map { it.trim() }
|
||||||
|
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.secretKey = queryParam["privatekey"].orEmpty()
|
||||||
|
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
|
||||||
|
config.publicKey = queryParam["publickey"].orEmpty()
|
||||||
|
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
|
config.reserved = (queryParam["reserved"] ?: "0,0,0")
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun toUri(config: ProfileItem): String {
|
||||||
|
val dicQuery = HashMap<String, String>()
|
||||||
|
|
||||||
|
dicQuery["publickey"] = config.publicKey.orEmpty()
|
||||||
|
if (config.reserved != null) {
|
||||||
|
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty()
|
||||||
|
}
|
||||||
|
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty()
|
||||||
|
if (config.mtu != null) {
|
||||||
|
dicQuery["mtu"] = config.mtu.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return toUri(config, config.secretKey, dicQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
|
outboundBean?.settings?.let { wireguard ->
|
||||||
|
wireguard.secretKey = profileItem.secretKey
|
||||||
|
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
|
||||||
|
wireguard.peers?.first()?.publicKey = profileItem.publicKey.orEmpty()
|
||||||
|
wireguard.peers?.first()?.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||||
|
wireguard.mtu = profileItem.mtu?.toInt()
|
||||||
|
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.HY2
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.dto.*
|
||||||
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
|
import com.v2ray.ang.fmt.ShadowsocksFmt
|
||||||
|
import com.v2ray.ang.fmt.SocksFmt
|
||||||
|
import com.v2ray.ang.fmt.TrojanFmt
|
||||||
|
import com.v2ray.ang.fmt.VlessFmt
|
||||||
|
import com.v2ray.ang.fmt.VmessFmt
|
||||||
|
import com.v2ray.ang.fmt.WireguardFmt
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object AngConfigManager {
|
||||||
|
/**
|
||||||
|
* parse config form qrcode or...
|
||||||
|
*/
|
||||||
|
private fun parseConfig(
|
||||||
|
str: String?,
|
||||||
|
subid: String,
|
||||||
|
subItem: SubscriptionItem?,
|
||||||
|
removedSelectedServer: ProfileItem?
|
||||||
|
): Int {
|
||||||
|
try {
|
||||||
|
if (str == null || TextUtils.isEmpty(str)) {
|
||||||
|
return R.string.toast_none_data
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
||||||
|
VmessFmt.parse(str)
|
||||||
|
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
|
||||||
|
ShadowsocksFmt.parse(str)
|
||||||
|
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
|
||||||
|
SocksFmt.parse(str)
|
||||||
|
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
|
||||||
|
TrojanFmt.parse(str)
|
||||||
|
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
|
||||||
|
VlessFmt.parse(str)
|
||||||
|
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
|
||||||
|
WireguardFmt.parse(str)
|
||||||
|
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
|
||||||
|
Hysteria2Fmt.parse(str)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config == null) {
|
||||||
|
return R.string.toast_incorrect_protocol
|
||||||
|
}
|
||||||
|
//filter
|
||||||
|
if (subItem?.filter != null && subItem.filter?.isNotEmpty() == true && config.remarks.isNotEmpty()) {
|
||||||
|
val matched = Regex(pattern = subItem.filter ?: "")
|
||||||
|
.containsMatchIn(input = config.remarks)
|
||||||
|
if (!matched) return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
config.subscriptionId = subid
|
||||||
|
val guid = MmkvManager.encodeServerConfig("", config)
|
||||||
|
if (removedSelectedServer != null &&
|
||||||
|
config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort
|
||||||
|
) {
|
||||||
|
MmkvManager.setSelectServer(guid)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* share config
|
||||||
|
*/
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
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 {
|
||||||
|
var count = 0
|
||||||
|
try {
|
||||||
|
MmkvManager.decodeSubscriptions().forEach {
|
||||||
|
count += updateConfigViaSub(it)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
|
||||||
|
try {
|
||||||
|
if (TextUtils.isEmpty(it.first)
|
||||||
|
|| TextUtils.isEmpty(it.second.remarks)
|
||||||
|
|| TextUtils.isEmpty(it.second.url)
|
||||||
|
) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (!it.second.enabled) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
val url = Utils.idnToASCII(it.second.url)
|
||||||
|
if (!Utils.isValidUrl(url)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
Log.d(AppConfig.ANG_PACKAGE, url)
|
||||||
|
|
||||||
|
var configText = try {
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
|
||||||
|
//e.printStackTrace()
|
||||||
|
""
|
||||||
|
}
|
||||||
|
if (configText.isEmpty()) {
|
||||||
|
configText = try {
|
||||||
|
Utils.getUrlContentWithCustomUserAgent(url)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (configText.isEmpty()) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return parseConfigViaSub(configText, it.first, false)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
|
||||||
|
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
||||||
|
if (count <= 0) {
|
||||||
|
count = parseBatchConfig(server, subid, append)
|
||||||
|
}
|
||||||
|
if (count <= 0) {
|
||||||
|
count = parseCustomConfigServer(server, subid)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importUrlAsSubscription(url: String): Int {
|
||||||
|
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
|
subscriptions.forEach {
|
||||||
|
if (it.second.url == url) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(url))
|
||||||
|
val subItem = SubscriptionItem()
|
||||||
|
subItem.remarks = uri.fragment ?: "import sub"
|
||||||
|
subItem.url = url
|
||||||
|
MmkvManager.encodeSubscription("", subItem)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
object MigrateManager {
|
||||||
|
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
||||||
|
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
|
fun migrateServerConfig2Profile(): Boolean {
|
||||||
|
if (serverStorage.count().toInt() == 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val serverList = serverStorage.allKeys() ?: return false
|
||||||
|
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
|
||||||
|
|
||||||
|
for (guid in serverList) {
|
||||||
|
var configOld = decodeServerConfigOld(guid) ?: continue
|
||||||
|
var config = decodeServerConfig(guid)
|
||||||
|
if (config != null) {
|
||||||
|
serverStorage.remove(guid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
config = migrateServerConfig2ProfileSub(configOld) ?: continue
|
||||||
|
config.subscriptionId = configOld.subscriptionId
|
||||||
|
|
||||||
|
MmkvManager.encodeServerConfig(guid, config)
|
||||||
|
|
||||||
|
//check and remove old
|
||||||
|
decodeServerConfig(guid) ?: continue
|
||||||
|
serverStorage.remove(guid)
|
||||||
|
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
|
||||||
|
}
|
||||||
|
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
|
||||||
|
return when (configOld.getProxyOutbound()?.protocol) {
|
||||||
|
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
||||||
|
EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
||||||
|
EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld)
|
||||||
|
EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld)
|
||||||
|
|
||||||
|
EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld)
|
||||||
|
EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld)
|
||||||
|
EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld)
|
||||||
|
EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld)
|
||||||
|
|
||||||
|
EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(configOld.configType)
|
||||||
|
|
||||||
|
val outbound = configOld.getProxyOutbound() ?: return null
|
||||||
|
config.remarks = configOld.remarks
|
||||||
|
config.server = outbound.getServerAddress()
|
||||||
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
|
config.method = outbound.getSecurityEncryption()
|
||||||
|
config.password = outbound.getPassword()
|
||||||
|
config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
|
||||||
|
|
||||||
|
config.network = outbound?.streamSettings?.network ?: "tcp"
|
||||||
|
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||||
|
config.headerType = transportDetails[0].orEmpty()
|
||||||
|
config.host = transportDetails[1].orEmpty()
|
||||||
|
config.path = transportDetails[2].orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
config.seed = outbound?.streamSettings?.kcpSettings?.seed
|
||||||
|
config.quicSecurity = outbound?.streamSettings?.quicSettings?.security
|
||||||
|
config.quicKey = outbound?.streamSettings?.quicSettings?.key
|
||||||
|
config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun"
|
||||||
|
config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName
|
||||||
|
config.authority = outbound?.streamSettings?.grpcSettings?.authority
|
||||||
|
|
||||||
|
config.security = outbound.streamSettings?.security
|
||||||
|
val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings
|
||||||
|
config.insecure = tlsSettings?.allowInsecure
|
||||||
|
config.sni = tlsSettings?.serverName
|
||||||
|
config.fingerPrint = tlsSettings?.fingerprint
|
||||||
|
config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString()
|
||||||
|
|
||||||
|
config.publicKey = tlsSettings?.publicKey
|
||||||
|
config.shortId = tlsSettings?.shortId
|
||||||
|
config.spiderX = tlsSettings?.spiderX
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.SOCKS)
|
||||||
|
|
||||||
|
val outbound = configOld.getProxyOutbound() ?: return null
|
||||||
|
config.remarks = configOld.remarks
|
||||||
|
config.server = outbound.getServerAddress()
|
||||||
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
|
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
|
||||||
|
config.password = outbound.getPassword()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.HTTP)
|
||||||
|
|
||||||
|
val outbound = configOld.getProxyOutbound() ?: return null
|
||||||
|
config.remarks = configOld.remarks
|
||||||
|
config.server = outbound.getServerAddress()
|
||||||
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
|
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
|
||||||
|
config.password = outbound.getPassword()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
|
val outbound = configOld.getProxyOutbound() ?: return null
|
||||||
|
config.remarks = configOld.remarks
|
||||||
|
config.server = outbound.getServerAddress()
|
||||||
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
|
|
||||||
|
outbound.settings?.let { wireguard ->
|
||||||
|
config.secretKey = wireguard.secretKey
|
||||||
|
config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString()
|
||||||
|
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
|
||||||
|
config.mtu = wireguard.mtu
|
||||||
|
config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString()
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
|
val outbound = configOld.getProxyOutbound() ?: return null
|
||||||
|
config.remarks = configOld.remarks
|
||||||
|
config.server = outbound.getServerAddress()
|
||||||
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
|
config.password = outbound.getPassword()
|
||||||
|
|
||||||
|
config.security = AppConfig.TLS
|
||||||
|
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
|
||||||
|
config.insecure = tlsSetting.allowInsecure
|
||||||
|
config.sni = tlsSetting.serverName
|
||||||
|
config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty()
|
||||||
|
|
||||||
|
}
|
||||||
|
config.obfsPassword = outbound.settings?.obfsPassword
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
|
||||||
|
val config = ProfileItem.create(EConfigType.CUSTOM)
|
||||||
|
|
||||||
|
val outbound = configOld.getProxyOutbound() ?: return null
|
||||||
|
config.remarks = configOld.remarks
|
||||||
|
config.server = outbound.getServerAddress()
|
||||||
|
config.serverPort = outbound.getServerPort().toString()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun decodeServerConfigOld(guid: String): ServerConfig? {
|
||||||
|
if (guid.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val json = serverStorage.decodeString(guid)
|
||||||
|
if (json.isNullOrBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return JsonUtil.fromJson(json, ServerConfig::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
368
V2rayNG/app/src/main/kotlin/com/v2ray/ang/handler/MmkvManager.kt
Normal file
368
V2rayNG/app/src/main/kotlin/com/v2ray/ang/handler/MmkvManager.kt
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
|
||||||
|
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
|
||||||
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
|
import com.v2ray.ang.dto.ServerAffiliationInfo
|
||||||
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
object MmkvManager {
|
||||||
|
|
||||||
|
//region private
|
||||||
|
|
||||||
|
//private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
|
||||||
|
private const val ID_MAIN = "MAIN"
|
||||||
|
private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
|
||||||
|
private const val ID_SERVER_RAW = "SERVER_RAW"
|
||||||
|
private const val ID_SERVER_AFF = "SERVER_AFF"
|
||||||
|
private const val ID_SUB = "SUB"
|
||||||
|
private const val ID_ASSET = "ASSET"
|
||||||
|
private const val ID_SETTING = "SETTING"
|
||||||
|
private const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
|
||||||
|
private const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
|
||||||
|
private const val KEY_SUB_IDS = "SUB_IDS"
|
||||||
|
|
||||||
|
//private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
private val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Server
|
||||||
|
|
||||||
|
fun getSelectServer(): String? {
|
||||||
|
return mainStorage.decodeString(KEY_SELECTED_SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectServer(guid: String) {
|
||||||
|
mainStorage.encode(KEY_SELECTED_SERVER, guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeServerList(serverList: MutableList<String>) {
|
||||||
|
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeServerList(): MutableList<String> {
|
||||||
|
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
||||||
|
return if (json.isNullOrBlank()) {
|
||||||
|
mutableListOf()
|
||||||
|
} else {
|
||||||
|
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun decodeServerConfig(guid: String): ProfileItem? {
|
||||||
|
if (guid.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val json = profileFullStorage.decodeString(guid)
|
||||||
|
if (json.isNullOrBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return JsonUtil.fromJson(json, ProfileItem::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun decodeProfileConfig(guid: String): ProfileLiteItem? {
|
||||||
|
// if (guid.isBlank()) {
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
// val json = profileStorage.decodeString(guid)
|
||||||
|
// if (json.isNullOrBlank()) {
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun encodeServerConfig(guid: String, config: ProfileItem): String {
|
||||||
|
val key = guid.ifBlank { Utils.getUuid() }
|
||||||
|
profileFullStorage.encode(key, JsonUtil.toJson(config))
|
||||||
|
val serverList = decodeServerList()
|
||||||
|
if (!serverList.contains(key)) {
|
||||||
|
serverList.add(0, key)
|
||||||
|
encodeServerList(serverList)
|
||||||
|
if (getSelectServer().isNullOrBlank()) {
|
||||||
|
mainStorage.encode(KEY_SELECTED_SERVER, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// val profile = ProfileLiteItem(
|
||||||
|
// configType = config.configType,
|
||||||
|
// subscriptionId = config.subscriptionId,
|
||||||
|
// remarks = config.remarks,
|
||||||
|
// server = config.getProxyOutbound()?.getServerAddress(),
|
||||||
|
// serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||||
|
// )
|
||||||
|
// profileStorage.encode(key, JsonUtil.toJson(profile))
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeServer(guid: String) {
|
||||||
|
if (guid.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (getSelectServer() == guid) {
|
||||||
|
mainStorage.remove(KEY_SELECTED_SERVER)
|
||||||
|
}
|
||||||
|
val serverList = decodeServerList()
|
||||||
|
serverList.remove(guid)
|
||||||
|
encodeServerList(serverList)
|
||||||
|
profileFullStorage.remove(guid)
|
||||||
|
//profileStorage.remove(guid)
|
||||||
|
serverAffStorage.remove(guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeServerViaSubid(subid: String) {
|
||||||
|
if (subid.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profileFullStorage.allKeys()?.forEach { key ->
|
||||||
|
decodeServerConfig(key)?.let { config ->
|
||||||
|
if (config.subscriptionId == subid) {
|
||||||
|
removeServer(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
|
||||||
|
if (guid.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val json = serverAffStorage.decodeString(guid)
|
||||||
|
if (json.isNullOrBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
|
||||||
|
if (guid.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
|
||||||
|
aff.testDelayMillis = testResult
|
||||||
|
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAllTestDelayResults(keys: List<String>?) {
|
||||||
|
keys?.forEach { key ->
|
||||||
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
|
aff.testDelayMillis = 0
|
||||||
|
serverAffStorage.encode(key, JsonUtil.toJson(aff))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAllServer() {
|
||||||
|
mainStorage.clearAll()
|
||||||
|
profileFullStorage.clearAll()
|
||||||
|
//profileStorage.clearAll()
|
||||||
|
serverAffStorage.clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeInvalidServer(guid: String) {
|
||||||
|
if (guid.isNotEmpty()) {
|
||||||
|
decodeServerAffiliationInfo(guid)?.let { aff ->
|
||||||
|
if (aff.testDelayMillis < 0L) {
|
||||||
|
removeServer(guid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serverAffStorage.allKeys()?.forEach { key ->
|
||||||
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
|
if (aff.testDelayMillis < 0L) {
|
||||||
|
removeServer(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeServerRaw(guid: String, config: String) {
|
||||||
|
serverRawStorage.encode(guid, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeServerRaw(guid: String): String? {
|
||||||
|
return serverRawStorage.decodeString(guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Subscriptions
|
||||||
|
|
||||||
|
private fun initSubsList() {
|
||||||
|
val subsList = decodeSubsList()
|
||||||
|
if (subsList.isNotEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subStorage.allKeys()?.forEach { key ->
|
||||||
|
subsList.add(key)
|
||||||
|
}
|
||||||
|
encodeSubsList(subsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
|
||||||
|
initSubsList()
|
||||||
|
|
||||||
|
val subscriptions = mutableListOf<Pair<String, SubscriptionItem>>()
|
||||||
|
decodeSubsList().forEach { key ->
|
||||||
|
val json = subStorage.decodeString(key)
|
||||||
|
if (!json.isNullOrBlank()) {
|
||||||
|
subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeSubscription(subid: String) {
|
||||||
|
subStorage.remove(subid)
|
||||||
|
val subsList = decodeSubsList()
|
||||||
|
subsList.remove(subid)
|
||||||
|
encodeSubsList(subsList)
|
||||||
|
|
||||||
|
removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
|
||||||
|
val key = guid.ifBlank { Utils.getUuid() }
|
||||||
|
subStorage.encode(key, JsonUtil.toJson(subItem))
|
||||||
|
|
||||||
|
val subsList = decodeSubsList()
|
||||||
|
if (!subsList.contains(key)) {
|
||||||
|
subsList.add(key)
|
||||||
|
encodeSubsList(subsList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
|
||||||
|
val json = subStorage.decodeString(subscriptionId) ?: return null
|
||||||
|
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeSubsList(subsList: MutableList<String>) {
|
||||||
|
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeSubsList(): MutableList<String> {
|
||||||
|
val json = mainStorage.decodeString(KEY_SUB_IDS)
|
||||||
|
return if (json.isNullOrBlank()) {
|
||||||
|
mutableListOf()
|
||||||
|
} else {
|
||||||
|
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Asset
|
||||||
|
|
||||||
|
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
||||||
|
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||||
|
assetStorage.allKeys()?.forEach { key ->
|
||||||
|
val json = assetStorage.decodeString(key)
|
||||||
|
if (!json.isNullOrBlank()) {
|
||||||
|
assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAssetUrl(assetid: String) {
|
||||||
|
assetStorage.remove(assetid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
|
||||||
|
val key = assetid.ifBlank { Utils.getUuid() }
|
||||||
|
assetStorage.encode(key, JsonUtil.toJson(assetItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeAsset(assetid: String): AssetUrlItem? {
|
||||||
|
val json = assetStorage.decodeString(assetid) ?: return null
|
||||||
|
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Routing
|
||||||
|
|
||||||
|
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
||||||
|
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
||||||
|
if (ruleset.isNullOrEmpty()) return null
|
||||||
|
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
|
||||||
|
if (rulesetList.isNullOrEmpty())
|
||||||
|
encodeSettings(PREF_ROUTING_RULESET, "")
|
||||||
|
else
|
||||||
|
encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
fun encodeSettings(key: String, value: String?): Boolean {
|
||||||
|
return settingsStorage.encode(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeSettings(key: String, value: Int): Boolean {
|
||||||
|
return settingsStorage.encode(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeSettings(key: String, value: Boolean): Boolean {
|
||||||
|
return settingsStorage.encode(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
|
||||||
|
return settingsStorage.encode(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun decodeSettingsString(key: String): String? {
|
||||||
|
return settingsStorage.decodeString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeSettingsString(key: String, defaultValue: String?): String? {
|
||||||
|
return settingsStorage.decodeString(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeSettingsBool(key: String): Boolean {
|
||||||
|
return settingsStorage.decodeBool(key,false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
|
||||||
|
return settingsStorage.decodeBool(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeSettingsInt(key: String, defaultValue: Int): Int {
|
||||||
|
return settingsStorage.decodeInt(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
|
||||||
|
return settingsStorage.decodeStringSet(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Others
|
||||||
|
|
||||||
|
fun encodeStartOnBoot(startOnBoot: Boolean) {
|
||||||
|
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeStartOnBoot(): Boolean {
|
||||||
|
return decodeSettingsBool(PREF_IS_BOOTED, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
||||||
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.RoutingType
|
||||||
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
|
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
|
||||||
|
import com.v2ray.ang.handler.MmkvManager.decodeServerList
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import com.v2ray.ang.util.Utils.parseInt
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.Collections
|
||||||
|
import kotlin.Int
|
||||||
|
|
||||||
|
object SettingsManager {
|
||||||
|
|
||||||
|
fun initRoutingRulesets(context: Context) {
|
||||||
|
val exist = MmkvManager.decodeRoutingRulesets()
|
||||||
|
if (exist.isNullOrEmpty()) {
|
||||||
|
val rulesetList = getPresetRoutingRulesets(context)
|
||||||
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
|
||||||
|
val fileName = RoutingType.fromIndex(index).fileName
|
||||||
|
val assets = Utils.readTextFromAssets(context, fileName)
|
||||||
|
if (TextUtils.isEmpty(assets)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun resetRoutingRulesets(context: Context, index: Int) {
|
||||||
|
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
|
||||||
|
resetRoutingRulesetsCommon(rulesetList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetRoutingRulesetsFromClipboard(content: String?): Boolean {
|
||||||
|
if (content.isNullOrEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java).toMutableList()
|
||||||
|
if (rulesetList.isNullOrEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
resetRoutingRulesetsCommon(rulesetList)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
|
||||||
|
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
|
||||||
|
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
|
||||||
|
if (key.looked == true) {
|
||||||
|
rulesetNew.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesetNew.addAll(rulesetList)
|
||||||
|
MmkvManager.encodeRoutingRulesets(rulesetNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRoutingRuleset(index: Int): RulesetItem? {
|
||||||
|
if (index < 0) return null
|
||||||
|
|
||||||
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
|
if (rulesetList.isNullOrEmpty()) return null
|
||||||
|
|
||||||
|
return rulesetList[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
|
||||||
|
if (ruleset == null) return
|
||||||
|
|
||||||
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
|
if (rulesetList.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
if (index < 0 || index >= rulesetList.count()) {
|
||||||
|
rulesetList.add(ruleset)
|
||||||
|
} else {
|
||||||
|
rulesetList[index] = ruleset
|
||||||
|
}
|
||||||
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeRoutingRuleset(index: Int) {
|
||||||
|
if (index < 0) return
|
||||||
|
|
||||||
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
|
if (rulesetList.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
rulesetList.removeAt(index)
|
||||||
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun routingRulesetsBypassLan(): Boolean {
|
||||||
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
|
val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any {
|
||||||
|
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
|
||||||
|
}
|
||||||
|
return exist == true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
|
||||||
|
val rulesetList = MmkvManager.decodeRoutingRulesets()
|
||||||
|
if (rulesetList.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
Collections.swap(rulesetList, fromPosition, toPosition)
|
||||||
|
MmkvManager.encodeRoutingRulesets(rulesetList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
|
||||||
|
val subsList = MmkvManager.decodeSubsList()
|
||||||
|
if (subsList.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
Collections.swap(subsList, fromPosition, toPosition)
|
||||||
|
MmkvManager.encodeSubsList(subsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerViaRemarks(remarks: String?): ProfileItem? {
|
||||||
|
if (remarks == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val serverList = decodeServerList()
|
||||||
|
for (guid in serverList) {
|
||||||
|
val profile = decodeServerConfig(guid)
|
||||||
|
if (profile != null && profile.remarks == remarks) {
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSocksPort(): Int {
|
||||||
|
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHttpPort(): Int {
|
||||||
|
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initAssets(context: Context, assets: AssetManager) {
|
||||||
|
val extFolder = Utils.userAssetPath(context)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val geo = arrayOf("geosite.dat", "geoip.dat")
|
||||||
|
assets.list("")
|
||||||
|
?.filter { geo.contains(it) }
|
||||||
|
?.filter { !File(extFolder, it).exists() }
|
||||||
|
?.forEach {
|
||||||
|
val target = File(extFolder, it)
|
||||||
|
assets.open(it).use { input ->
|
||||||
|
FileOutputStream(target).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i(
|
||||||
|
ANG_PACKAGE,
|
||||||
|
"Copied from apk assets folder to ${target.absolutePath}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,629 @@
|
|||||||
|
package com.v2ray.ang.handler
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_ADDRESSES
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_DOMAIN
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_PUB_ADDRESSES
|
||||||
|
import com.v2ray.ang.AppConfig.DNS_PUB_DOMAIN
|
||||||
|
import com.v2ray.ang.AppConfig.GEOIP_CN
|
||||||
|
import com.v2ray.ang.AppConfig.GEOSITE_CN
|
||||||
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
|
import com.v2ray.ang.AppConfig.GOOGLEAPIS_CN_DOMAIN
|
||||||
|
import com.v2ray.ang.AppConfig.GOOGLEAPIS_COM_DOMAIN
|
||||||
|
import com.v2ray.ang.AppConfig.HEADER_TYPE_HTTP
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
|
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
|
||||||
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
||||||
|
import com.v2ray.ang.dto.ConfigResult
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
||||||
|
import com.v2ray.ang.fmt.HttpFmt
|
||||||
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
|
import com.v2ray.ang.fmt.ShadowsocksFmt
|
||||||
|
import com.v2ray.ang.fmt.SocksFmt
|
||||||
|
import com.v2ray.ang.fmt.TrojanFmt
|
||||||
|
import com.v2ray.ang.fmt.VlessFmt
|
||||||
|
import com.v2ray.ang.fmt.VmessFmt
|
||||||
|
import com.v2ray.ang.fmt.WireguardFmt
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
object V2rayConfigManager {
|
||||||
|
|
||||||
|
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
|
||||||
|
try {
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
||||||
|
if (config.configType == EConfigType.CUSTOM) {
|
||||||
|
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
|
||||||
|
val domainPort = config.getServerAddressAndPort()
|
||||||
|
return ConfigResult(true, guid, raw, domainPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = getV2rayNonCustomConfig(context, config)
|
||||||
|
Log.d(ANG_PACKAGE, result.content)
|
||||||
|
result.guid = guid
|
||||||
|
return result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return ConfigResult(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult {
|
||||||
|
val result = ConfigResult(false)
|
||||||
|
|
||||||
|
val address = config.server ?: return result
|
||||||
|
if (!Utils.isIpAddress(address)) {
|
||||||
|
if (!Utils.isValidUrl(address)) {
|
||||||
|
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//取得默认配置
|
||||||
|
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||||
|
if (TextUtils.isEmpty(assets)) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
|
||||||
|
v2rayConfig.log.loglevel =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
||||||
|
v2rayConfig.remarks = config.remarks
|
||||||
|
|
||||||
|
inbounds(v2rayConfig)
|
||||||
|
|
||||||
|
val isPlugin = config.configType == EConfigType.HYSTERIA2
|
||||||
|
val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
|
||||||
|
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
|
||||||
|
|
||||||
|
routing(v2rayConfig)
|
||||||
|
|
||||||
|
fakedns(v2rayConfig)
|
||||||
|
|
||||||
|
dns(v2rayConfig)
|
||||||
|
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||||
|
customLocalDns(v2rayConfig)
|
||||||
|
}
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
|
||||||
|
v2rayConfig.stats = null
|
||||||
|
v2rayConfig.policy = null
|
||||||
|
}
|
||||||
|
|
||||||
|
result.status = true
|
||||||
|
result.content = v2rayConfig.toPrettyPrinting()
|
||||||
|
result.domainPort = if (retMore.first) retMore.second else retOut.second
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
||||||
|
try {
|
||||||
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
|
||||||
|
v2rayConfig.inbounds.forEach { curInbound ->
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
||||||
|
//bind all inbounds to localhost if the user requests
|
||||||
|
curInbound.listen = LOOPBACK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v2rayConfig.inbounds[0].port = socksPort
|
||||||
|
val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
||||||
|
val sniffAllTlsAndHttp =
|
||||||
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
|
||||||
|
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
|
||||||
|
v2rayConfig.inbounds[0].sniffing?.routeOnly =
|
||||||
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
|
||||||
|
if (!sniffAllTlsAndHttp) {
|
||||||
|
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
|
||||||
|
}
|
||||||
|
if (fakedns) {
|
||||||
|
v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
|
||||||
|
}
|
||||||
|
|
||||||
|
v2rayConfig.inbounds[1].port = httpPort
|
||||||
|
|
||||||
|
// if (httpPort > 0) {
|
||||||
|
// val httpCopy = v2rayConfig.inbounds[0].copy()
|
||||||
|
// httpCopy.port = httpPort
|
||||||
|
// httpCopy.protocol = "http"
|
||||||
|
// v2rayConfig.inbounds.add(httpCopy)
|
||||||
|
// }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun outbounds(v2rayConfig: V2rayConfig, config: ProfileItem, isPlugin: Boolean): Pair<Boolean, String>? {
|
||||||
|
if (isPlugin) {
|
||||||
|
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
|
||||||
|
val outboundNew = V2rayConfig.OutboundBean(
|
||||||
|
mux = null,
|
||||||
|
protocol = EConfigType.SOCKS.name.lowercase(),
|
||||||
|
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||||
|
servers = listOf(
|
||||||
|
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean(
|
||||||
|
address = LOOPBACK,
|
||||||
|
port = socksPort
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (v2rayConfig.outbounds.isNotEmpty()) {
|
||||||
|
v2rayConfig.outbounds[0] = outboundNew
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.add(outboundNew)
|
||||||
|
}
|
||||||
|
return Pair(true, outboundNew.getServerAddressAndPort())
|
||||||
|
}
|
||||||
|
|
||||||
|
val outbound = getProxyOutbound(config) ?: return null
|
||||||
|
val ret = updateOutboundWithGlobalSettings(outbound)
|
||||||
|
if (!ret) return null
|
||||||
|
|
||||||
|
if (v2rayConfig.outbounds.isNotEmpty()) {
|
||||||
|
v2rayConfig.outbounds[0] = outbound
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.add(outbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOutboundFragment(v2rayConfig)
|
||||||
|
return Pair(true, config.getServerAddressAndPort())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fakedns(v2rayConfig: V2rayConfig) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
||||||
|
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
||||||
|
) {
|
||||||
|
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
||||||
|
try {
|
||||||
|
|
||||||
|
v2rayConfig.routing.domainStrategy =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
||||||
|
?: "IPIfNonMatch"
|
||||||
|
|
||||||
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
|
rulesetItems?.forEach { key ->
|
||||||
|
routingUserRule(key, v2rayConfig)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun routingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
|
||||||
|
try {
|
||||||
|
if (item == null || !item.enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
|
||||||
|
|
||||||
|
v2rayConfig.routing.rules.add(rule)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun userRule2Domain(tag: String): ArrayList<String> {
|
||||||
|
val domain = ArrayList<String>()
|
||||||
|
|
||||||
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
|
rulesetItems?.forEach { key ->
|
||||||
|
if (key != null && key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
|
||||||
|
key.domain?.forEach {
|
||||||
|
if (it != GEOSITE_PRIVATE
|
||||||
|
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
|
||||||
|
) {
|
||||||
|
domain.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
|
||||||
|
try {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
||||||
|
val geositeCn = arrayListOf(GEOSITE_CN)
|
||||||
|
val proxyDomain = userRule2Domain(TAG_PROXY)
|
||||||
|
val directDomain = userRule2Domain(TAG_DIRECT)
|
||||||
|
// fakedns with all domains to make it always top priority
|
||||||
|
v2rayConfig.dns.servers?.add(
|
||||||
|
0,
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
address = "fakedns",
|
||||||
|
domains = geositeCn.plus(proxyDomain).plus(directDomain)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS inbound对象
|
||||||
|
val remoteDns = Utils.getRemoteDnsServers()
|
||||||
|
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
|
||||||
|
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
|
||||||
|
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
|
||||||
|
port = 53,
|
||||||
|
network = "tcp,udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
val localDnsPort = Utils.parseInt(
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT),
|
||||||
|
AppConfig.PORT_LOCAL_DNS.toInt()
|
||||||
|
)
|
||||||
|
v2rayConfig.inbounds.add(
|
||||||
|
V2rayConfig.InboundBean(
|
||||||
|
tag = "dns-in",
|
||||||
|
port = localDnsPort,
|
||||||
|
listen = LOOPBACK,
|
||||||
|
protocol = "dokodemo-door",
|
||||||
|
settings = dnsInboundSettings,
|
||||||
|
sniffing = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS outbound对象
|
||||||
|
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
|
||||||
|
v2rayConfig.outbounds.add(
|
||||||
|
V2rayConfig.OutboundBean(
|
||||||
|
protocol = "dns",
|
||||||
|
tag = "dns-out",
|
||||||
|
settings = null,
|
||||||
|
streamSettings = null,
|
||||||
|
mux = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS routing tag
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
0, RulesBean(
|
||||||
|
inboundTag = arrayListOf("dns-in"),
|
||||||
|
outboundTag = "dns-out",
|
||||||
|
domain = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dns(v2rayConfig: V2rayConfig): Boolean {
|
||||||
|
try {
|
||||||
|
val hosts = mutableMapOf<String, Any>()
|
||||||
|
val servers = ArrayList<Any>()
|
||||||
|
|
||||||
|
//remote Dns
|
||||||
|
val remoteDns = Utils.getRemoteDnsServers()
|
||||||
|
val proxyDomain = userRule2Domain(TAG_PROXY)
|
||||||
|
remoteDns.forEach {
|
||||||
|
servers.add(it)
|
||||||
|
}
|
||||||
|
if (proxyDomain.size > 0) {
|
||||||
|
servers.add(
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
address = remoteDns.first(),
|
||||||
|
domains = proxyDomain,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// domestic DNS
|
||||||
|
val domesticDns = Utils.getDomesticDnsServers()
|
||||||
|
val directDomain = userRule2Domain(TAG_DIRECT)
|
||||||
|
val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
|
||||||
|
val geoipCn = arrayListOf(GEOIP_CN)
|
||||||
|
if (directDomain.size > 0) {
|
||||||
|
servers.add(
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
address = domesticDns.first(),
|
||||||
|
domains = directDomain,
|
||||||
|
expectIPs = if (isCnRoutingMode) geoipCn else null,
|
||||||
|
skipFallback = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Utils.isPureIpAddress(domesticDns.first())) {
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
0, RulesBean(
|
||||||
|
outboundTag = TAG_DIRECT,
|
||||||
|
port = "53",
|
||||||
|
ip = arrayListOf(domesticDns.first()),
|
||||||
|
domain = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//block dns
|
||||||
|
val blkDomain = userRule2Domain(TAG_BLOCKED)
|
||||||
|
if (blkDomain.size > 0) {
|
||||||
|
hosts.putAll(blkDomain.map { it to LOOPBACK })
|
||||||
|
}
|
||||||
|
|
||||||
|
// hardcode googleapi rule to fix play store problems
|
||||||
|
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
|
||||||
|
|
||||||
|
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
||||||
|
hosts[DNS_PUB_DOMAIN] = DNS_PUB_ADDRESSES
|
||||||
|
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
|
||||||
|
hosts[DNS_ONE_ONE_DOMAIN] = DNS_ONE_ONE_ADDRESSES
|
||||||
|
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
|
||||||
|
|
||||||
|
|
||||||
|
// DNS dns对象
|
||||||
|
v2rayConfig.dns = V2rayConfig.DnsBean(
|
||||||
|
servers = servers,
|
||||||
|
hosts = hosts
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS routing
|
||||||
|
if (Utils.isPureIpAddress(remoteDns.first())) {
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
0, RulesBean(
|
||||||
|
outboundTag = TAG_PROXY,
|
||||||
|
port = "53",
|
||||||
|
ip = arrayListOf(remoteDns.first()),
|
||||||
|
domain = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
|
||||||
|
try {
|
||||||
|
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
||||||
|
val protocol = outbound.protocol
|
||||||
|
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.WIREGUARD.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
|
) {
|
||||||
|
muxEnabled = false
|
||||||
|
} else if (protocol.equals(EConfigType.VLESS.name, true)
|
||||||
|
&& outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true
|
||||||
|
) {
|
||||||
|
muxEnabled = false
|
||||||
|
}
|
||||||
|
if (muxEnabled == true) {
|
||||||
|
outbound.mux?.enabled = true
|
||||||
|
outbound.mux?.concurrency =
|
||||||
|
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_CONCURRENCY, 8)
|
||||||
|
outbound.mux?.xudpConcurrency =
|
||||||
|
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY, 16)
|
||||||
|
outbound.mux?.xudpProxyUDP443 =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
|
||||||
|
} else {
|
||||||
|
outbound.mux?.enabled = false
|
||||||
|
outbound.mux?.concurrency = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
|
var localTunAddr = if (outbound.settings?.address == null) {
|
||||||
|
listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6)
|
||||||
|
} else {
|
||||||
|
outbound.settings?.address as List<*>
|
||||||
|
}
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) != true) {
|
||||||
|
localTunAddr = listOf(localTunAddr.first())
|
||||||
|
}
|
||||||
|
outbound.settings?.address = localTunAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outbound.streamSettings?.network == DEFAULT_NETWORK
|
||||||
|
&& outbound.streamSettings?.tcpSettings?.header?.type == HEADER_TYPE_HTTP
|
||||||
|
) {
|
||||||
|
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
||||||
|
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
||||||
|
|
||||||
|
val requestString: String by lazy {
|
||||||
|
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
|
||||||
|
}
|
||||||
|
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
||||||
|
requestString,
|
||||||
|
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
||||||
|
)
|
||||||
|
outbound.streamSettings?.tcpSettings?.header?.request?.path =
|
||||||
|
if (path.isNullOrEmpty()) {
|
||||||
|
listOf("/")
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
|
||||||
|
try {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS
|
||||||
|
&& v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragmentOutbound =
|
||||||
|
V2rayConfig.OutboundBean(
|
||||||
|
protocol = PROTOCOL_FREEDOM,
|
||||||
|
tag = TAG_FRAGMENT,
|
||||||
|
mux = null
|
||||||
|
)
|
||||||
|
|
||||||
|
var packets =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
||||||
|
if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY
|
||||||
|
&& packets == "tlshello"
|
||||||
|
) {
|
||||||
|
packets = "1-3"
|
||||||
|
} else if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.TLS
|
||||||
|
&& packets != "tlshello"
|
||||||
|
) {
|
||||||
|
packets = "tlshello"
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||||
|
fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
|
||||||
|
packets = packets,
|
||||||
|
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||||
|
?: "50-100",
|
||||||
|
interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||||
|
?: "10-20"
|
||||||
|
),
|
||||||
|
noises = listOf(
|
||||||
|
V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
|
||||||
|
type = "rand",
|
||||||
|
packet = "10-20",
|
||||||
|
delay = "10-16",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
|
||||||
|
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
||||||
|
TcpNoDelay = true,
|
||||||
|
mark = 255
|
||||||
|
)
|
||||||
|
)
|
||||||
|
v2rayConfig.outbounds.add(fragmentOutbound)
|
||||||
|
|
||||||
|
//proxy chain
|
||||||
|
v2rayConfig.outbounds[0].streamSettings?.sockopt =
|
||||||
|
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
||||||
|
dialerProxy = TAG_FRAGMENT
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moreOutbounds(
|
||||||
|
v2rayConfig: V2rayConfig,
|
||||||
|
subscriptionId: String,
|
||||||
|
isPlugin: Boolean
|
||||||
|
): Pair<Boolean, String> {
|
||||||
|
val returnPair = Pair(false, "")
|
||||||
|
var domainPort: String = ""
|
||||||
|
|
||||||
|
if (isPlugin) {
|
||||||
|
return returnPair
|
||||||
|
}
|
||||||
|
//fragment proxy
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
|
||||||
|
return returnPair
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptionId.isEmpty()) {
|
||||||
|
return returnPair
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return returnPair
|
||||||
|
|
||||||
|
//current proxy
|
||||||
|
val outbound = v2rayConfig.outbounds[0]
|
||||||
|
|
||||||
|
//Previous proxy
|
||||||
|
val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
|
||||||
|
if (prevNode != null) {
|
||||||
|
val prevOutbound = getProxyOutbound(prevNode)
|
||||||
|
if (prevOutbound != null) {
|
||||||
|
updateOutboundWithGlobalSettings(prevOutbound)
|
||||||
|
prevOutbound.tag = TAG_PROXY + "2"
|
||||||
|
v2rayConfig.outbounds.add(prevOutbound)
|
||||||
|
outbound.streamSettings?.sockopt =
|
||||||
|
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
||||||
|
dialerProxy = prevOutbound.tag
|
||||||
|
)
|
||||||
|
domainPort = prevNode.getServerAddressAndPort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Next proxy
|
||||||
|
val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
|
||||||
|
if (nextNode != null) {
|
||||||
|
val nextOutbound = getProxyOutbound(nextNode)
|
||||||
|
if (nextOutbound != null) {
|
||||||
|
updateOutboundWithGlobalSettings(nextOutbound)
|
||||||
|
nextOutbound.tag = TAG_PROXY
|
||||||
|
v2rayConfig.outbounds.add(0, nextOutbound)
|
||||||
|
outbound.tag = TAG_PROXY + "1"
|
||||||
|
nextOutbound.streamSettings?.sockopt =
|
||||||
|
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
||||||
|
dialerProxy = outbound.tag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return returnPair
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainPort.isNotEmpty()) {
|
||||||
|
return Pair(true, domainPort)
|
||||||
|
}
|
||||||
|
return returnPair
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
|
||||||
|
return when (profileItem.configType) {
|
||||||
|
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
|
||||||
|
EConfigType.CUSTOM -> null
|
||||||
|
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toOutbound(profileItem)
|
||||||
|
EConfigType.SOCKS -> SocksFmt.toOutbound(profileItem)
|
||||||
|
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
|
||||||
|
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
|
||||||
|
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
|
||||||
|
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem)
|
||||||
|
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||||
|
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
package com.v2ray.ang.plugin
|
||||||
|
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
|
||||||
|
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
|
||||||
|
init {
|
||||||
|
check(resolveInfo.providerInfo != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val componentInfo get() = resolveInfo.providerInfo!!
|
||||||
|
}
|
||||||
43
V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/Plugin.kt
Normal file
43
V2rayNG/app/src/main/kotlin/com/v2ray/ang/plugin/Plugin.kt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||||
|
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
package com.v2ray.ang.plugin
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
|
||||||
|
abstract class Plugin {
|
||||||
|
abstract val id: String
|
||||||
|
abstract val label: CharSequence
|
||||||
|
abstract val version: Int
|
||||||
|
abstract val versionName: String
|
||||||
|
open val icon: Drawable? get() = null
|
||||||
|
open val defaultConfig: String? get() = null
|
||||||
|
open val packageName: String get() = ""
|
||||||
|
open val directBootAware: Boolean get() = true
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
return id == (other as Plugin).id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode() = id.hashCode()
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
package com.v2ray.ang.plugin
|
||||||
|
|
||||||
|
object PluginContract {
|
||||||
|
|
||||||
|
const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN"
|
||||||
|
const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY"
|
||||||
|
const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id"
|
||||||
|
const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plugin.executable_path"
|
||||||
|
const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable"
|
||||||
|
|
||||||
|
const val COLUMN_PATH = "path"
|
||||||
|
const val COLUMN_MODE = "mode"
|
||||||
|
const val SCHEME = "plugin"
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||||
|
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
package com.v2ray.ang.plugin
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import com.v2ray.ang.AngApplication
|
||||||
|
|
||||||
|
class PluginList : ArrayList<Plugin>() {
|
||||||
|
init {
|
||||||
|
addAll(
|
||||||
|
AngApplication.application.packageManager.queryIntentContentProviders(
|
||||||
|
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
|
||||||
|
)
|
||||||
|
.filter { it.providerInfo.exported }.map { NativePlugin(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
val lookup = mutableMapOf<String, Plugin>().apply {
|
||||||
|
for (plugin in this@PluginList.toList()) {
|
||||||
|
fun check(old: Plugin?) {
|
||||||
|
if (old != null && old != plugin) {
|
||||||
|
this@PluginList.remove(old)
|
||||||
|
}
|
||||||
|
/* if (old != null && old !== plugin) {
|
||||||
|
val packages = this@PluginList.filter { it.id == plugin.id }
|
||||||
|
.joinToString { it.packageName }
|
||||||
|
val message = "Conflicting plugins found from: $packages"
|
||||||
|
Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
|
||||||
|
throw IllegalStateException(message)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
check(put(plugin.id, plugin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2021 by nekohasekai <contact-AngApplication@sekai.icu> *
|
||||||
|
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
package com.v2ray.ang.plugin
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ComponentInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ProviderInfo
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.system.Os
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import com.v2ray.ang.AngApplication
|
||||||
|
import com.v2ray.ang.extension.listenForPackageChanges
|
||||||
|
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
object PluginManager {
|
||||||
|
|
||||||
|
class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin)
|
||||||
|
|
||||||
|
private var receiver: BroadcastReceiver? = null
|
||||||
|
private var cachedPlugins: PluginList? = null
|
||||||
|
fun fetchPlugins() = synchronized(this) {
|
||||||
|
if (receiver == null) receiver = AngApplication.application.listenForPackageChanges {
|
||||||
|
synchronized(this) {
|
||||||
|
receiver = null
|
||||||
|
cachedPlugins = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cachedPlugins == null) cachedPlugins = PluginList()
|
||||||
|
cachedPlugins!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildUri(id: String, authority: String) = Uri.Builder()
|
||||||
|
.scheme(PluginContract.SCHEME)
|
||||||
|
.authority(authority)
|
||||||
|
.path("/$id")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
data class InitResult(
|
||||||
|
val path: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun init(pluginId: String): InitResult? {
|
||||||
|
if (pluginId.isEmpty()) return null
|
||||||
|
var throwable: Throwable? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = initNative(pluginId)
|
||||||
|
if (result != null) return result
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
if (throwable == null) throwable = t //Logs.w(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw throwable ?: PluginNotFoundException(pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNative(pluginId: String): InitResult? {
|
||||||
|
var flags = PackageManager.GET_META_DATA
|
||||||
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
|
flags =
|
||||||
|
flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
|
||||||
|
}
|
||||||
|
var providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
||||||
|
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "com.github.dyhkwong.AngApplication")), flags
|
||||||
|
)
|
||||||
|
.filter { it.providerInfo.exported }
|
||||||
|
if (providers.isEmpty()) {
|
||||||
|
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
||||||
|
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "io.nekohasekai.AngApplication")), flags
|
||||||
|
)
|
||||||
|
.filter { it.providerInfo.exported }
|
||||||
|
}
|
||||||
|
if (providers.isEmpty()) {
|
||||||
|
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
||||||
|
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "moe.matsuri.lite")), flags
|
||||||
|
)
|
||||||
|
.filter { it.providerInfo.exported }
|
||||||
|
}
|
||||||
|
if (providers.isEmpty()) {
|
||||||
|
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
||||||
|
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags
|
||||||
|
)
|
||||||
|
.filter { it.providerInfo.exported }
|
||||||
|
}
|
||||||
|
if (providers.isEmpty()) {
|
||||||
|
providers = AngApplication.application.packageManager.queryIntentContentProviders(
|
||||||
|
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
|
||||||
|
).filter {
|
||||||
|
it.providerInfo.exported &&
|
||||||
|
it.providerInfo.metaData.containsKey(METADATA_KEY_ID) &&
|
||||||
|
it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId
|
||||||
|
}
|
||||||
|
if (providers.size > 1) {
|
||||||
|
providers = listOf(providers[0]) // What if there is more than one?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (providers.isEmpty()) return null
|
||||||
|
if (providers.size > 1) {
|
||||||
|
val message =
|
||||||
|
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
|
||||||
|
Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show()
|
||||||
|
throw IllegalStateException(message)
|
||||||
|
}
|
||||||
|
val provider = providers.single().providerInfo
|
||||||
|
var failure: Throwable? = null
|
||||||
|
try {
|
||||||
|
initNativeFaster(provider)?.also { return InitResult(it) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// Logs.w("Initializing native plugin faster mode failed")
|
||||||
|
failure = t
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = Uri.Builder().apply {
|
||||||
|
scheme(ContentResolver.SCHEME_CONTENT)
|
||||||
|
authority(provider.authority)
|
||||||
|
}.build()
|
||||||
|
try {
|
||||||
|
return initNativeFast(
|
||||||
|
AngApplication.application.contentResolver,
|
||||||
|
pluginId,
|
||||||
|
uri
|
||||||
|
)?.let { InitResult(it) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// Logs.w("Initializing native plugin fast mode failed")
|
||||||
|
failure?.also { t.addSuppressed(it) }
|
||||||
|
failure = t
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return initNativeSlow(
|
||||||
|
AngApplication.application.contentResolver,
|
||||||
|
pluginId,
|
||||||
|
uri
|
||||||
|
)?.let { InitResult(it) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
failure?.also { t.addSuppressed(it) }
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNativeFaster(provider: ProviderInfo): String? {
|
||||||
|
return provider.loadString(PluginContract.METADATA_KEY_EXECUTABLE_PATH)
|
||||||
|
?.let { relativePath ->
|
||||||
|
File(provider.applicationInfo.nativeLibraryDir).resolve(relativePath).apply {
|
||||||
|
check(canExecute())
|
||||||
|
}.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNativeFast(cr: ContentResolver, pluginId: String, uri: Uri): String? {
|
||||||
|
return cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, bundleOf())
|
||||||
|
?.getString(PluginContract.EXTRA_ENTRY)?.also {
|
||||||
|
check(File(it).canExecute())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Recycle")
|
||||||
|
private fun initNativeSlow(cr: ContentResolver, pluginId: String, uri: Uri): String? {
|
||||||
|
var initialized = false
|
||||||
|
fun entryNotFound(): Nothing =
|
||||||
|
throw IndexOutOfBoundsException("Plugin entry binary not found")
|
||||||
|
|
||||||
|
val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin")
|
||||||
|
(cr.query(
|
||||||
|
uri,
|
||||||
|
arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
?: return null).use { cursor ->
|
||||||
|
if (!cursor.moveToFirst()) entryNotFound()
|
||||||
|
pluginDir.deleteRecursively()
|
||||||
|
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
|
||||||
|
val pluginDirPath = pluginDir.absolutePath + '/'
|
||||||
|
do {
|
||||||
|
val path = cursor.getString(0)
|
||||||
|
val file = File(pluginDir, path)
|
||||||
|
check(file.absolutePath.startsWith(pluginDirPath))
|
||||||
|
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
|
||||||
|
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
|
||||||
|
}
|
||||||
|
Os.chmod(
|
||||||
|
file.absolutePath, when (cursor.getType(1)) {
|
||||||
|
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
|
||||||
|
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
|
||||||
|
else -> throw IllegalArgumentException("File mode should be of type int")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (path == pluginId) initialized = true
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
if (!initialized) entryNotFound()
|
||||||
|
return File(pluginDir, pluginId).absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) {
|
||||||
|
is String -> value
|
||||||
|
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
|
||||||
|
.getString(value)
|
||||||
|
|
||||||
|
null -> null
|
||||||
|
else -> error("meta-data $key has invalid type ${value.javaClass}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu> *
|
||||||
|
* Copyright (C) 2021 by Max Lv <max.c.lv@gmail.com> *
|
||||||
|
* Copyright (C) 2021 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||||
|
* *
|
||||||
|
* This program is free software: you can redistribute it and/or modify *
|
||||||
|
* it under the terms of the GNU General Public License as published by *
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or *
|
||||||
|
* (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* This program is distributed in the hope that it will be useful, *
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||||
|
* GNU General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU General Public License *
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
package com.v2ray.ang.plugin
|
||||||
|
|
||||||
|
import android.content.pm.ComponentInfo
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
|
import com.v2ray.ang.AngApplication
|
||||||
|
import com.v2ray.ang.plugin.PluginManager.loadString
|
||||||
|
|
||||||
|
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
|
||||||
|
protected abstract val componentInfo: ComponentInfo
|
||||||
|
|
||||||
|
override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
|
||||||
|
override val version by lazy {
|
||||||
|
AngApplication.application.getPackageInfo(componentInfo.packageName).versionCode
|
||||||
|
}
|
||||||
|
override val versionName: String by lazy {
|
||||||
|
AngApplication.application.getPackageInfo(componentInfo.packageName).versionName!!
|
||||||
|
}
|
||||||
|
override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
|
||||||
|
override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
|
||||||
|
override val packageName: String get() = componentInfo.packageName
|
||||||
|
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.v2ray.ang.receiver
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
//Check if context is not null and action is the one we want
|
||||||
|
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||||
|
//Check if flag is true and a server is selected
|
||||||
|
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
|
||||||
|
//Start v2ray
|
||||||
|
V2RayServiceManager.startV2Ray(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,30 +4,27 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
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.MmkvManager
|
|
||||||
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class TaskerReceiver : BroadcastReceiver() {
|
class TaskerReceiver : BroadcastReceiver() {
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
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)
|
||||||
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
|
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty()
|
||||||
|
|
||||||
if (switch == null || guid == null || TextUtils.isEmpty(guid)) {
|
if (switch == null || TextUtils.isEmpty(guid)) {
|
||||||
return
|
return
|
||||||
} else if (switch) {
|
} else if (switch) {
|
||||||
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
|
||||||
Utils.startVServiceFromToggle(context)
|
Utils.startVServiceFromToggle(context)
|
||||||
} else {
|
} else {
|
||||||
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
MmkvManager.setSelectServer(guid)
|
||||||
V2RayServiceManager.startV2Ray(context)
|
V2RayServiceManager.startV2Ray(context)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.v2ray.ang.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ProcessService {
|
||||||
|
private var process: Process? = null
|
||||||
|
|
||||||
|
fun runProcess(context: Context, cmd: MutableList<String>) {
|
||||||
|
Log.d(ANG_PACKAGE, cmd.toString())
|
||||||
|
|
||||||
|
try {
|
||||||
|
val proBuilder = ProcessBuilder(cmd)
|
||||||
|
proBuilder.redirectErrorStream(true)
|
||||||
|
process = proBuilder
|
||||||
|
.directory(context.filesDir)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
Thread.sleep(50L)
|
||||||
|
Log.d(ANG_PACKAGE, "runProcess check")
|
||||||
|
process?.waitFor()
|
||||||
|
Log.d(ANG_PACKAGE, "runProcess exited")
|
||||||
|
}
|
||||||
|
Log.d(ANG_PACKAGE, process.toString())
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopProcess() {
|
||||||
|
try {
|
||||||
|
Log.d(ANG_PACKAGE, "runProcess destroy")
|
||||||
|
process?.destroy()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import android.graphics.drawable.Icon
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
@@ -32,24 +33,31 @@ class QSTileService : TileService() {
|
|||||||
qsTile?.updateTile()
|
qsTile?.updateTile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
|
*/
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
|
|
||||||
setState(Tile.STATE_INACTIVE)
|
setState(Tile.STATE_INACTIVE)
|
||||||
mMsgReceive = ReceiveMessageHandler(this)
|
mMsgReceive = ReceiveMessageHandler(this)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), Context.RECEIVER_EXPORTED)
|
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
} else {
|
|
||||||
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopListening() {
|
override fun onStopListening() {
|
||||||
super.onStopListening()
|
super.onStopListening()
|
||||||
|
|
||||||
unregisterReceiver(mMsgReceive)
|
try {
|
||||||
mMsgReceive = null
|
applicationContext.unregisterReceiver(mMsgReceive)
|
||||||
|
mMsgReceive = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ import com.v2ray.ang.AppConfig
|
|||||||
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
|
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
|
||||||
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
|
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager.updateConfigViaSub
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
object SubscriptionUpdater {
|
object SubscriptionUpdater {
|
||||||
|
|
||||||
@@ -40,8 +39,8 @@ object SubscriptionUpdater {
|
|||||||
|
|
||||||
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
|
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
|
||||||
|
|
||||||
for (i in subs) {
|
for (sub in subs) {
|
||||||
val subscription = i.second
|
val subItem = sub.second
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
|
notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
|
||||||
@@ -56,11 +55,10 @@ object SubscriptionUpdater {
|
|||||||
notificationManager.notify(3, notification.build())
|
notificationManager.notify(3, notification.build())
|
||||||
Log.d(
|
Log.d(
|
||||||
AppConfig.ANG_PACKAGE,
|
AppConfig.ANG_PACKAGE,
|
||||||
"subscription automatic update: ---${subscription.remarks}"
|
"subscription automatic update: ---${subItem.remarks}"
|
||||||
)
|
)
|
||||||
val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url)
|
updateConfigViaSub(Pair(sub.first, subItem))
|
||||||
AngConfigManager.importBatchConfig(configs, i.first, false)
|
notification.setContentText("Updating ${subItem.remarks}")
|
||||||
notification.setContentText("Updating ${subscription.remarks}")
|
|
||||||
}
|
}
|
||||||
notificationManager.cancel(3)
|
notificationManager.cancel(3)
|
||||||
return Result.success()
|
return Result.success()
|
||||||
|
|||||||
@@ -13,19 +13,21 @@ import android.os.Build
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.tencent.mmkv.MMKV
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||||
|
import com.v2ray.ang.AppConfig.VPN
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.extension.toSpeedString
|
import com.v2ray.ang.extension.toSpeedString
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.ui.MainActivity
|
import com.v2ray.ang.ui.MainActivity
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.PluginUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.V2rayConfigUtil
|
|
||||||
import go.Seq
|
import go.Seq
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
@@ -46,8 +48,6 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
|
||||||
private val mMsgReceive = ReceiveMessageHandler()
|
private val mMsgReceive = ReceiveMessageHandler()
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
var serviceControl: SoftReference<ServiceControl>? = null
|
var serviceControl: SoftReference<ServiceControl>? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -55,7 +55,7 @@ object V2RayServiceManager {
|
|||||||
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
Seq.setContext(value?.get()?.getService()?.applicationContext)
|
||||||
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
|
||||||
}
|
}
|
||||||
var currentConfig: ServerConfig? = null
|
var currentConfig: ProfileItem? = null
|
||||||
|
|
||||||
private var lastQueryTime = 0L
|
private var lastQueryTime = 0L
|
||||||
private var mBuilder: NotificationCompat.Builder? = null
|
private var mBuilder: NotificationCompat.Builder? = null
|
||||||
@@ -64,16 +64,18 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
fun startV2Ray(context: Context) {
|
fun startV2Ray(context: Context) {
|
||||||
if (v2rayPoint.isRunning) return
|
if (v2rayPoint.isRunning) return
|
||||||
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
|
val guid = MmkvManager.getSelectServer() ?: return
|
||||||
val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||||
if (!result.status) return
|
if (!Utils.isValidUrl(config.server) && !Utils.isIpAddress(config.server)) return
|
||||||
|
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||||
|
// if (!result.status) return
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) == true) {
|
||||||
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
||||||
} else {
|
} else {
|
||||||
context.toast(R.string.toast_services_start)
|
context.toast(R.string.toast_services_start)
|
||||||
}
|
}
|
||||||
val intent = if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") {
|
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == 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)
|
||||||
@@ -108,13 +110,11 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onEmitStatus(l: Long, s: String?): Long {
|
override fun onEmitStatus(l: Long, s: String?): Long {
|
||||||
//Logger.d(s)
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setup(s: String): Long {
|
override fun setup(s: String): Long {
|
||||||
val serviceControl = serviceControl?.get() ?: return -1
|
val serviceControl = serviceControl?.get() ?: return -1
|
||||||
//Logger.d(s)
|
|
||||||
return try {
|
return try {
|
||||||
serviceControl.startService()
|
serviceControl.startService()
|
||||||
lastQueryTime = System.currentTimeMillis()
|
lastQueryTime = System.currentTimeMillis()
|
||||||
@@ -127,14 +127,19 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
|
*/
|
||||||
|
|
||||||
fun startV2rayPoint() {
|
fun startV2rayPoint() {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
val service = serviceControl?.get()?.getService() ?: return
|
||||||
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
|
val guid = MmkvManager.getSelectServer() ?: return
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
val config = MmkvManager.decodeServerConfig(guid) ?: return
|
||||||
if (v2rayPoint.isRunning) {
|
if (v2rayPoint.isRunning) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
|
val result = V2rayConfigManager.getV2rayConfig(service, guid)
|
||||||
if (!result.status)
|
if (!result.status)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -143,21 +148,17 @@ object V2RayServiceManager {
|
|||||||
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||||
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
|
|
||||||
} else {
|
|
||||||
service.registerReceiver(mMsgReceive, mFilter)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
v2rayPoint.configureFileContent = result.content
|
v2rayPoint.configureFileContent = result.content
|
||||||
v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
|
v2rayPoint.domainName = result.domainPort
|
||||||
currentConfig = config
|
currentConfig = config
|
||||||
|
|
||||||
try {
|
try {
|
||||||
v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false)
|
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
@@ -165,6 +166,8 @@ object V2RayServiceManager {
|
|||||||
if (v2rayPoint.isRunning) {
|
if (v2rayPoint.isRunning) {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||||
showNotification()
|
showNotification()
|
||||||
|
|
||||||
|
PluginUtil.runPlugin(service, config, result.domainPort)
|
||||||
} else {
|
} else {
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
|
||||||
cancelNotification()
|
cancelNotification()
|
||||||
@@ -192,6 +195,7 @@ object V2RayServiceManager {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(ANG_PACKAGE, e.toString())
|
Log.d(ANG_PACKAGE, e.toString())
|
||||||
}
|
}
|
||||||
|
PluginUtil.stopPlugin()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ReceiveMessageHandler : BroadcastReceiver() {
|
private class ReceiveMessageHandler : BroadcastReceiver() {
|
||||||
@@ -199,7 +203,6 @@ object V2RayServiceManager {
|
|||||||
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 -> {
|
||||||
//Logger.e("ReceiveMessageHandler", intent?.getIntExtra("key", 0).toString())
|
|
||||||
if (v2rayPoint.isRunning) {
|
if (v2rayPoint.isRunning) {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
||||||
} else {
|
} else {
|
||||||
@@ -378,7 +381,7 @@ object V2RayServiceManager {
|
|||||||
private fun startSpeedNotification() {
|
private fun startSpeedNotification() {
|
||||||
if (mDisposable == null &&
|
if (mDisposable == null &&
|
||||||
v2rayPoint.isRunning &&
|
v2rayPoint.isRunning &&
|
||||||
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
|
||||||
) {
|
) {
|
||||||
var lastZeroSpeed = false
|
var lastZeroSpeed = false
|
||||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||||
@@ -428,10 +431,11 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopSpeedNotification() {
|
private fun stopSpeedNotification() {
|
||||||
if (mDisposable != null) {
|
mDisposable?.let {
|
||||||
mDisposable?.dispose() //stop queryStats
|
it.dispose() //stop queryStats
|
||||||
mDisposable = null
|
mDisposable = null
|
||||||
updateNotification(currentConfig?.remarks, 0, 0)
|
updateNotification(currentConfig?.remarks, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import android.os.IBinder
|
|||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
|
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
|
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
|
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.extension.serializable
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.V2rayConfigManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
|
import com.v2ray.ang.util.PluginUtil
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
import com.v2ray.ang.util.SpeedtestUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import go.Seq
|
import go.Seq
|
||||||
@@ -19,7 +24,7 @@ import libv2ray.Libv2ray
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class V2RayTestService : Service() {
|
class V2RayTestService : Service() {
|
||||||
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) }
|
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -30,10 +35,10 @@ class V2RayTestService : Service() {
|
|||||||
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 -> {
|
||||||
val contentPair = intent.getSerializableExtra("content") as Pair<String, String>
|
val guid = intent.serializable<String>("content") ?: ""
|
||||||
realTestScope.launch {
|
realTestScope.launch {
|
||||||
val result = SpeedtestUtil.realPing(contentPair.second)
|
val result = startRealPing(guid)
|
||||||
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result))
|
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,4 +52,20 @@ class V2RayTestService : Service() {
|
|||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startRealPing(guid: String): Long {
|
||||||
|
val retFailure = -1L
|
||||||
|
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
|
||||||
|
if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
|
val delay = PluginUtil.realPingHy2(this, config)
|
||||||
|
return delay
|
||||||
|
} else {
|
||||||
|
val config = V2rayConfigManager.getV2rayConfig(this, guid)
|
||||||
|
if (!config.status) {
|
||||||
|
return retFailure
|
||||||
|
}
|
||||||
|
return SpeedtestUtil.realPing(config.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import android.os.ParcelFileDescriptor
|
|||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.ERoutingMode
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.MmkvManager
|
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 com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -39,7 +41,6 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
private const val TUN2SOCKS = "libtun2socks.so"
|
private const val TUN2SOCKS = "libtun2socks.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
private lateinit var mInterface: ParcelFileDescriptor
|
private lateinit var mInterface: ParcelFileDescriptor
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
@@ -64,7 +65,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager }
|
||||||
|
|
||||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||||
private val defaultNetworkCallback by lazy {
|
private val defaultNetworkCallback by lazy {
|
||||||
@@ -117,13 +118,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
val builder = Builder()
|
val builder = Builder()
|
||||||
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||||
|
|
||||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
|
||||||
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
|
||||||
|
|
||||||
builder.setMtu(VPN_MTU)
|
builder.setMtu(VPN_MTU)
|
||||||
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||||
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
val bypassLan = SettingsManager.routingRulesetsBypassLan()
|
||||||
|
if (bypassLan) {
|
||||||
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
|
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
|
||||||
val addr = it.split('/')
|
val addr = it.split('/')
|
||||||
builder.addRoute(addr[0], addr[1].toInt())
|
builder.addRoute(addr[0], addr[1].toInt())
|
||||||
@@ -132,31 +131,34 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
builder.addRoute("0.0.0.0", 0)
|
builder.addRoute("0.0.0.0", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||||
if (routingMode == ERoutingMode.BYPASS_LAN.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value) {
|
if (bypassLan) {
|
||||||
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
|
||||||
} else {
|
} else {
|
||||||
builder.addRoute("::", 0)
|
builder.addRoute("::", 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||||
builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
} else {
|
// } else {
|
||||||
Utils.getVpnDnsServers()
|
Utils.getVpnDnsServers()
|
||||||
.forEach {
|
.forEach {
|
||||||
if (Utils.isPureIpAddress(it)) {
|
if (Utils.isPureIpAddress(it)) {
|
||||||
builder.addDnsServer(it)
|
builder.addDnsServer(it)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
|
val selfPackageName = BuildConfig.APPLICATION_ID
|
||||||
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
|
||||||
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
|
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 {
|
apps?.forEach {
|
||||||
try {
|
try {
|
||||||
if (bypassApps)
|
if (bypassApps)
|
||||||
@@ -164,9 +166,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
else
|
else
|
||||||
builder.addAllowedApplication(it)
|
builder.addAllowedApplication(it)
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
//Logger.d(e)
|
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
builder.addDisallowedApplication(selfPackageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the old interface since the parameters have been changed.
|
// Close the old interface since the parameters have been changed.
|
||||||
@@ -201,26 +205,26 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun runTun2socks() {
|
private fun runTun2socks() {
|
||||||
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
val cmd = arrayListOf(
|
val cmd = arrayListOf(
|
||||||
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||||
"--netif-netmask", "255.255.255.252",
|
"--netif-netmask", "255.255.255.252",
|
||||||
"--socks-server-addr", "127.0.0.1:${socksPort}",
|
"--socks-server-addr", "$LOOPBACK:${socksPort}",
|
||||||
"--tunmtu", VPN_MTU.toString(),
|
"--tunmtu", VPN_MTU.toString(),
|
||||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||||
"--enable-udprelay",
|
"--enable-udprelay",
|
||||||
"--loglevel", "notice"
|
"--loglevel", "notice"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
|
||||||
cmd.add("--netif-ip6addr")
|
cmd.add("--netif-ip6addr")
|
||||||
cmd.add(PRIVATE_VLAN6_ROUTER)
|
cmd.add(PRIVATE_VLAN6_ROUTER)
|
||||||
}
|
}
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
|
||||||
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
||||||
cmd.add("--dnsgw")
|
cmd.add("--dnsgw")
|
||||||
cmd.add("127.0.0.1:${localDnsPort}")
|
cmd.add("$LOOPBACK:${localDnsPort}")
|
||||||
}
|
}
|
||||||
Log.d(packageName, cmd.toString())
|
Log.d(packageName, cmd.toString())
|
||||||
|
|
||||||
@@ -230,7 +234,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
process = proBuilder
|
process = proBuilder
|
||||||
.directory(applicationContext.filesDir)
|
.directory(applicationContext.filesDir)
|
||||||
.start()
|
.start()
|
||||||
Thread(Runnable {
|
Thread {
|
||||||
Log.d(packageName, "$TUN2SOCKS check")
|
Log.d(packageName, "$TUN2SOCKS check")
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
Log.d(packageName, "$TUN2SOCKS exited")
|
Log.d(packageName, "$TUN2SOCKS exited")
|
||||||
@@ -238,7 +242,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
Log.d(packageName, "$TUN2SOCKS restart")
|
Log.d(packageName, "$TUN2SOCKS restart")
|
||||||
runTun2socks()
|
runTun2socks()
|
||||||
}
|
}
|
||||||
}).start()
|
}.start()
|
||||||
Log.d(packageName, process.toString())
|
Log.d(packageName, process.toString())
|
||||||
|
|
||||||
sendFd()
|
sendFd()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.Manifest
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||||
@@ -133,13 +134,15 @@ class AboutActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showFileChooser() {
|
private fun showFileChooser() {
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
intent.type = "*/*"
|
type = "*/*"
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
toast(R.string.toast_require_file_manager)
|
toast(R.string.toast_require_file_manager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,27 +152,23 @@ class AboutActivity : BaseActivity() {
|
|||||||
val uri = it.data?.data
|
val uri = it.data?.data
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
if (it.resultCode == RESULT_OK && uri != null) {
|
||||||
try {
|
try {
|
||||||
try {
|
val targetFile =
|
||||||
val targetFile =
|
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
||||||
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
|
contentResolver.openInputStream(uri).use { input ->
|
||||||
contentResolver.openInputStream(uri).use { input ->
|
targetFile.outputStream().use { fileOut ->
|
||||||
targetFile.outputStream().use { fileOut ->
|
input?.copyTo(fileOut)
|
||||||
input?.copyTo(fileOut)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (restoreConfiguration(targetFile)) {
|
|
||||||
toast(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
toast(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
|
if (restoreConfiguration(targetFile)) {
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
|
||||||
toast(e.message.toString())
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -34,9 +34,6 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val context = newBase?.let {
|
super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, Utils.getLocale()))
|
||||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
|
||||||
}
|
|
||||||
super.attachBaseContext(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,46 +33,42 @@ class LogcatActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun logcat(shouldFlushLog: Boolean) {
|
private fun logcat(shouldFlushLog: Boolean) {
|
||||||
|
binding.pbWaiting.visibility = View.VISIBLE
|
||||||
|
|
||||||
try {
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
binding.pbWaiting.visibility = View.VISIBLE
|
try {
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Default) {
|
|
||||||
if (shouldFlushLog) {
|
if (shouldFlushLog) {
|
||||||
val lst = LinkedHashSet<String>()
|
val lst = linkedSetOf("logcat", "-c")
|
||||||
lst.add("logcat")
|
|
||||||
lst.add("-c")
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val lst = LinkedHashSet<String>()
|
val lst = linkedSetOf(
|
||||||
lst.add("logcat")
|
"logcat", "-d", "-v", "time", "-s",
|
||||||
lst.add("-d")
|
"GoLog,tun2socks,$ANG_PACKAGE,AndroidRuntime,System.err"
|
||||||
lst.add("-v")
|
)
|
||||||
lst.add("time")
|
|
||||||
lst.add("-s")
|
|
||||||
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
|
||||||
val process = withContext(Dispatchers.IO) {
|
val process = withContext(Dispatchers.IO) {
|
||||||
Runtime.getRuntime().exec(lst.toTypedArray())
|
Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
}
|
}
|
||||||
// val bufferedReader = BufferedReader(
|
|
||||||
// InputStreamReader(process.inputStream))
|
|
||||||
// val allText = bufferedReader.use(BufferedReader::readText)
|
|
||||||
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
val allText = process.inputStream.bufferedReader().use { it.readText() }
|
||||||
launch(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.tvLogcat.text = allText
|
binding.tvLogcat.text = allText
|
||||||
binding.tvLogcat.movementMethod = ScrollingMovementMethod()
|
binding.tvLogcat.movementMethod = ScrollingMovementMethod()
|
||||||
binding.pbWaiting.visibility = View.GONE
|
binding.pbWaiting.visibility = View.GONE
|
||||||
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
|
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
|
||||||
}
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
binding.pbWaiting.visibility = View.GONE
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
menuInflater.inflate(R.menu.menu_logcat, menu)
|
||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
|||||||
@@ -29,14 +29,16 @@ 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.tbruyelle.rxpermissions3.RxPermissions
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
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.handler.AngConfigManager
|
||||||
|
import com.v2ray.ang.handler.MigrateManager
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
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.android.schedulers.AndroidSchedulers
|
||||||
@@ -44,6 +46,7 @@ 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 me.drakeet.support.toast.ToastCompat
|
import me.drakeet.support.toast.ToastCompat
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@@ -87,7 +90,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)
|
Utils.stopVService(this)
|
||||||
} else if ((MmkvManager.settingsStorage?.decodeString(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) {
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
@@ -111,11 +114,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
val callback = SimpleItemTouchHelperCallback(adapter)
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
mItemTouchHelper = ItemTouchHelper(callback)
|
|
||||||
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
|
|
||||||
val toggle = ActionBarDrawerToggle(
|
val toggle = ActionBarDrawerToggle(
|
||||||
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
this, binding.drawerLayout, binding.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close
|
||||||
)
|
)
|
||||||
@@ -125,13 +126,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
|
|
||||||
initGroupTab()
|
initGroupTab()
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
|
migrateLegacy()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
RxPermissions(this)
|
RxPermissions(this)
|
||||||
.request(Manifest.permission.POST_NOTIFICATIONS)
|
.request(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (!it)
|
if (!it)
|
||||||
toast(R.string.toast_permission_denied)
|
toast(R.string.toast_permission_denied_notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +173,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainViewModel.startListenBroadcast()
|
mainViewModel.startListenBroadcast()
|
||||||
mainViewModel.copyAssets(assets)
|
mainViewModel.initAssets(assets)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateLegacy() {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val result = MigrateManager.migrateServerConfig2Profile()
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (result) {
|
||||||
|
toast(getString(R.string.migration_success))
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
} else {
|
||||||
|
//toast(getString(R.string.migration_fail))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initGroupTab() {
|
private fun initGroupTab() {
|
||||||
@@ -198,7 +215,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startV2Ray() {
|
fun startV2Ray() {
|
||||||
if (MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
|
toast(R.string.title_file_chooser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
V2RayServiceManager.startV2Ray(this)
|
V2RayServiceManager.startV2Ray(this)
|
||||||
@@ -278,6 +296,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.import_manually_http -> {
|
||||||
|
importManually(EConfigType.HTTP.value)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.import_manually_trojan -> {
|
R.id.import_manually_trojan -> {
|
||||||
importManually(EConfigType.TROJAN.value)
|
importManually(EConfigType.TROJAN.value)
|
||||||
true
|
true
|
||||||
@@ -288,6 +311,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.import_manually_hysteria2 -> {
|
||||||
|
importManually(EConfigType.HYSTERIA2.value)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.import_config_custom_clipboard -> {
|
R.id.import_config_custom_clipboard -> {
|
||||||
importConfigCustomClipboard()
|
importConfigCustomClipboard()
|
||||||
true
|
true
|
||||||
@@ -330,11 +358,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.ping_all -> {
|
R.id.ping_all -> {
|
||||||
|
toast(R.string.connection_test_testing)
|
||||||
mainViewModel.testAllTcping()
|
mainViewModel.testAllTcping()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.real_ping_all -> {
|
R.id.real_ping_all -> {
|
||||||
|
toast(R.string.connection_test_testing)
|
||||||
mainViewModel.testAllRealPing()
|
mainViewModel.testAllRealPing()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -478,30 +508,35 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun importBatchConfig(server: String?) {
|
private fun importBatchConfig(server: String?) {
|
||||||
// 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) {
|
||||||
val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
|
try {
|
||||||
delay(500L)
|
val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
|
||||||
launch(Dispatchers.Main) {
|
delay(500L)
|
||||||
if (count > 0) {
|
withContext(Dispatchers.Main) {
|
||||||
toast(R.string.toast_success)
|
when {
|
||||||
mainViewModel.reloadServerList()
|
count > 0 -> {
|
||||||
} else if (countSub > 0) {
|
toast(R.string.toast_success)
|
||||||
initGroupTab()
|
mainViewModel.reloadServerList()
|
||||||
} else {
|
}
|
||||||
toast(R.string.toast_failure)
|
|
||||||
|
countSub > 0 -> initGroupTab()
|
||||||
|
else -> toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
binding.pbWaiting.hide()
|
||||||
}
|
}
|
||||||
//dialog.dismiss()
|
} catch (e: Exception) {
|
||||||
binding.pbWaiting.hide()
|
withContext(Dispatchers.Main) {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
binding.pbWaiting.hide()
|
||||||
|
}
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun importConfigCustomClipboard()
|
private fun importConfigCustomClipboard()
|
||||||
: Boolean {
|
: Boolean {
|
||||||
try {
|
try {
|
||||||
@@ -706,10 +741,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.user_asset_setting -> {
|
R.id.routing_setting -> {
|
||||||
startActivity(Intent(this, UserAssetActivity::class.java))
|
requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
R.id.promotion -> {
|
R.id.promotion -> {
|
||||||
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.v2ray.ang.AngApplication.Companion.application
|
import com.v2ray.ang.AngApplication.Companion.application
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -17,13 +16,12 @@ 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.SubscriptionItem
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
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.AngConfigManager
|
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
@@ -66,17 +64,12 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
||||||
}
|
}
|
||||||
if (guid == MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
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 = ""
|
holder.itemMainBinding.tvSubscription.text = MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks ?: ""
|
||||||
val json = MmkvManager.subStorage?.decodeString(profile.subscriptionId)
|
|
||||||
if (!json.isNullOrBlank()) {
|
|
||||||
val sub = Gson().fromJson(json, SubscriptionItem::class.java)
|
|
||||||
holder.itemMainBinding.tvSubscription.text = sub.remarks
|
|
||||||
}
|
|
||||||
|
|
||||||
var shareOptions = share_method.asList()
|
var shareOptions = share_method.asList()
|
||||||
when (profile.configType) {
|
when (profile.configType) {
|
||||||
@@ -85,16 +78,20 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
shareOptions = shareOptions.takeLast(1)
|
shareOptions = shareOptions.takeLast(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
EConfigType.VLESS -> {
|
|
||||||
holder.itemMainBinding.tvType.text = profile.configType.name
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
holder.itemMainBinding.tvType.text = profile.configType.name.lowercase()
|
holder.itemMainBinding.tvType.text = profile.configType.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val strState = "${profile.server?.dropLast(3)}*** : ${profile.serverPort}"
|
// 隐藏主页服务器地址为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.tvStatistics.text = strState
|
||||||
|
|
||||||
@@ -132,6 +129,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
||||||
val intent = Intent().putExtra("guid", guid)
|
val intent = Intent().putExtra("guid", guid)
|
||||||
.putExtra("isRunning", isRunning)
|
.putExtra("isRunning", isRunning)
|
||||||
|
.putExtra("createConfigType", profile.configType.value)
|
||||||
if (profile.configType == EConfigType.CUSTOM) {
|
if (profile.configType == EConfigType.CUSTOM) {
|
||||||
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
||||||
} else {
|
} else {
|
||||||
@@ -139,8 +137,8 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
||||||
if (guid != MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
if (guid != MmkvManager.getSelectServer()) {
|
||||||
if (MmkvManager.settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
removeServer(guid, position)
|
removeServer(guid, position)
|
||||||
@@ -158,9 +156,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
}
|
}
|
||||||
|
|
||||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||||
val selected = MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
val selected = MmkvManager.getSelectServer()
|
||||||
if (guid != selected) {
|
if (guid != selected) {
|
||||||
MmkvManager.mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
|
MmkvManager.setSelectServer(guid)
|
||||||
if (!TextUtils.isEmpty(selected)) {
|
if (!TextUtils.isEmpty(selected)) {
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
|
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
|
||||||
}
|
}
|
||||||
@@ -236,31 +234,16 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
|
||||||
BaseViewHolder(itemFooterBinding.root)
|
BaseViewHolder(itemFooterBinding.root)
|
||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
|
||||||
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return
|
|
||||||
if (guid != MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)) {
|
|
||||||
// mActivity.alert(R.string.del_config_comfirm) {
|
|
||||||
// positiveButton(android.R.string.ok) {
|
|
||||||
mActivity.mainViewModel.removeServer(guid)
|
|
||||||
notifyItemRemoved(position)
|
|
||||||
// }
|
|
||||||
// show()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
||||||
notifyItemMoved(fromPosition, toPosition)
|
notifyItemMoved(fromPosition, toPosition)
|
||||||
// position is changed, since position is used by click callbacks, need to update range
|
|
||||||
if (toPosition > fromPosition)
|
|
||||||
notifyItemRangeChanged(fromPosition, toPosition - fromPosition + 1)
|
|
||||||
else
|
|
||||||
notifyItemRangeChanged(toPosition, fromPosition - toPosition + 1)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemMoveCompleted() {
|
override fun onItemMoveCompleted() {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onItemDismiss(position: Int) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.appcompat.widget.SearchView
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
@@ -18,8 +17,8 @@ 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.v2RayApplication
|
import com.v2ray.ang.extension.v2RayApplication
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.AppManagerUtil
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
@@ -34,7 +33,6 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
|
|
||||||
private var adapter: PerAppProxyAdapter? = null
|
private var adapter: PerAppProxyAdapter? = null
|
||||||
private var appsAll: List<AppInfo>? = null
|
private var appsAll: List<AppInfo>? = null
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -43,7 +41,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||||
binding.recyclerView.addItemDecoration(dividerItemDecoration)
|
binding.recyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
|
|
||||||
val blacklist = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
|
|
||||||
AppManagerUtil.rxLoadNetworkAppList(this)
|
AppManagerUtil.rxLoadNetworkAppList(this)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@@ -134,14 +132,14 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
***/
|
***/
|
||||||
|
|
||||||
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
|
||||||
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
|
||||||
}
|
}
|
||||||
binding.switchPerAppProxy.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
binding.switchPerAppProxy.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
|
||||||
|
|
||||||
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
|
||||||
settingsStorage.encode(AppConfig.PREF_BYPASS_APPS, isChecked)
|
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
|
||||||
}
|
}
|
||||||
binding.switchBypassApps.isChecked = settingsStorage.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
|
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
|
||||||
|
|
||||||
/***
|
/***
|
||||||
et_search.setOnEditorActionListener { v, actionId, event ->
|
et_search.setOnEditorActionListener { v, actionId, event ->
|
||||||
@@ -177,7 +175,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
adapter?.let {
|
adapter?.let {
|
||||||
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
|
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +215,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
it.notifyDataSetChanged()
|
it.notifyDataSetChanged()
|
||||||
true
|
true
|
||||||
} ?: false
|
} == true
|
||||||
|
|
||||||
R.id.select_proxy_app -> {
|
R.id.select_proxy_app -> {
|
||||||
selectProxyApp()
|
selectProxyApp()
|
||||||
|
|||||||
@@ -59,22 +59,23 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
|||||||
fun bind(appInfo: AppInfo) {
|
fun bind(appInfo: AppInfo) {
|
||||||
this.appInfo = appInfo
|
this.appInfo = appInfo
|
||||||
|
|
||||||
|
// Set app icon and name
|
||||||
itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
|
itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
|
||||||
// name.text = appInfo.appName
|
itemBypassBinding.name.text = if (appInfo.isSystemApp) {
|
||||||
|
String.format("** %s", appInfo.appName)
|
||||||
itemBypassBinding.checkBox.isChecked = inBlacklist
|
|
||||||
itemBypassBinding.packageName.text = appInfo.packageName
|
|
||||||
if (appInfo.isSystemApp) {
|
|
||||||
itemBypassBinding.name.text = String.format("** %1s", appInfo.appName)
|
|
||||||
//name.textColor = Color.RED
|
|
||||||
} else {
|
} else {
|
||||||
itemBypassBinding.name.text = appInfo.appName
|
appInfo.appName
|
||||||
//name.textColor = Color.DKGRAY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set package name and checkbox state
|
||||||
|
itemBypassBinding.packageName.text = appInfo.packageName
|
||||||
|
itemBypassBinding.checkBox.isChecked = inBlacklist
|
||||||
|
|
||||||
|
// Handle item click to toggle blacklist status
|
||||||
itemView.setOnClickListener(this)
|
itemView.setOnClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
if (inBlacklist) {
|
if (inBlacklist) {
|
||||||
blacklist.remove(appInfo.packageName)
|
blacklist.remove(appInfo.packageName)
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
||||||
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class RoutingEditActivity : BaseActivity() {
|
||||||
|
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
|
||||||
|
private val position by lazy { intent.getIntExtra("position", -1) }
|
||||||
|
|
||||||
|
private val outbound_tag: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.outbound_tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(binding.root)
|
||||||
|
title = getString(R.string.routing_settings_rule_title)
|
||||||
|
|
||||||
|
val rulesetItem = SettingsManager.getRoutingRuleset(position)
|
||||||
|
if (rulesetItem != null) {
|
||||||
|
bindingServer(rulesetItem)
|
||||||
|
} else {
|
||||||
|
clearServer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
|
||||||
|
binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
|
||||||
|
binding.chkLocked.isChecked = rulesetItem.looked == true
|
||||||
|
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
|
||||||
|
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
|
||||||
|
binding.etPort.text = Utils.getEditable(rulesetItem.port)
|
||||||
|
binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
|
||||||
|
binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
|
||||||
|
val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag)
|
||||||
|
binding.spOutboundTag.setSelection(outbound)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearServer(): Boolean {
|
||||||
|
binding.etRemarks.text = null
|
||||||
|
binding.spOutboundTag.setSelection(0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveServer(): Boolean {
|
||||||
|
val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem()
|
||||||
|
|
||||||
|
rulesetItem.apply {
|
||||||
|
remarks = binding.etRemarks.text.toString()
|
||||||
|
looked = binding.chkLocked.isChecked
|
||||||
|
domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() }
|
||||||
|
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||||
|
ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() }
|
||||||
|
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||||
|
protocol = binding.etProtocol.text.toString().takeIf { it.isNotEmpty() }
|
||||||
|
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||||
|
port = binding.etPort.text.toString().takeIf { it.isNotEmpty() }
|
||||||
|
network = binding.etNetwork.text.toString().takeIf { it.isNotEmpty() }
|
||||||
|
outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rulesetItem.remarks.isNullOrEmpty()) {
|
||||||
|
toast(R.string.sub_setting_remarks)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsManager.saveRoutingRuleset(position, rulesetItem)
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun deleteServer(): Boolean {
|
||||||
|
if (position >= 0) {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
SettingsManager.removeRoutingRuleset(position)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.action_server, menu)
|
||||||
|
val del_config = menu.findItem(R.id.del_config)
|
||||||
|
|
||||||
|
if (position < 0) {
|
||||||
|
del_config?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
R.id.del_config -> {
|
||||||
|
deleteServer()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.save_config -> {
|
||||||
|
saveServer()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
|
||||||
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class RoutingSettingActivity : BaseActivity() {
|
||||||
|
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
|
var rulesets: MutableList<RulesetItem> = mutableListOf()
|
||||||
|
private val adapter by lazy { RoutingSettingRecyclerAdapter(this) }
|
||||||
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
|
private val routing_domain_strategy: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.routing_domain_strategy)
|
||||||
|
}
|
||||||
|
private val preset_rulesets: Array<out String> by lazy {
|
||||||
|
resources.getStringArray(R.array.preset_rulesets)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
title = getString(R.string.routing_settings_title)
|
||||||
|
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
|
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
|
val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
|
||||||
|
found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
|
||||||
|
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
refreshData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.menu_routing_setting, menu)
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
R.id.add_rule -> {
|
||||||
|
startActivity(Intent(this, RoutingEditActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.user_asset_setting -> {
|
||||||
|
startActivity(Intent(this, UserAssetActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.import_rulesets -> {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
SettingsManager.resetRoutingRulesets(this@RoutingSettingActivity, i)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
refreshData()
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.import_rulesets_from_clipboard -> {
|
||||||
|
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val clipboard = try {
|
||||||
|
Utils.getClipboard(this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val result = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (result) {
|
||||||
|
refreshData()
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
//do nothing
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshData() {
|
||||||
|
rulesets.clear()
|
||||||
|
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.v2ray.ang.databinding.ItemRecyclerRoutingSettingBinding
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
|
|
||||||
|
class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
|
||||||
|
ItemTouchHelperAdapter {
|
||||||
|
|
||||||
|
private var mActivity: RoutingSettingActivity = activity
|
||||||
|
override fun getItemCount() = mActivity.rulesets.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||||
|
val ruleset = mActivity.rulesets[position]
|
||||||
|
|
||||||
|
holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks
|
||||||
|
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString()
|
||||||
|
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
|
||||||
|
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
|
||||||
|
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.looked == true
|
||||||
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
|
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
|
||||||
|
mActivity.startActivity(
|
||||||
|
Intent(mActivity, RoutingEditActivity::class.java)
|
||||||
|
.putExtra("position", position)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
|
if (!it.isPressed) return@setOnCheckedChangeListener
|
||||||
|
ruleset.enabled = isChecked
|
||||||
|
SettingsManager.saveRoutingRuleset(position, ruleset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
|
||||||
|
return MainViewHolder(
|
||||||
|
ItemRecyclerRoutingSettingBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainViewHolder(val itemRoutingSettingBinding: ItemRecyclerRoutingSettingBinding) :
|
||||||
|
BaseViewHolder(itemRoutingSettingBinding.root), ItemTouchHelperViewHolder
|
||||||
|
|
||||||
|
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
fun onItemSelected() {
|
||||||
|
itemView.setBackgroundColor(Color.LTGRAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onItemClear() {
|
||||||
|
itemView.setBackgroundColor(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
|
SettingsManager.swapRoutingRuleset(fromPosition, toPosition)
|
||||||
|
notifyItemMoved(fromPosition, toPosition)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemMoveCompleted() {
|
||||||
|
mActivity.refreshData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemDismiss(position: Int) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.ActivityRoutingSettingsBinding
|
|
||||||
|
|
||||||
class RoutingSettingsActivity : BaseActivity() {
|
|
||||||
private val binding by lazy { ActivityRoutingSettingsBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
private val titles: Array<out String> by lazy {
|
|
||||||
resources.getStringArray(R.array.routing_tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
title = getString(R.string.title_pref_routing_custom)
|
|
||||||
|
|
||||||
val fragments = ArrayList<Fragment>()
|
|
||||||
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_AGENT))
|
|
||||||
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_DIRECT))
|
|
||||||
fragments.add(RoutingSettingsFragment().newInstance(AppConfig.PREF_V2RAY_ROUTING_BLOCKED))
|
|
||||||
|
|
||||||
val adapter = FragmentAdapter(this, fragments)
|
|
||||||
binding.viewpager.adapter = adapter
|
|
||||||
//tablayout.setTabTextColors(Color.BLACK, Color.RED)
|
|
||||||
TabLayoutMediator(binding.tablayout, binding.viewpager) { tab, position ->
|
|
||||||
tab.text = titles[position]
|
|
||||||
}.attach()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.databinding.FragmentRoutingSettingsBinding
|
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.v2RayApplication
|
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class RoutingSettingsFragment : Fragment() {
|
|
||||||
private val binding by lazy { FragmentRoutingSettingsBinding.inflate(layoutInflater) }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val routing_arg = "routing_arg"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
// Inflate the layout for this fragment
|
|
||||||
return binding.root// inflater.inflate(R.layout.fragment_routing_settings, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newInstance(arg: String): Fragment {
|
|
||||||
val fragment = RoutingSettingsFragment()
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putString(routing_arg, arg)
|
|
||||||
fragment.arguments = bundle
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
val content = settingsStorage?.getString(requireArguments().getString(routing_arg), "")
|
|
||||||
binding.etRoutingContent.text = Utils.getEditable(content)
|
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
inflater.inflate(R.menu.menu_routing, menu)
|
|
||||||
return super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.save_routing -> {
|
|
||||||
saveRouting()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.del_routing -> {
|
|
||||||
binding.etRoutingContent.text = null
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.scan_replace -> {
|
|
||||||
scanQRcode(true)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.scan_append -> {
|
|
||||||
scanQRcode(false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.default_rules -> {
|
|
||||||
setDefaultRules()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveRouting() {
|
|
||||||
val content = binding.etRoutingContent.text.toString()
|
|
||||||
settingsStorage?.encode(requireArguments().getString(routing_arg), content)
|
|
||||||
activity?.toast(R.string.toast_success)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun scanQRcode(forReplace: Boolean): Boolean {
|
|
||||||
// try {
|
|
||||||
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
|
|
||||||
// .addCategory(Intent.CATEGORY_DEFAULT)
|
|
||||||
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
RxPermissions(requireActivity())
|
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.subscribe {
|
|
||||||
if (it)
|
|
||||||
if (forReplace)
|
|
||||||
scanQRCodeForReplace.launch(Intent(activity, ScannerActivity::class.java))
|
|
||||||
else
|
|
||||||
scanQRCodeForAppend.launch(Intent(activity, ScannerActivity::class.java))
|
|
||||||
else
|
|
||||||
activity?.toast(R.string.toast_permission_denied)
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
val content = it.data?.getStringExtra("SCAN_RESULT")
|
|
||||||
binding.etRoutingContent.text = Utils.getEditable(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scanQRCodeForAppend = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == RESULT_OK) {
|
|
||||||
val content = it.data?.getStringExtra("SCAN_RESULT")
|
|
||||||
binding.etRoutingContent.text = Utils.getEditable("${binding.etRoutingContent.text},$content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDefaultRules(): Boolean {
|
|
||||||
var url = AppConfig.v2rayCustomRoutingListUrl
|
|
||||||
var tag = ""
|
|
||||||
when (requireArguments().getString(routing_arg)) {
|
|
||||||
AppConfig.PREF_V2RAY_ROUTING_AGENT -> {
|
|
||||||
tag = AppConfig.TAG_PROXY
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT -> {
|
|
||||||
tag = AppConfig.TAG_DIRECT
|
|
||||||
}
|
|
||||||
|
|
||||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED -> {
|
|
||||||
tag = AppConfig.TAG_BLOCKED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
url += tag
|
|
||||||
|
|
||||||
activity?.toast(R.string.msg_downloading_content)
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val content = Utils.getUrlContext(url, 5000)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
val routingList = if (TextUtils.isEmpty(content)) {
|
|
||||||
Utils.readTextFromAssets(activity?.v2RayApplication, "custom_routing_$tag")
|
|
||||||
} else {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
binding.etRoutingContent.text = Utils.getEditable(routingList)
|
|
||||||
saveRouting()
|
|
||||||
//toast(R.string.toast_success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
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.util.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
|
||||||
class ScScannerActivity : BaseActivity() {
|
class ScScannerActivity : BaseActivity() {
|
||||||
|
|
||||||
@@ -20,26 +20,31 @@ class ScScannerActivity : BaseActivity() {
|
|||||||
fun importQRcode(): Boolean {
|
fun importQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
RxPermissions(this)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.subscribe {
|
.subscribe { granted ->
|
||||||
if (it)
|
if (granted) {
|
||||||
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
|
||||||
else
|
} else {
|
||||||
toast(R.string.toast_permission_denied)
|
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 (count, countSub) = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false)
|
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
|
||||||
|
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
|
||||||
|
|
||||||
if (count + countSub > 0) {
|
if (count + countSub > 0) {
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.toast_failure)
|
toast(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
startActivity(Intent(this, MainActivity::class.java))
|
startActivity(Intent(this, MainActivity::class.java))
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ 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 com.tbruyelle.rxpermissions3.RxPermissions
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
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
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import io.github.g00fy2.quickie.QRResult
|
import io.github.g00fy2.quickie.QRResult
|
||||||
import io.github.g00fy2.quickie.ScanCustomCode
|
import io.github.g00fy2.quickie.ScanCustomCode
|
||||||
@@ -22,12 +21,11 @@ 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 settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
|
||||||
launchScan()
|
launchScan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,19 +74,17 @@ class ScannerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
RxPermissions(this)
|
RxPermissions(this)
|
||||||
.request(permission)
|
.request(permission)
|
||||||
.subscribe {
|
.subscribe { granted ->
|
||||||
if (it) {
|
if (granted) {
|
||||||
try {
|
showFileChooser()
|
||||||
showFileChooser()
|
} else {
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
toast(R.string.toast_permission_denied)
|
toast(R.string.toast_permission_denied)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +105,21 @@ class ScannerActivity : BaseActivity() {
|
|||||||
val uri = it.data?.data
|
val uri = it.data?.data
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
if (it.resultCode == RESULT_OK && uri != null) {
|
||||||
try {
|
try {
|
||||||
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
|
val inputStream = contentResolver.openInputStream(uri)
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream?.close()
|
||||||
|
|
||||||
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
|
||||||
finished(text.orEmpty())
|
if (text.isNullOrEmpty()) {
|
||||||
|
toast(R.string.toast_decoding_failed)
|
||||||
|
} else {
|
||||||
|
finished(text)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
toast(e.message.toString())
|
toast(R.string.toast_decoding_failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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
|
||||||
@@ -12,40 +13,30 @@ import android.widget.LinearLayout
|
|||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.AppConfig.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.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_ADDRESS_V6
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
|
|
||||||
import com.v2ray.ang.extension.removeWhiteSpace
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.MmkvManager.ID_MAIN
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.Utils.getIpv6Address
|
|
||||||
|
|
||||||
class ServerActivity : BaseActivity() {
|
class ServerActivity : BaseActivity() {
|
||||||
|
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
|
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
|
||||||
private val isRunning by lazy {
|
private val isRunning by lazy {
|
||||||
intent.getBooleanExtra("isRunning", false)
|
intent.getBooleanExtra("isRunning", false)
|
||||||
&& editGuid.isNotEmpty()
|
&& editGuid.isNotEmpty()
|
||||||
&& editGuid == mainStorage?.decodeString(KEY_SELECTED_SERVER)
|
&& editGuid == MmkvManager.getSelectServer()
|
||||||
}
|
}
|
||||||
private val createConfigType by lazy {
|
private val createConfigType by lazy {
|
||||||
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value))
|
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value))
|
||||||
@@ -96,17 +87,16 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val et_address: EditText by lazy { findViewById(R.id.et_address) }
|
private val et_address: EditText by lazy { findViewById(R.id.et_address) }
|
||||||
private val et_port: EditText by lazy { findViewById(R.id.et_port) }
|
private val et_port: EditText by lazy { findViewById(R.id.et_port) }
|
||||||
private val et_id: EditText by lazy { findViewById(R.id.et_id) }
|
private val et_id: EditText by lazy { findViewById(R.id.et_id) }
|
||||||
private val et_alterId: EditText? by lazy { findViewById(R.id.et_alterId) }
|
|
||||||
private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
|
private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
|
||||||
private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
|
private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
|
||||||
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
|
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
|
||||||
private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
|
private val sp_stream_security: Spinner? by lazy { findViewById(R.id.sp_stream_security) }
|
||||||
private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
|
private val sp_allow_insecure: Spinner? by lazy { findViewById(R.id.sp_allow_insecure) }
|
||||||
private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.l5) }
|
private val container_allow_insecure: LinearLayout? by lazy { findViewById(R.id.lay_allow_insecure) }
|
||||||
private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
|
private val et_sni: EditText? by lazy { findViewById(R.id.et_sni) }
|
||||||
private val container_sni: LinearLayout? by lazy { findViewById(R.id.l2) }
|
private val container_sni: LinearLayout? by lazy { findViewById(R.id.lay_sni) }
|
||||||
private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
|
private val sp_stream_fingerprint: Spinner? by lazy { findViewById(R.id.sp_stream_fingerprint) } //uTLS
|
||||||
private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.l3) }
|
private val container_fingerprint: LinearLayout? by lazy { findViewById(R.id.lay_stream_fingerprint) }
|
||||||
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
|
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
|
||||||
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
|
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
|
||||||
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
|
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
|
||||||
@@ -115,18 +105,20 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) }
|
private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) }
|
||||||
private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
|
private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
|
||||||
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
|
||||||
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.l4) }
|
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
|
||||||
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
|
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
|
||||||
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.l6) }
|
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
|
||||||
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
|
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
|
||||||
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.l7) }
|
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
|
||||||
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
|
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
|
||||||
private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.l8) }
|
private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.lay_spider_x) }
|
||||||
private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
|
private val et_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
|
||||||
private val et_reserved2: EditText? by lazy { findViewById(R.id.et_reserved2) }
|
|
||||||
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
|
|
||||||
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
|
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
|
||||||
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
|
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
|
||||||
|
private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
|
||||||
|
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
|
||||||
|
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
|
||||||
|
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -139,9 +131,11 @@ class ServerActivity : BaseActivity() {
|
|||||||
EConfigType.CUSTOM -> return
|
EConfigType.CUSTOM -> return
|
||||||
EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
|
EConfigType.SHADOWSOCKS -> setContentView(R.layout.activity_server_shadowsocks)
|
||||||
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
|
EConfigType.SOCKS -> setContentView(R.layout.activity_server_socks)
|
||||||
|
EConfigType.HTTP -> setContentView(R.layout.activity_server_socks)
|
||||||
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
|
EConfigType.VLESS -> setContentView(R.layout.activity_server_vless)
|
||||||
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
|
EConfigType.TROJAN -> setContentView(R.layout.activity_server_trojan)
|
||||||
EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard)
|
EConfigType.WIREGUARD -> setContentView(R.layout.activity_server_wireguard)
|
||||||
|
EConfigType.HYSTERIA2 -> setContentView(R.layout.activity_server_hysteria2)
|
||||||
}
|
}
|
||||||
sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
sp_network?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(
|
override fun onItemSelected(
|
||||||
@@ -159,11 +153,31 @@ class ServerActivity : BaseActivity() {
|
|||||||
sp_header_type_title?.text = if (networks[position] == "grpc")
|
sp_header_type_title?.text = if (networks[position] == "grpc")
|
||||||
getString(R.string.server_lab_mode_type) else
|
getString(R.string.server_lab_mode_type) else
|
||||||
getString(R.string.server_lab_head_type)
|
getString(R.string.server_lab_head_type)
|
||||||
config?.getProxyOutbound()?.getTransportSettingDetails()?.let { transportDetails ->
|
sp_header_type?.setSelection(
|
||||||
sp_header_type?.setSelection(Utils.arrayFind(types, transportDetails[0]))
|
Utils.arrayFind(
|
||||||
et_request_host?.text = Utils.getEditable(transportDetails[1])
|
types,
|
||||||
et_path?.text = Utils.getEditable(transportDetails[2])
|
when (networks[position]) {
|
||||||
}
|
"grpc" -> config?.mode
|
||||||
|
else -> config?.headerType
|
||||||
|
}.orEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
et_request_host?.text = Utils.getEditable(
|
||||||
|
when (networks[position]) {
|
||||||
|
"quic" -> config?.quicSecurity
|
||||||
|
"grpc" -> config?.authority
|
||||||
|
else -> config?.host
|
||||||
|
}.orEmpty()
|
||||||
|
)
|
||||||
|
et_path?.text = Utils.getEditable(
|
||||||
|
when (networks[position]) {
|
||||||
|
"kcp" -> config?.seed
|
||||||
|
"quic" -> config?.quicKey
|
||||||
|
"grpc" -> config?.serviceName
|
||||||
|
else -> config?.path
|
||||||
|
}.orEmpty()
|
||||||
|
)
|
||||||
|
|
||||||
tv_request_host?.text = Utils.getEditable(
|
tv_request_host?.text = Utils.getEditable(
|
||||||
getString(
|
getString(
|
||||||
@@ -207,29 +221,46 @@ class ServerActivity : BaseActivity() {
|
|||||||
position: Int,
|
position: Int,
|
||||||
id: Long
|
id: Long
|
||||||
) {
|
) {
|
||||||
if (streamSecuritys[position].isBlank()) {
|
val isBlank = streamSecuritys[position].isBlank()
|
||||||
container_sni?.visibility = View.GONE
|
val isTLS = streamSecuritys[position] == TLS
|
||||||
container_fingerprint?.visibility = View.GONE
|
|
||||||
container_alpn?.visibility = View.GONE
|
when {
|
||||||
container_allow_insecure?.visibility = View.GONE
|
// Case 1: Null or blank
|
||||||
container_public_key?.visibility = View.GONE
|
isBlank -> {
|
||||||
container_short_id?.visibility = View.GONE
|
listOf(
|
||||||
container_spider_x?.visibility = View.GONE
|
container_sni, container_fingerprint, container_alpn,
|
||||||
} else {
|
container_allow_insecure, container_public_key,
|
||||||
container_sni?.visibility = View.VISIBLE
|
container_short_id, container_spider_x
|
||||||
container_fingerprint?.visibility = View.VISIBLE
|
).forEach { it?.visibility = View.GONE }
|
||||||
container_alpn?.visibility = View.VISIBLE
|
}
|
||||||
if (streamSecuritys[position] == TLS) {
|
|
||||||
|
// Case 2: TLS value
|
||||||
|
isTLS -> {
|
||||||
|
listOf(
|
||||||
|
container_sni,
|
||||||
|
container_fingerprint,
|
||||||
|
container_alpn
|
||||||
|
).forEach { it?.visibility = View.VISIBLE }
|
||||||
container_allow_insecure?.visibility = View.VISIBLE
|
container_allow_insecure?.visibility = View.VISIBLE
|
||||||
container_public_key?.visibility = View.GONE
|
listOf(
|
||||||
container_short_id?.visibility = View.GONE
|
container_public_key,
|
||||||
container_spider_x?.visibility = View.GONE
|
container_short_id,
|
||||||
} else {
|
container_spider_x
|
||||||
container_allow_insecure?.visibility = View.GONE
|
).forEach { it?.visibility = View.GONE }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3: Other reality values
|
||||||
|
else -> {
|
||||||
|
listOf(container_sni, container_fingerprint).forEach {
|
||||||
|
it?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
container_alpn?.visibility = View.GONE
|
container_alpn?.visibility = View.GONE
|
||||||
container_public_key?.visibility = View.VISIBLE
|
container_allow_insecure?.visibility = View.GONE
|
||||||
container_short_id?.visibility = View.VISIBLE
|
listOf(
|
||||||
container_spider_x?.visibility = View.VISIBLE
|
container_public_key,
|
||||||
|
container_short_id,
|
||||||
|
container_spider_x
|
||||||
|
).forEach { it?.visibility = View.VISIBLE }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,115 +279,99 @@ class ServerActivity : BaseActivity() {
|
|||||||
/**
|
/**
|
||||||
* binding selected server config
|
* binding selected server config
|
||||||
*/
|
*/
|
||||||
private fun bindingServer(config: ServerConfig): Boolean {
|
private fun bindingServer(config: ProfileItem): Boolean {
|
||||||
val outbound = config.getProxyOutbound() ?: return false
|
|
||||||
|
|
||||||
et_remarks.text = Utils.getEditable(config.remarks)
|
et_remarks.text = Utils.getEditable(config.remarks)
|
||||||
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
|
et_address.text = Utils.getEditable(config.server.orEmpty())
|
||||||
et_port.text =
|
et_port.text = Utils.getEditable(config.serverPort ?: DEFAULT_PORT.toString())
|
||||||
Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
|
et_id.text = Utils.getEditable(config.password.orEmpty())
|
||||||
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
|
|
||||||
et_alterId?.text =
|
if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
|
||||||
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
|
et_security?.text = Utils.getEditable(config.username.orEmpty())
|
||||||
if (config.configType == EConfigType.SOCKS) {
|
|
||||||
et_security?.text =
|
|
||||||
Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
|
|
||||||
} else if (config.configType == EConfigType.VLESS) {
|
} else if (config.configType == EConfigType.VLESS) {
|
||||||
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
|
et_security?.text = Utils.getEditable(config.method.orEmpty())
|
||||||
val flow = Utils.arrayFind(
|
val flow = Utils.arrayFind(flows, config.flow.orEmpty())
|
||||||
flows,
|
|
||||||
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty()
|
|
||||||
)
|
|
||||||
if (flow >= 0) {
|
if (flow >= 0) {
|
||||||
sp_flow?.setSelection(flow)
|
sp_flow?.setSelection(flow)
|
||||||
}
|
}
|
||||||
} else if (config.configType == EConfigType.WIREGUARD) {
|
} else if (config.configType == EConfigType.WIREGUARD) {
|
||||||
et_public_key?.text =
|
et_id.text = Utils.getEditable(config.secretKey.orEmpty())
|
||||||
Utils.getEditable(outbound.settings?.peers?.get(0)?.publicKey.orEmpty())
|
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||||
if (outbound.settings?.reserved == null) {
|
if (config.reserved == null) {
|
||||||
et_reserved1?.text = Utils.getEditable("0")
|
et_reserved1?.text = Utils.getEditable("0,0,0")
|
||||||
et_reserved2?.text = Utils.getEditable("0")
|
|
||||||
et_reserved3?.text = Utils.getEditable("0")
|
|
||||||
} else {
|
} else {
|
||||||
et_reserved1?.text =
|
et_reserved1?.text = Utils.getEditable(config.reserved?.toString())
|
||||||
Utils.getEditable(outbound.settings?.reserved?.get(0).toString())
|
|
||||||
et_reserved2?.text =
|
|
||||||
Utils.getEditable(outbound.settings?.reserved?.get(1).toString())
|
|
||||||
et_reserved3?.text =
|
|
||||||
Utils.getEditable(outbound.settings?.reserved?.get(2).toString())
|
|
||||||
}
|
}
|
||||||
if (outbound.settings?.address == null) {
|
if (config.localAddress == null) {
|
||||||
et_local_address?.text =
|
et_local_address?.text = Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
||||||
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
|
||||||
} else {
|
} else {
|
||||||
val list = outbound.settings?.address as List<*>
|
et_local_address?.text = Utils.getEditable(config.localAddress)
|
||||||
et_local_address?.text = Utils.getEditable(list.joinToString())
|
|
||||||
}
|
}
|
||||||
if (outbound.settings?.mtu == null) {
|
if (config.mtu == null) {
|
||||||
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
||||||
} else {
|
} else {
|
||||||
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString())
|
et_local_mtu?.text = Utils.getEditable(config.mtu.toString())
|
||||||
}
|
}
|
||||||
|
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
|
et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
|
||||||
|
et_port_hop?.text = Utils.getEditable(config.portHopping)
|
||||||
|
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
|
||||||
|
et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
|
||||||
}
|
}
|
||||||
val securityEncryptions =
|
|
||||||
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||||
val security =
|
val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
|
||||||
Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
|
|
||||||
if (security >= 0) {
|
if (security >= 0) {
|
||||||
sp_security?.setSelection(security)
|
sp_security?.setSelection(security)
|
||||||
}
|
}
|
||||||
|
|
||||||
val streamSetting = config.outboundBean?.streamSettings ?: return true
|
val streamSecurity = Utils.arrayFind(streamSecuritys, config.security.orEmpty())
|
||||||
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
|
|
||||||
if (streamSecurity >= 0) {
|
if (streamSecurity >= 0) {
|
||||||
sp_stream_security?.setSelection(streamSecurity)
|
sp_stream_security?.setSelection(streamSecurity)
|
||||||
(streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting ->
|
container_sni?.visibility = View.VISIBLE
|
||||||
container_sni?.visibility = View.VISIBLE
|
container_fingerprint?.visibility = View.VISIBLE
|
||||||
container_fingerprint?.visibility = View.VISIBLE
|
container_alpn?.visibility = View.VISIBLE
|
||||||
container_alpn?.visibility = View.VISIBLE
|
|
||||||
et_sni?.text = Utils.getEditable(tlsSetting.serverName)
|
et_sni?.text = Utils.getEditable(config.sni)
|
||||||
tlsSetting.fingerprint?.let {
|
config.fingerPrint?.let {
|
||||||
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint)
|
val utlsIndex = Utils.arrayFind(uTlsItems, it)
|
||||||
sp_stream_fingerprint?.setSelection(utlsIndex)
|
sp_stream_fingerprint?.setSelection(utlsIndex)
|
||||||
}
|
|
||||||
tlsSetting.alpn?.let {
|
|
||||||
val alpnIndex = Utils.arrayFind(
|
|
||||||
alpns,
|
|
||||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
|
||||||
)
|
|
||||||
sp_stream_alpn?.setSelection(alpnIndex)
|
|
||||||
}
|
|
||||||
if (streamSetting.tlsSettings != null) {
|
|
||||||
container_allow_insecure?.visibility = View.VISIBLE
|
|
||||||
val allowinsecure =
|
|
||||||
Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
|
|
||||||
if (allowinsecure >= 0) {
|
|
||||||
sp_allow_insecure?.setSelection(allowinsecure)
|
|
||||||
}
|
|
||||||
container_public_key?.visibility = View.GONE
|
|
||||||
container_short_id?.visibility = View.GONE
|
|
||||||
container_spider_x?.visibility = View.GONE
|
|
||||||
} else { // reality settings
|
|
||||||
container_public_key?.visibility = View.VISIBLE
|
|
||||||
et_public_key?.text = Utils.getEditable(tlsSetting.publicKey.orEmpty())
|
|
||||||
container_short_id?.visibility = View.VISIBLE
|
|
||||||
et_short_id?.text = Utils.getEditable(tlsSetting.shortId.orEmpty())
|
|
||||||
container_spider_x?.visibility = View.VISIBLE
|
|
||||||
et_spider_x?.text = Utils.getEditable(tlsSetting.spiderX.orEmpty())
|
|
||||||
container_allow_insecure?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (streamSetting.tlsSettings == null && streamSetting.realitySettings == null) {
|
config.alpn?.let {
|
||||||
container_sni?.visibility = View.GONE
|
val alpnIndex = Utils.arrayFind(alpns, it)
|
||||||
container_fingerprint?.visibility = View.GONE
|
sp_stream_alpn?.setSelection(alpnIndex)
|
||||||
container_alpn?.visibility = View.GONE
|
}
|
||||||
container_allow_insecure?.visibility = View.GONE
|
if (config.security == TLS) {
|
||||||
|
container_allow_insecure?.visibility = View.VISIBLE
|
||||||
|
val allowinsecure = Utils.arrayFind(allowinsecures, config.insecure.toString())
|
||||||
|
if (allowinsecure >= 0) {
|
||||||
|
sp_allow_insecure?.setSelection(allowinsecure)
|
||||||
|
}
|
||||||
container_public_key?.visibility = View.GONE
|
container_public_key?.visibility = View.GONE
|
||||||
container_short_id?.visibility = View.GONE
|
container_short_id?.visibility = View.GONE
|
||||||
container_spider_x?.visibility = View.GONE
|
container_spider_x?.visibility = View.GONE
|
||||||
|
} else if (config.security == REALITY) { // reality settings
|
||||||
|
container_public_key?.visibility = View.VISIBLE
|
||||||
|
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
|
||||||
|
container_short_id?.visibility = View.VISIBLE
|
||||||
|
et_short_id?.text = Utils.getEditable(config.shortId.orEmpty())
|
||||||
|
container_spider_x?.visibility = View.VISIBLE
|
||||||
|
et_spider_x?.text = Utils.getEditable(config.spiderX.orEmpty())
|
||||||
|
container_allow_insecure?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val network = Utils.arrayFind(networks, streamSetting.network)
|
|
||||||
|
if (config.security.isNullOrEmpty()) {
|
||||||
|
container_sni?.visibility = View.GONE
|
||||||
|
container_fingerprint?.visibility = View.GONE
|
||||||
|
container_alpn?.visibility = View.GONE
|
||||||
|
container_allow_insecure?.visibility = View.GONE
|
||||||
|
container_public_key?.visibility = View.GONE
|
||||||
|
container_short_id?.visibility = View.GONE
|
||||||
|
container_spider_x?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val network = Utils.arrayFind(networks, config.network.orEmpty())
|
||||||
if (network >= 0) {
|
if (network >= 0) {
|
||||||
sp_network?.setSelection(network)
|
sp_network?.setSelection(network)
|
||||||
}
|
}
|
||||||
@@ -371,7 +386,6 @@ class ServerActivity : BaseActivity() {
|
|||||||
et_address.text = null
|
et_address.text = null
|
||||||
et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
|
et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
|
||||||
et_id.text = null
|
et_id.text = null
|
||||||
et_alterId?.text = Utils.getEditable("0")
|
|
||||||
sp_security?.setSelection(0)
|
sp_security?.setSelection(0)
|
||||||
sp_network?.setSelection(0)
|
sp_network?.setSelection(0)
|
||||||
|
|
||||||
@@ -385,9 +399,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
//et_security.text = null
|
//et_security.text = null
|
||||||
sp_flow?.setSelection(0)
|
sp_flow?.setSelection(0)
|
||||||
et_public_key?.text = null
|
et_public_key?.text = null
|
||||||
et_reserved1?.text = Utils.getEditable("0")
|
et_reserved1?.text = Utils.getEditable("0,0,0")
|
||||||
et_reserved2?.text = Utils.getEditable("0")
|
|
||||||
et_reserved3?.text = Utils.getEditable("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},${WIREGUARD_LOCAL_ADDRESS_V6}")
|
||||||
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
|
||||||
@@ -406,15 +418,21 @@ class ServerActivity : BaseActivity() {
|
|||||||
toast(R.string.server_lab_address)
|
toast(R.string.server_lab_address)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val port = Utils.parseInt(et_port.text.toString())
|
if (createConfigType != EConfigType.HYSTERIA2) {
|
||||||
if (port <= 0) {
|
if (Utils.parseInt(et_port.text.toString()) <= 0) {
|
||||||
toast(R.string.server_lab_port)
|
toast(R.string.server_lab_port)
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val config =
|
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
|
||||||
MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
|
if (config.configType != EConfigType.SOCKS
|
||||||
if (config.configType != EConfigType.SOCKS && TextUtils.isEmpty(et_id.text.toString())) {
|
&& config.configType != EConfigType.HTTP
|
||||||
if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.SHADOWSOCKS) {
|
&& TextUtils.isEmpty(et_id.text.toString())
|
||||||
|
) {
|
||||||
|
if (config.configType == EConfigType.TROJAN
|
||||||
|
|| config.configType == EConfigType.SHADOWSOCKS
|
||||||
|
|| config.configType == EConfigType.HYSTERIA2
|
||||||
|
) {
|
||||||
toast(R.string.server_lab_id3)
|
toast(R.string.server_lab_id3)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.server_lab_id)
|
toast(R.string.server_lab_id)
|
||||||
@@ -427,142 +445,97 @@ class ServerActivity : BaseActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
et_alterId?.let {
|
|
||||||
val alterId = Utils.parseInt(it.text.toString())
|
|
||||||
if (alterId < 0) {
|
|
||||||
toast(R.string.server_lab_alterid)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.remarks = et_remarks.text.toString().trim()
|
saveCommon(config)
|
||||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
saveStreamSettings(config)
|
||||||
saveVnext(vnext, port, config)
|
saveTls(config)
|
||||||
}
|
|
||||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
saveServers(server, port, config)
|
|
||||||
}
|
|
||||||
val wireguard = config.outboundBean?.settings
|
|
||||||
wireguard?.peers?.get(0)?.let { _ ->
|
|
||||||
savePeer(wireguard, port)
|
|
||||||
}
|
|
||||||
config.outboundBean?.streamSettings?.let {
|
|
||||||
saveStreamSettings(it)
|
|
||||||
}
|
|
||||||
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))
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveVnext(
|
private fun saveCommon(config: ProfileItem) {
|
||||||
vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean,
|
config.remarks = et_remarks.text.toString().trim()
|
||||||
port: Int,
|
config.server = et_address.text.toString().trim()
|
||||||
config: ServerConfig
|
config.serverPort = et_port.text.toString().trim()
|
||||||
) {
|
config.password = et_id.text.toString().trim()
|
||||||
vnext.address = et_address.text.toString().trim()
|
|
||||||
vnext.port = port
|
|
||||||
vnext.users[0].id = et_id.text.toString().trim()
|
|
||||||
if (config.configType == EConfigType.VMESS) {
|
|
||||||
vnext.users[0].alterId = Utils.parseInt(et_alterId?.text.toString())
|
|
||||||
vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0]
|
|
||||||
} else if (config.configType == EConfigType.VLESS) {
|
|
||||||
vnext.users[0].encryption = et_security?.text.toString().trim()
|
|
||||||
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveServers(
|
if (config.configType == EConfigType.VMESS) {
|
||||||
server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean,
|
config.method = securitys[sp_security?.selectedItemPosition ?: 0]
|
||||||
port: Int,
|
} else if (config.configType == EConfigType.VLESS) {
|
||||||
config: ServerConfig
|
config.method = et_security?.text.toString().trim()
|
||||||
) {
|
config.flow = flows[sp_flow?.selectedItemPosition ?: 0]
|
||||||
server.address = et_address.text.toString().trim()
|
} else if (config.configType == EConfigType.SHADOWSOCKS) {
|
||||||
server.port = port
|
config.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
|
||||||
if (config.configType == EConfigType.SHADOWSOCKS) {
|
} else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
|
||||||
server.password = et_id.text.toString().trim()
|
if (!TextUtils.isEmpty(et_security?.text) || !TextUtils.isEmpty(et_id.text)) {
|
||||||
server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
|
config.username = et_security?.text.toString().trim()
|
||||||
} else if (config.configType == EConfigType.SOCKS) {
|
|
||||||
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
|
|
||||||
server.users = null
|
|
||||||
} else {
|
|
||||||
val socksUsersBean =
|
|
||||||
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
|
||||||
socksUsersBean.user = et_security?.text.toString().trim()
|
|
||||||
socksUsersBean.pass = et_id.text.toString().trim()
|
|
||||||
server.users = listOf(socksUsersBean)
|
|
||||||
}
|
}
|
||||||
} else if (config.configType == EConfigType.TROJAN) {
|
} else if (config.configType == EConfigType.TROJAN) {
|
||||||
server.password = et_id.text.toString().trim()
|
} else if (config.configType == EConfigType.WIREGUARD) {
|
||||||
|
config.secretKey = et_id.text.toString().trim()
|
||||||
|
config.publicKey = et_public_key?.text.toString().trim()
|
||||||
|
config.reserved = et_reserved1?.text.toString().trim()
|
||||||
|
config.localAddress = et_local_address?.text.toString().trim()
|
||||||
|
config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
||||||
|
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
|
config.obfsPassword = et_obfs_password?.text?.toString()
|
||||||
|
config.portHopping = et_port_hop?.text?.toString()
|
||||||
|
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
|
||||||
|
config.pinSHA256 = et_pinsha256?.text?.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun savePeer(wireguard: V2rayConfig.OutboundBean.OutSettingsBean, port: Int) {
|
|
||||||
wireguard.secretKey = et_id.text.toString().trim()
|
|
||||||
wireguard.peers?.get(0)?.publicKey = et_public_key?.text.toString().trim()
|
|
||||||
wireguard.peers?.get(0)?.endpoint =
|
|
||||||
getIpv6Address(et_address.text.toString().trim()) + ":" + port
|
|
||||||
val reserved1 = Utils.parseInt(et_reserved1?.text.toString())
|
|
||||||
val reserved2 = Utils.parseInt(et_reserved2?.text.toString())
|
|
||||||
val reserved3 = Utils.parseInt(et_reserved3?.text.toString())
|
|
||||||
if (reserved1 > 0 || reserved2 > 0 || reserved3 > 0) {
|
|
||||||
wireguard.reserved = listOf(reserved1, reserved2, reserved3)
|
|
||||||
} else {
|
|
||||||
wireguard.reserved = null
|
|
||||||
}
|
|
||||||
wireguard.address = et_local_address?.text.toString().removeWhiteSpace().split(",")
|
|
||||||
wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) {
|
private fun saveStreamSettings(profileItem: ProfileItem) {
|
||||||
val network = sp_network?.selectedItemPosition ?: return
|
val network = sp_network?.selectedItemPosition ?: return
|
||||||
val type = sp_header_type?.selectedItemPosition ?: return
|
val type = sp_header_type?.selectedItemPosition ?: return
|
||||||
val requestHost = et_request_host?.text?.toString()?.trim() ?: return
|
val requestHost = et_request_host?.text?.toString()?.trim() ?: return
|
||||||
val path = et_path?.text?.toString()?.trim() ?: return
|
val path = et_path?.text?.toString()?.trim() ?: return
|
||||||
val sniField = et_sni?.text?.toString()?.trim() ?: return
|
|
||||||
val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
|
profileItem.network = networks[network]
|
||||||
|
profileItem.headerType = transportTypes(networks[network])[type]
|
||||||
|
profileItem.host = requestHost
|
||||||
|
profileItem.path = path
|
||||||
|
profileItem.seed = path
|
||||||
|
profileItem.quicSecurity = requestHost
|
||||||
|
profileItem.quicKey = path
|
||||||
|
profileItem.mode = transportTypes(networks[network])[type]
|
||||||
|
profileItem.serviceName = path
|
||||||
|
profileItem.authority = requestHost
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveTls(config: ProfileItem) {
|
||||||
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
||||||
val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
|
val sniField = et_sni?.text?.toString()?.trim()
|
||||||
val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return
|
val allowInsecureField = sp_allow_insecure?.selectedItemPosition
|
||||||
val publicKey = et_public_key?.text?.toString()?.trim() ?: return
|
val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: 0
|
||||||
val shortId = et_short_id?.text?.toString()?.trim() ?: return
|
val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: 0
|
||||||
val spiderX = et_spider_x?.text?.toString()?.trim() ?: return
|
val publicKey = et_public_key?.text?.toString()
|
||||||
|
val shortId = et_short_id?.text?.toString()
|
||||||
|
val spiderX = et_spider_x?.text?.toString()
|
||||||
|
|
||||||
var sni = streamSetting.populateTransportSettings(
|
val allowInsecure =
|
||||||
transport = networks[network],
|
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
|
||||||
headerType = transportTypes(networks[network])[type],
|
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE) == true
|
||||||
host = requestHost,
|
} else {
|
||||||
path = path,
|
allowinsecures[allowInsecureField].toBoolean()
|
||||||
seed = path,
|
}
|
||||||
quicSecurity = requestHost,
|
|
||||||
key = path,
|
|
||||||
mode = transportTypes(networks[network])[type],
|
|
||||||
serviceName = path,
|
|
||||||
authority = requestHost,
|
|
||||||
)
|
|
||||||
if (sniField.isNotBlank()) {
|
|
||||||
sni = sniField
|
|
||||||
}
|
|
||||||
val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) {
|
|
||||||
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
|
|
||||||
} else {
|
|
||||||
allowinsecures[allowInsecureField].toBoolean()
|
|
||||||
}
|
|
||||||
|
|
||||||
streamSetting.populateTlsSettings(
|
config.security = streamSecuritys[streamSecurity]
|
||||||
streamSecurity = streamSecuritys[streamSecurity],
|
config.insecure = allowInsecure
|
||||||
allowInsecure = allowInsecure,
|
config.sni = sniField
|
||||||
sni = sni,
|
config.fingerPrint = uTlsItems[utlsIndex]
|
||||||
fingerprint = uTlsItems[utlsIndex],
|
config.alpn = alpns[alpnIndex]
|
||||||
alpns = alpns[alpnIndex],
|
config.publicKey = publicKey
|
||||||
publicKey = publicKey,
|
config.shortId = shortId
|
||||||
shortId = shortId,
|
config.spiderX = spiderX
|
||||||
spiderX = spiderX
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun transportTypes(network: String?): Array<out String> {
|
private fun transportTypes(network: String?): Array<out String> {
|
||||||
@@ -586,12 +559,12 @@ class ServerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save server config
|
* delete server config
|
||||||
*/
|
*/
|
||||||
private fun deleteServer(): Boolean {
|
private fun deleteServer(): Boolean {
|
||||||
if (editGuid.isNotEmpty()) {
|
if (editGuid.isNotEmpty()) {
|
||||||
if (editGuid != mainStorage?.decodeString(KEY_SELECTED_SERVER)) {
|
if (editGuid != MmkvManager.getSelectServer()) {
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
MmkvManager.removeServer(editGuid)
|
MmkvManager.removeServer(editGuid)
|
||||||
|
|||||||
@@ -8,28 +8,24 @@ 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.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
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.ServerConfig
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import me.drakeet.support.toast.ToastCompat
|
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) }
|
||||||
|
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
|
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
|
||||||
private val isRunning by lazy {
|
private val isRunning by lazy {
|
||||||
intent.getBooleanExtra("isRunning", false)
|
intent.getBooleanExtra("isRunning", false)
|
||||||
&& editGuid.isNotEmpty()
|
&& editGuid.isNotEmpty()
|
||||||
&& editGuid == mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER)
|
&& editGuid == MmkvManager.getSelectServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -50,16 +46,14 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bingding seleced server config
|
* Binding selected server config
|
||||||
*/
|
*/
|
||||||
private fun bindingServer(config: ServerConfig): Boolean {
|
private fun bindingServer(config: ProfileItem): Boolean {
|
||||||
binding.etRemarks.text = Utils.getEditable(config.remarks)
|
binding.etRemarks.text = Utils.getEditable(config.remarks)
|
||||||
val raw = serverRawStorage?.decodeString(editGuid)
|
val raw = MmkvManager.decodeServerRaw(editGuid)
|
||||||
if (raw.isNullOrBlank()) {
|
val configContent = raw.orEmpty()
|
||||||
binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty()))
|
|
||||||
} else {
|
binding.editor.setTextContent(Utils.getEditable(configContent))
|
||||||
binding.editor.setTextContent(Utils.getEditable(raw))
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,20 +74,23 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val v2rayConfig = try {
|
val profileItem = try {
|
||||||
Gson().fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
|
CustomFmt.parse(binding.editor.text.toString())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
|
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM)
|
||||||
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString()
|
binding.etRemarks.text.let {
|
||||||
config.fullConfig = v2rayConfig
|
config.remarks = if (it.isNullOrEmpty()) profileItem?.remarks.orEmpty() else it.toString()
|
||||||
|
}
|
||||||
|
config.server = profileItem?.server
|
||||||
|
config.serverPort = profileItem?.serverPort
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
serverRawStorage?.encode(editGuid, binding.editor.text.toString())
|
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ import androidx.activity.viewModels
|
|||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.PeriodicWorkRequest
|
import androidx.work.PeriodicWorkRequest
|
||||||
import androidx.work.multiprocess.RemoteWorkManager
|
import androidx.work.multiprocess.RemoteWorkManager
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
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.VPN
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.extension.toLongEx
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.service.SubscriptionUpdater
|
import com.v2ray.ang.service.SubscriptionUpdater
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.viewmodel.SettingsViewModel
|
import com.v2ray.ang.viewmodel.SettingsViewModel
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -36,7 +36,6 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
|
||||||
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
|
||||||
@@ -44,8 +43,6 @@ 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 routingCustom by lazy { findPreference<Preference>(AppConfig.PREF_ROUTING_CUSTOM) }
|
|
||||||
|
|
||||||
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) }
|
||||||
private val muxXudpConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_XUDP_CONCURRENCY) }
|
private val muxXudpConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_XUDP_CONCURRENCY) }
|
||||||
@@ -89,11 +86,6 @@ class SettingsActivity : BaseActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
routingCustom?.setOnPreferenceClickListener {
|
|
||||||
startActivity(Intent(activity, RoutingSettingsActivity::class.java))
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
mux?.setOnPreferenceChangeListener { _, newValue ->
|
mux?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
updateMux(newValue as Boolean)
|
updateMux(newValue as Boolean)
|
||||||
true
|
true
|
||||||
@@ -128,7 +120,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
val value = newValue as Boolean
|
val value = newValue as Boolean
|
||||||
autoUpdateCheck?.isChecked = value
|
autoUpdateCheck?.isChecked = value
|
||||||
autoUpdateInterval?.isEnabled = value
|
autoUpdateInterval?.isEnabled = value
|
||||||
autoUpdateInterval?.text?.toLong()?.let {
|
autoUpdateInterval?.text?.toLongEx()?.let {
|
||||||
if (newValue) configureUpdateTask(it) else cancelUpdateTask()
|
if (newValue) configureUpdateTask(it) else cancelUpdateTask()
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -138,9 +130,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
// It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
|
// It must be greater than 15 minutes because WorkManager couldn't run tasks under 15 minutes intervals
|
||||||
nval =
|
nval =
|
||||||
if (TextUtils.isEmpty(nval) || nval.toLong() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
|
if (TextUtils.isEmpty(nval) || nval.toLongEx() < 15) AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL else nval
|
||||||
autoUpdateInterval?.summary = nval
|
autoUpdateInterval?.summary = nval
|
||||||
configureUpdateTask(nval.toLong())
|
configureUpdateTask(nval.toLongEx())
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,33 +172,33 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, "VPN"))
|
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
|
||||||
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||||
fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
||||||
localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
||||||
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
||||||
|
|
||||||
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false))
|
updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false))
|
||||||
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)
|
mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
||||||
muxConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8")
|
muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")
|
||||||
muxXudpConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
|
||||||
|
|
||||||
updateFragment(settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false))
|
updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false))
|
||||||
fragment?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false)
|
fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)
|
||||||
fragmentPackets?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
|
fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
|
||||||
fragmentLength?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
|
||||||
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
||||||
|
|
||||||
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
autoUpdateInterval?.summary =
|
autoUpdateInterval?.summary =
|
||||||
settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
||||||
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
|
|
||||||
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||||
httpPort?.summary = settingsStorage.decodeString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
|
httpPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
|
||||||
remoteDns?.summary = settingsStorage.decodeString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
|
||||||
domesticDns?.summary = settingsStorage.decodeString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
|
||||||
delayTestUrl?.summary = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
|
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
|
||||||
|
|
||||||
initSharedPreference()
|
initSharedPreference()
|
||||||
}
|
}
|
||||||
@@ -233,11 +225,12 @@ class SettingsActivity : BaseActivity() {
|
|||||||
AppConfig.PREF_SNIFFING_ENABLED,
|
AppConfig.PREF_SNIFFING_ENABLED,
|
||||||
).forEach { key ->
|
).forEach { key ->
|
||||||
findPreference<CheckBoxPreference>(key)?.isChecked =
|
findPreference<CheckBoxPreference>(key)?.isChecked =
|
||||||
settingsStorage.decodeBool(key, true)
|
MmkvManager.decodeSettingsBool(key, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
AppConfig.PREF_ROUTE_ONLY_ENABLED,
|
AppConfig.PREF_ROUTE_ONLY_ENABLED,
|
||||||
|
AppConfig.PREF_IS_BOOTED,
|
||||||
AppConfig.PREF_BYPASS_APPS,
|
AppConfig.PREF_BYPASS_APPS,
|
||||||
AppConfig.PREF_SPEED_ENABLED,
|
AppConfig.PREF_SPEED_ENABLED,
|
||||||
AppConfig.PREF_CONFIRM_REMOVE,
|
AppConfig.PREF_CONFIRM_REMOVE,
|
||||||
@@ -247,12 +240,11 @@ class SettingsActivity : BaseActivity() {
|
|||||||
AppConfig.PREF_ALLOW_INSECURE
|
AppConfig.PREF_ALLOW_INSECURE
|
||||||
).forEach { key ->
|
).forEach { key ->
|
||||||
findPreference<CheckBoxPreference>(key)?.isChecked =
|
findPreference<CheckBoxPreference>(key)?.isChecked =
|
||||||
settingsStorage.decodeBool(key, false)
|
MmkvManager.decodeSettingsBool(key, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
||||||
AppConfig.PREF_ROUTING_MODE,
|
|
||||||
AppConfig.PREF_MUX_XUDP_QUIC,
|
AppConfig.PREF_MUX_XUDP_QUIC,
|
||||||
AppConfig.PREF_FRAGMENT_PACKETS,
|
AppConfig.PREF_FRAGMENT_PACKETS,
|
||||||
AppConfig.PREF_LANGUAGE,
|
AppConfig.PREF_LANGUAGE,
|
||||||
@@ -260,23 +252,23 @@ class SettingsActivity : BaseActivity() {
|
|||||||
AppConfig.PREF_LOGLEVEL,
|
AppConfig.PREF_LOGLEVEL,
|
||||||
AppConfig.PREF_MODE
|
AppConfig.PREF_MODE
|
||||||
).forEach { key ->
|
).forEach { key ->
|
||||||
if (settingsStorage.decodeString(key) != null) {
|
if (MmkvManager.decodeSettingsString(key) != null) {
|
||||||
findPreference<ListPreference>(key)?.value = settingsStorage.decodeString(key)
|
findPreference<ListPreference>(key)?.value = MmkvManager.decodeSettingsString(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMode(mode: String?) {
|
private fun updateMode(mode: String?) {
|
||||||
val vpn = mode == "VPN"
|
val vpn = mode == VPN
|
||||||
perAppProxy?.isEnabled = vpn
|
perAppProxy?.isEnabled = vpn
|
||||||
perAppProxy?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
|
||||||
localDns?.isEnabled = vpn
|
localDns?.isEnabled = vpn
|
||||||
fakeDns?.isEnabled = vpn
|
fakeDns?.isEnabled = vpn
|
||||||
localDnsPort?.isEnabled = vpn
|
localDnsPort?.isEnabled = vpn
|
||||||
vpnDns?.isEnabled = vpn
|
vpnDns?.isEnabled = vpn
|
||||||
if (vpn) {
|
if (vpn) {
|
||||||
updateLocalDns(
|
updateLocalDns(
|
||||||
settingsStorage.getBoolean(
|
MmkvManager.decodeSettingsBool(
|
||||||
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
@@ -318,19 +310,17 @@ class SettingsActivity : BaseActivity() {
|
|||||||
muxXudpConcurrency?.isEnabled = enabled
|
muxXudpConcurrency?.isEnabled = enabled
|
||||||
muxXudpQuic?.isEnabled = enabled
|
muxXudpQuic?.isEnabled = enabled
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
updateMuxConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
|
updateMuxConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
|
||||||
updateMuxXudpConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
|
updateMuxXudpConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMuxConcurrency(value: String?) {
|
private fun updateMuxConcurrency(value: String?) {
|
||||||
if (value == null) {
|
val concurrency = value?.toIntOrNull() ?: 8
|
||||||
} else {
|
muxConcurrency?.summary = concurrency.toString()
|
||||||
val concurrency = value.toIntOrNull() ?: 8
|
|
||||||
muxConcurrency?.summary = concurrency.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun updateMuxXudpConcurrency(value: String?) {
|
private fun updateMuxXudpConcurrency(value: String?) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
muxXudpQuic?.isEnabled = true
|
muxXudpQuic?.isEnabled = true
|
||||||
@@ -346,9 +336,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
fragmentLength?.isEnabled = enabled
|
fragmentLength?.isEnabled = enabled
|
||||||
fragmentInterval?.isEnabled = enabled
|
fragmentInterval?.isEnabled = enabled
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
updateFragmentPackets(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
|
updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
|
||||||
updateFragmentLength(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
|
updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
|
||||||
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
|
updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ 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.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
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.util.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
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -23,7 +21,6 @@ class SubEditActivity : BaseActivity() {
|
|||||||
var del_config: MenuItem? = null
|
var del_config: MenuItem? = null
|
||||||
var save_config: MenuItem? = null
|
var save_config: MenuItem? = null
|
||||||
|
|
||||||
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
|
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -31,9 +28,9 @@ class SubEditActivity : BaseActivity() {
|
|||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
title = getString(R.string.title_sub_setting)
|
title = getString(R.string.title_sub_setting)
|
||||||
|
|
||||||
val json = subStorage?.decodeString(editSubId)
|
val subItem = MmkvManager.decodeSubscription(editSubId)
|
||||||
if (!json.isNullOrBlank()) {
|
if (subItem != null) {
|
||||||
bindingServer(Gson().fromJson(json, SubscriptionItem::class.java))
|
bindingServer(subItem)
|
||||||
} else {
|
} else {
|
||||||
clearServer()
|
clearServer()
|
||||||
}
|
}
|
||||||
@@ -45,8 +42,11 @@ class SubEditActivity : BaseActivity() {
|
|||||||
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)
|
||||||
binding.etUrl.text = Utils.getEditable(subItem.url)
|
binding.etUrl.text = Utils.getEditable(subItem.url)
|
||||||
|
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.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
|
||||||
|
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,10 @@ class SubEditActivity : BaseActivity() {
|
|||||||
private fun clearServer(): Boolean {
|
private fun clearServer(): Boolean {
|
||||||
binding.etRemarks.text = null
|
binding.etRemarks.text = null
|
||||||
binding.etUrl.text = null
|
binding.etUrl.text = null
|
||||||
|
binding.etFilter.text = null
|
||||||
binding.chkEnable.isChecked = true
|
binding.chkEnable.isChecked = true
|
||||||
|
binding.etPreProfile.text = null
|
||||||
|
binding.etNextProfile.text = null
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,31 +67,33 @@ class SubEditActivity : BaseActivity() {
|
|||||||
* save server config
|
* save server config
|
||||||
*/
|
*/
|
||||||
private fun saveServer(): Boolean {
|
private fun saveServer(): Boolean {
|
||||||
val subItem: SubscriptionItem
|
val subItem = MmkvManager.decodeSubscription(editSubId) ?: SubscriptionItem()
|
||||||
val json = subStorage?.decodeString(editSubId)
|
|
||||||
var subId = editSubId
|
|
||||||
if (!json.isNullOrBlank()) {
|
|
||||||
subItem = Gson().fromJson(json, SubscriptionItem::class.java)
|
|
||||||
} else {
|
|
||||||
subId = Utils.getUuid()
|
|
||||||
subItem = SubscriptionItem()
|
|
||||||
}
|
|
||||||
|
|
||||||
subItem.remarks = binding.etRemarks.text.toString()
|
subItem.remarks = binding.etRemarks.text.toString()
|
||||||
subItem.url = binding.etUrl.text.toString()
|
subItem.url = binding.etUrl.text.toString()
|
||||||
|
subItem.filter = binding.etFilter.text.toString()
|
||||||
subItem.enabled = binding.chkEnable.isChecked
|
subItem.enabled = binding.chkEnable.isChecked
|
||||||
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
|
||||||
|
subItem.prevProfile = binding.etPreProfile.text.toString()
|
||||||
|
subItem.nextProfile = binding.etNextProfile.text.toString()
|
||||||
|
|
||||||
if (TextUtils.isEmpty(subItem.remarks)) {
|
if (TextUtils.isEmpty(subItem.remarks)) {
|
||||||
toast(R.string.sub_setting_remarks)
|
toast(R.string.sub_setting_remarks)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// if (TextUtils.isEmpty(subItem.url)) {
|
if (subItem.url.isNotEmpty()) {
|
||||||
// toast(R.string.sub_setting_url)
|
if (!Utils.isValidUrl(subItem.url)) {
|
||||||
// return false
|
toast(R.string.toast_invalid_url)
|
||||||
// }
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
subStorage?.encode(subId, Gson().toJson(subItem))
|
if (!Utils.isValidSubUrl(subItem.url)) {
|
||||||
|
toast(R.string.toast_insecure_url_protocol)
|
||||||
|
//return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MmkvManager.encodeSubscription(editSubId, subItem)
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ 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 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.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.toast
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -23,6 +25,7 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
|
|
||||||
var subscriptions: List<Pair<String, SubscriptionItem>> = listOf()
|
var subscriptions: List<Pair<String, SubscriptionItem>> = listOf()
|
||||||
private val adapter by lazy { SubSettingRecyclerAdapter(this) }
|
private val adapter by lazy { SubSettingRecyclerAdapter(this) }
|
||||||
|
private var mItemTouchHelper: ItemTouchHelper? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -33,12 +36,14 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
|
||||||
|
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
subscriptions = MmkvManager.decodeSubscriptions()
|
refreshData()
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -77,4 +82,9 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshData() {
|
||||||
|
subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,21 +8,20 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
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
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
|
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
|
||||||
|
|
||||||
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) :
|
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
|
||||||
RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
|
|
||||||
|
|
||||||
private var mActivity: SubSettingActivity = activity
|
private var mActivity: SubSettingActivity = activity
|
||||||
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
private val share_method: Array<out String> by lazy {
|
private val share_method: Array<out String> by lazy {
|
||||||
mActivity.resources.getStringArray(R.array.share_sub_method)
|
mActivity.resources.getStringArray(R.array.share_sub_method)
|
||||||
@@ -35,11 +34,7 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) :
|
|||||||
val subItem = mActivity.subscriptions[position].second
|
val subItem = mActivity.subscriptions[position].second
|
||||||
holder.itemSubSettingBinding.tvName.text = subItem.remarks
|
holder.itemSubSettingBinding.tvName.text = subItem.remarks
|
||||||
holder.itemSubSettingBinding.tvUrl.text = subItem.url
|
holder.itemSubSettingBinding.tvUrl.text = subItem.url
|
||||||
if (subItem.enabled) {
|
holder.itemSubSettingBinding.chkEnable.isChecked = subItem.enabled
|
||||||
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(R.color.colorAccent)
|
|
||||||
} else {
|
|
||||||
holder.itemSubSettingBinding.chkEnable.setBackgroundResource(0)
|
|
||||||
}
|
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
|
||||||
@@ -48,10 +43,12 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) :
|
|||||||
.putExtra("subId", subId)
|
.putExtra("subId", subId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
holder.itemSubSettingBinding.infoContainer.setOnClickListener {
|
|
||||||
subItem.enabled = !subItem.enabled
|
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
|
||||||
subStorage?.encode(subId, Gson().toJson(subItem))
|
if (!it.isPressed) return@setOnCheckedChangeListener
|
||||||
notifyItemChanged(position)
|
subItem.enabled = isChecked
|
||||||
|
MmkvManager.encodeSubscription(subId, subItem)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(subItem.url)) {
|
if (TextUtils.isEmpty(subItem.url)) {
|
||||||
@@ -99,5 +96,28 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) :
|
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) :
|
||||||
RecyclerView.ViewHolder(itemSubSettingBinding.root)
|
BaseViewHolder(itemSubSettingBinding.root), ItemTouchHelperViewHolder
|
||||||
|
|
||||||
|
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
fun onItemSelected() {
|
||||||
|
itemView.setBackgroundColor(Color.LTGRAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onItemClear() {
|
||||||
|
itemView.setBackgroundColor(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
|
SettingsManager.swapSubscriptions(fromPosition, toPosition)
|
||||||
|
notifyItemMoved(fromPosition, toPosition)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemMoveCompleted() {
|
||||||
|
mActivity.refreshData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemDismiss(position: Int) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
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.ActivityTaskerBinding
|
import com.v2ray.ang.databinding.ActivityTaskerBinding
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
|
||||||
class TaskerActivity : BaseActivity() {
|
class TaskerActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) }
|
||||||
@@ -21,8 +20,6 @@ class TaskerActivity : BaseActivity() {
|
|||||||
private var lstData: ArrayList<String> = ArrayList()
|
private var lstData: ArrayList<String> = ArrayList()
|
||||||
private var lstGuid: ArrayList<String> = ArrayList()
|
private var lstGuid: ArrayList<String> = ArrayList()
|
||||||
|
|
||||||
private val serverStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
@@ -31,7 +28,7 @@ class TaskerActivity : BaseActivity() {
|
|||||||
lstData.add("Default")
|
lstData.add("Default")
|
||||||
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
|
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
|
||||||
|
|
||||||
serverStorage?.allKeys()?.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)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import android.util.Log
|
|||||||
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.util.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
class UrlSchemeActivity : BaseActivity() {
|
class UrlSchemeActivity : BaseActivity() {
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ 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.google.gson.Gson
|
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
import com.tbruyelle.rxpermissions3.RxPermissions
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
|
||||||
@@ -30,10 +29,12 @@ import com.v2ray.ang.databinding.LayoutProgressBinding
|
|||||||
import com.v2ray.ang.dto.AssetUrlItem
|
import com.v2ray.ang.dto.AssetUrlItem
|
||||||
import com.v2ray.ang.extension.toTrafficString
|
import com.v2ray.ang.extension.toTrafficString
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
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
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
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
|
||||||
@@ -45,8 +46,6 @@ import java.util.Date
|
|||||||
|
|
||||||
class UserAssetActivity : BaseActivity() {
|
class UserAssetActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
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")
|
||||||
@@ -72,23 +71,11 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
return super.onCreateOptionsMenu(menu)
|
return super.onCreateOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
// Use when to streamline the option selection
|
||||||
R.id.add_file -> {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
showFileChooser()
|
R.id.add_file -> showFileChooser().let { true }
|
||||||
true
|
R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
|
||||||
}
|
R.id.download_file -> downloadGeoFiles().let { true }
|
||||||
|
|
||||||
R.id.add_url -> {
|
|
||||||
val intent = Intent(this, UserAssetUrlActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.download_file -> {
|
|
||||||
downloadGeoFiles()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,31 +108,29 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val chooseFile =
|
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it ->
|
val uri = result.data?.data
|
||||||
val uri = it.data?.data
|
if (result.resultCode == RESULT_OK && uri != null) {
|
||||||
if (it.resultCode == RESULT_OK && uri != null) {
|
val assetId = Utils.getUuid()
|
||||||
val assetId = Utils.getUuid()
|
runCatching {
|
||||||
try {
|
val assetItem = AssetUrlItem(
|
||||||
val assetItem = AssetUrlItem(
|
getCursorName(uri) ?: uri.toString(),
|
||||||
getCursorName(uri) ?: uri.toString(),
|
"file"
|
||||||
"file"
|
)
|
||||||
)
|
|
||||||
|
|
||||||
// check remarks unique
|
val assetList = MmkvManager.decodeAssetUrls()
|
||||||
val assetList = MmkvManager.decodeAssetUrls()
|
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
|
||||||
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
|
toast(R.string.msg_remark_is_duplicate)
|
||||||
toast(R.string.msg_remark_is_duplicate)
|
} else {
|
||||||
return@registerForActivityResult
|
MmkvManager.encodeAsset(assetId, assetItem)
|
||||||
}
|
|
||||||
assetStorage?.encode(assetId, Gson().toJson(assetItem))
|
|
||||||
copyFile(uri)
|
copyFile(uri)
|
||||||
} catch (e: Exception) {
|
|
||||||
toast(R.string.toast_asset_copy_failed)
|
|
||||||
MmkvManager.removeAssetUrl(assetId)
|
|
||||||
}
|
}
|
||||||
|
}.onFailure {
|
||||||
|
toast(R.string.toast_asset_copy_failed)
|
||||||
|
MmkvManager.removeAssetUrl(assetId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun copyFile(uri: Uri): String {
|
private fun copyFile(uri: Uri): String {
|
||||||
val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
|
val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
|
||||||
@@ -178,7 +163,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
.show()
|
.show()
|
||||||
toast(R.string.msg_downloading_content)
|
toast(R.string.msg_downloading_content)
|
||||||
|
|
||||||
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
var assets = MmkvManager.decodeAssetUrls()
|
var assets = MmkvManager.decodeAssetUrls()
|
||||||
assets = addBuiltInGeoItems(assets)
|
assets = addBuiltInGeoItems(assets)
|
||||||
|
|
||||||
@@ -215,7 +200,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
URL(item.url).openConnection(
|
URL(item.url).openConnection(
|
||||||
Proxy(
|
Proxy(
|
||||||
Proxy.Type.HTTP,
|
Proxy.Type.HTTP,
|
||||||
InetSocketAddress("127.0.0.1", httpPort)
|
InetSocketAddress(LOOPBACK, httpPort)
|
||||||
)
|
)
|
||||||
) as HttpURLConnection
|
) as HttpURLConnection
|
||||||
}
|
}
|
||||||
@@ -255,6 +240,15 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
return list + assets
|
return list + assets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun initAssets() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
SettingsManager.initAssets(this@UserAssetActivity, assets)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
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(
|
||||||
@@ -286,10 +280,10 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
|
|
||||||
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
|
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
|
||||||
holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
|
||||||
} else {
|
} else {
|
||||||
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
|
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
|
||||||
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
|
//holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
|
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
|
||||||
@@ -298,9 +292,16 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
|
||||||
file?.delete()
|
AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm)
|
||||||
MmkvManager.removeAssetUrl(item.first)
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
binding.recyclerView.adapter?.notifyItemRemoved(position)
|
file?.delete()
|
||||||
|
MmkvManager.removeAssetUrl(item.first)
|
||||||
|
initAssets()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import android.text.TextUtils
|
|||||||
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.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
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.util.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
|
||||||
|
|
||||||
@@ -22,7 +20,6 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
var save_config: MenuItem? = null
|
var save_config: MenuItem? = null
|
||||||
|
|
||||||
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
val extDir by lazy { File(Utils.userAssetPath(this)) }
|
||||||
private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
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?) {
|
||||||
@@ -30,9 +27,9 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
title = getString(R.string.title_user_asset_add_url)
|
title = getString(R.string.title_user_asset_add_url)
|
||||||
|
|
||||||
val json = assetStorage?.decodeString(editAssetId)
|
val assetItem = MmkvManager.decodeAsset(editAssetId)
|
||||||
if (!json.isNullOrBlank()) {
|
if (assetItem != null) {
|
||||||
bindingAsset(Gson().fromJson(json, AssetUrlItem::class.java))
|
bindingAsset(assetItem)
|
||||||
} else {
|
} else {
|
||||||
clearAsset()
|
clearAsset()
|
||||||
}
|
}
|
||||||
@@ -60,12 +57,9 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
* save asset config
|
* save asset config
|
||||||
*/
|
*/
|
||||||
private fun saveServer(): Boolean {
|
private fun saveServer(): Boolean {
|
||||||
val assetItem: AssetUrlItem
|
var assetItem = MmkvManager.decodeAsset(editAssetId)
|
||||||
val json = assetStorage?.decodeString(editAssetId)
|
|
||||||
var assetId = editAssetId
|
var assetId = editAssetId
|
||||||
if (!json.isNullOrBlank()) {
|
if (assetItem != null) {
|
||||||
assetItem = Gson().fromJson(json, AssetUrlItem::class.java)
|
|
||||||
|
|
||||||
// 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()) {
|
||||||
@@ -96,7 +90,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
assetStorage?.encode(assetId, Gson().toJson(assetItem))
|
MmkvManager.encodeAsset(assetId, assetItem)
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -1,599 +0,0 @@
|
|||||||
package com.v2ray.ang.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.dto.*
|
|
||||||
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
|
|
||||||
import com.v2ray.ang.util.fmt.ShadowsocksFmt
|
|
||||||
import com.v2ray.ang.util.fmt.SocksFmt
|
|
||||||
import com.v2ray.ang.util.fmt.TrojanFmt
|
|
||||||
import com.v2ray.ang.util.fmt.VlessFmt
|
|
||||||
import com.v2ray.ang.util.fmt.VmessFmt
|
|
||||||
import com.v2ray.ang.util.fmt.WireguardFmt
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
object AngConfigManager {
|
|
||||||
private val mainStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_MAIN,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private val serverRawStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SERVER_RAW,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy loading config
|
|
||||||
*/
|
|
||||||
// fun migrateLegacyConfig(c: Context): Boolean? {
|
|
||||||
// try {
|
|
||||||
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(c)
|
|
||||||
// val context = defaultSharedPreferences.getString(ANG_CONFIG, "")
|
|
||||||
// if (context.isNullOrBlank()) {
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// val angConfig = Gson().fromJson(context, AngConfig::class.java)
|
|
||||||
// for (i in angConfig.vmess.indices) {
|
|
||||||
// upgradeServerVersion(angConfig.vmess[i])
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// copyLegacySettings(defaultSharedPreferences)
|
|
||||||
// migrateVmessBean(angConfig, defaultSharedPreferences)
|
|
||||||
// migrateSubItemBean(angConfig)
|
|
||||||
//
|
|
||||||
// defaultSharedPreferences.edit().remove(ANG_CONFIG).apply()
|
|
||||||
// return true
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// }
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun copyLegacySettings(sharedPreferences: SharedPreferences) {
|
|
||||||
// listOf(
|
|
||||||
// AppConfig.PREF_MODE,
|
|
||||||
// AppConfig.PREF_REMOTE_DNS,
|
|
||||||
// AppConfig.PREF_DOMESTIC_DNS,
|
|
||||||
// AppConfig.PREF_LOCAL_DNS_PORT,
|
|
||||||
// AppConfig.PREF_SOCKS_PORT,
|
|
||||||
// AppConfig.PREF_HTTP_PORT,
|
|
||||||
// AppConfig.PREF_LOGLEVEL,
|
|
||||||
// AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
|
||||||
// AppConfig.PREF_ROUTING_MODE,
|
|
||||||
// AppConfig.PREF_V2RAY_ROUTING_AGENT,
|
|
||||||
// AppConfig.PREF_V2RAY_ROUTING_BLOCKED,
|
|
||||||
// AppConfig.PREF_V2RAY_ROUTING_DIRECT,
|
|
||||||
// ).forEach { key ->
|
|
||||||
// settingsStorage?.encode(key, sharedPreferences.getString(key, null))
|
|
||||||
// }
|
|
||||||
// listOf(
|
|
||||||
// AppConfig.PREF_SPEED_ENABLED,
|
|
||||||
// AppConfig.PREF_PROXY_SHARING,
|
|
||||||
// AppConfig.PREF_LOCAL_DNS_ENABLED,
|
|
||||||
// AppConfig.PREF_ALLOW_INSECURE,
|
|
||||||
// AppConfig.PREF_PREFER_IPV6,
|
|
||||||
// AppConfig.PREF_PER_APP_PROXY,
|
|
||||||
// AppConfig.PREF_BYPASS_APPS,
|
|
||||||
// ).forEach { key ->
|
|
||||||
// settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
|
|
||||||
// }
|
|
||||||
// settingsStorage?.encode(
|
|
||||||
// AppConfig.PREF_SNIFFING_ENABLED,
|
|
||||||
// sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true)
|
|
||||||
// )
|
|
||||||
// settingsStorage?.encode(
|
|
||||||
// AppConfig.PREF_PER_APP_PROXY_SET,
|
|
||||||
// sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf())
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun migrateVmessBean(angConfig: AngConfig, sharedPreferences: SharedPreferences) {
|
|
||||||
// angConfig.vmess.forEachIndexed { index, vmessBean ->
|
|
||||||
// val type = EConfigType.fromInt(vmessBean.configType) ?: return@forEachIndexed
|
|
||||||
// val config = ServerConfig.create(type)
|
|
||||||
// config.remarks = vmessBean.remarks
|
|
||||||
// config.subscriptionId = vmessBean.subid
|
|
||||||
// if (type == EConfigType.CUSTOM) {
|
|
||||||
// val jsonConfig = sharedPreferences.getString(ANG_CONFIG + vmessBean.guid, "")
|
|
||||||
// val v2rayConfig = try {
|
|
||||||
// Gson().fromJson(jsonConfig, V2rayConfig::class.java)
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// return@forEachIndexed
|
|
||||||
// }
|
|
||||||
// config.fullConfig = v2rayConfig
|
|
||||||
// serverRawStorage?.encode(vmessBean.guid, jsonConfig)
|
|
||||||
// } else {
|
|
||||||
// config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
|
||||||
// vnext.address = vmessBean.address
|
|
||||||
// vnext.port = vmessBean.port
|
|
||||||
// vnext.users[0].id = vmessBean.id
|
|
||||||
// if (config.configType == EConfigType.VMESS) {
|
|
||||||
// vnext.users[0].alterId = vmessBean.alterId
|
|
||||||
// vnext.users[0].security = vmessBean.security
|
|
||||||
// } else if (config.configType == EConfigType.VLESS) {
|
|
||||||
// vnext.users[0].encryption = vmessBean.security
|
|
||||||
// vnext.users[0].flow = vmessBean.flow
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
// server.address = vmessBean.address
|
|
||||||
// server.port = vmessBean.port
|
|
||||||
// if (config.configType == EConfigType.SHADOWSOCKS) {
|
|
||||||
// server.password = vmessBean.id
|
|
||||||
// server.method = vmessBean.security
|
|
||||||
// } else if (config.configType == EConfigType.SOCKS) {
|
|
||||||
// if (TextUtils.isEmpty(vmessBean.security) && TextUtils.isEmpty(vmessBean.id)) {
|
|
||||||
// server.users = null
|
|
||||||
// } else {
|
|
||||||
// val socksUsersBean =
|
|
||||||
// V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
|
||||||
// socksUsersBean.user = vmessBean.security
|
|
||||||
// socksUsersBean.pass = vmessBean.id
|
|
||||||
// server.users = listOf(socksUsersBean)
|
|
||||||
// }
|
|
||||||
// } else if (config.configType == EConfigType.TROJAN) {
|
|
||||||
// server.password = vmessBean.id
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// config.outboundBean?.streamSettings?.let { streamSetting ->
|
|
||||||
// val sni = streamSetting.populateTransportSettings(
|
|
||||||
// vmessBean.network,
|
|
||||||
// vmessBean.headerType,
|
|
||||||
// vmessBean.requestHost,
|
|
||||||
// vmessBean.path,
|
|
||||||
// vmessBean.path,
|
|
||||||
// vmessBean.requestHost,
|
|
||||||
// vmessBean.path,
|
|
||||||
// vmessBean.headerType,
|
|
||||||
// vmessBean.path,
|
|
||||||
// vmessBean.requestHost,
|
|
||||||
// )
|
|
||||||
// val allowInsecure = if (vmessBean.allowInsecure.isBlank()) {
|
|
||||||
// settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
|
||||||
// } else {
|
|
||||||
// vmessBean.allowInsecure.toBoolean()
|
|
||||||
// }
|
|
||||||
// var fingerprint = streamSetting.tlsSettings?.fingerprint
|
|
||||||
// streamSetting.populateTlsSettings(
|
|
||||||
// vmessBean.streamSecurity, allowInsecure,
|
|
||||||
// vmessBean.sni.ifBlank { sni }, fingerprint, null, null, null, null
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// val key = MmkvManager.encodeServerConfig(vmessBean.guid, config)
|
|
||||||
// if (index == angConfig.index) {
|
|
||||||
// mainStorage?.encode(KEY_SELECTED_SERVER, key)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun migrateSubItemBean(angConfig: AngConfig) {
|
|
||||||
// angConfig.subItem.forEach {
|
|
||||||
// val subItem = SubscriptionItem()
|
|
||||||
// subItem.remarks = it.remarks
|
|
||||||
// subItem.url = it.url
|
|
||||||
// subItem.enabled = it.enabled
|
|
||||||
// subStorage?.encode(it.id, Gson().toJson(subItem))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* parse config form qrcode or...
|
|
||||||
*/
|
|
||||||
private fun parseConfig(
|
|
||||||
str: String?,
|
|
||||||
subid: String,
|
|
||||||
removedSelectedServer: ServerConfig?
|
|
||||||
): Int {
|
|
||||||
try {
|
|
||||||
if (str == null || TextUtils.isEmpty(str)) {
|
|
||||||
return R.string.toast_none_data
|
|
||||||
}
|
|
||||||
|
|
||||||
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
|
||||||
VmessFmt.parseVmess(str)
|
|
||||||
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
|
|
||||||
ShadowsocksFmt.parseShadowsocks(str)
|
|
||||||
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
|
|
||||||
SocksFmt.parseSocks(str)
|
|
||||||
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
|
|
||||||
TrojanFmt.parseTrojan(str)
|
|
||||||
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
|
|
||||||
VlessFmt.parseVless(str)
|
|
||||||
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
|
|
||||||
WireguardFmt.parseWireguard(str)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config == null) {
|
|
||||||
return R.string.toast_incorrect_protocol
|
|
||||||
}
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val guid = MmkvManager.encodeServerConfig("", config)
|
|
||||||
if (removedSelectedServer != null &&
|
|
||||||
config.getProxyOutbound()
|
|
||||||
?.getServerAddress() == removedSelectedServer.getProxyOutbound()
|
|
||||||
?.getServerAddress() &&
|
|
||||||
config.getProxyOutbound()
|
|
||||||
?.getServerPort() == removedSelectedServer.getProxyOutbound()
|
|
||||||
?.getServerPort()
|
|
||||||
) {
|
|
||||||
mainStorage?.encode(KEY_SELECTED_SERVER, guid)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* share config
|
|
||||||
*/
|
|
||||||
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.VLESS -> VlessFmt.toUri(config)
|
|
||||||
EConfigType.TROJAN -> TrojanFmt.toUri(config)
|
|
||||||
EConfigType.WIREGUARD -> WireguardFmt.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())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = V2rayConfigUtil.getV2rayConfig(context, guid)
|
|
||||||
if (result.status) {
|
|
||||||
Utils.setClipboard(context, result.content)
|
|
||||||
} else {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * upgrade
|
|
||||||
// */
|
|
||||||
// private fun upgradeServerVersion(vmess: AngConfig.VmessBean): Int {
|
|
||||||
// try {
|
|
||||||
// if (vmess.configVersion == 2) {
|
|
||||||
// return 0
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// when (vmess.network) {
|
|
||||||
// "ws", "h2" -> {
|
|
||||||
// var path = ""
|
|
||||||
// var host = ""
|
|
||||||
// val lstParameter = vmess.requestHost.split(";")
|
|
||||||
// if (lstParameter.isNotEmpty()) {
|
|
||||||
// path = lstParameter[0].trim()
|
|
||||||
// }
|
|
||||||
// if (lstParameter.size > 1) {
|
|
||||||
// path = lstParameter[0].trim()
|
|
||||||
// host = lstParameter[1].trim()
|
|
||||||
// }
|
|
||||||
// vmess.path = path
|
|
||||||
// vmess.requestHost = host
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// vmess.configVersion = 2
|
|
||||||
// return 0
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// return -1
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
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()
|
|
||||||
.forEach { str ->
|
|
||||||
if (str.startsWith(AppConfig.PROTOCOL_HTTP) || str.startsWith(AppConfig.PROTOCOL_HTTPS)) {
|
|
||||||
count += MmkvManager.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(
|
|
||||||
mainStorage?.decodeString(KEY_SELECTED_SERVER).orEmpty()
|
|
||||||
)?.let {
|
|
||||||
if (it.subscriptionId == subid) {
|
|
||||||
return@let it
|
|
||||||
}
|
|
||||||
return@let null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = 0
|
|
||||||
servers.lines()
|
|
||||||
.reversed()
|
|
||||||
.forEach {
|
|
||||||
val resId = parseConfig(it, subid, 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 gson = GsonBuilder().setPrettyPrinting().create()
|
|
||||||
val gson = GsonBuilder()
|
|
||||||
.setPrettyPrinting()
|
|
||||||
.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()
|
|
||||||
val serverList: Array<Any> =
|
|
||||||
Gson().fromJson(server, Array<Any>::class.java)
|
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
|
||||||
var count = 0
|
|
||||||
for (srv in serverList.reversed()) {
|
|
||||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
|
||||||
config.fullConfig =
|
|
||||||
Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
|
|
||||||
config.remarks = config.fullConfig?.remarks
|
|
||||||
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
|
|
||||||
.toString())
|
|
||||||
config.subscriptionId = subid
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
serverRawStorage?.encode(key, gson.toJson(srv))
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
// For compatibility
|
|
||||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
|
||||||
config.subscriptionId = subid
|
|
||||||
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
|
|
||||||
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
serverRawStorage?.encode(key, server)
|
|
||||||
return 1
|
|
||||||
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
|
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server)
|
|
||||||
?: return R.string.toast_incorrect_protocol
|
|
||||||
config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
serverRawStorage?.encode(key, server)
|
|
||||||
return 1
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConfigViaSubAll(): Int {
|
|
||||||
var count = 0
|
|
||||||
try {
|
|
||||||
MmkvManager.decodeSubscriptions().forEach {
|
|
||||||
count += updateConfigViaSub(it)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
|
|
||||||
try {
|
|
||||||
if (TextUtils.isEmpty(it.first)
|
|
||||||
|| TextUtils.isEmpty(it.second.remarks)
|
|
||||||
|| TextUtils.isEmpty(it.second.url)
|
|
||||||
) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if (!it.second.enabled) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
val url = Utils.idnToASCII(it.second.url)
|
|
||||||
if (!Utils.isValidUrl(url)) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, url)
|
|
||||||
var configText = try {
|
|
||||||
Utils.getUrlContentWithCustomUserAgent(url)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
""
|
|
||||||
}
|
|
||||||
if (configText.isEmpty()) {
|
|
||||||
configText = try {
|
|
||||||
val httpPort = Utils.parseInt(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
|
|
||||||
AppConfig.PORT_HTTP.toInt()
|
|
||||||
)
|
|
||||||
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (configText.isEmpty()) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return parseConfigViaSub(configText, it.first, false)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
|
|
||||||
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseBatchConfig(server, subid, append)
|
|
||||||
}
|
|
||||||
if (count <= 0) {
|
|
||||||
count = parseCustomConfigServer(server, subid)
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,19 @@
|
|||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
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 io.reactivex.rxjava3.core.Observable
|
||||||
|
|
||||||
object AppManagerUtil {
|
object AppManagerUtil {
|
||||||
fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
|
private fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
|
||||||
val packageManager = ctx.packageManager
|
val packageManager = ctx.packageManager
|
||||||
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
|
||||||
val apps = ArrayList<AppInfo>()
|
val apps = ArrayList<AppInfo>()
|
||||||
|
|
||||||
for (pkg in packages) {
|
for (pkg in packages) {
|
||||||
if (!pkg.hasInternetPermission && pkg.packageName != "android") continue
|
//if (!pkg.hasInternetPermission && pkg.packageName != "android") continue
|
||||||
|
|
||||||
val applicationInfo = pkg.applicationInfo
|
val applicationInfo = pkg.applicationInfo
|
||||||
|
|
||||||
@@ -35,9 +33,9 @@ object AppManagerUtil {
|
|||||||
it.onNext(loadNetworkAppList(ctx))
|
it.onNext(loadNetworkAppList(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
val PackageInfo.hasInternetPermission: Boolean
|
// val PackageInfo.hasInternetPermission: Boolean
|
||||||
get() {
|
// get() {
|
||||||
val permissions = requestedPermissions
|
// val permissions = requestedPermissions
|
||||||
return permissions?.any { it == Manifest.permission.INTERNET } ?: false
|
// return permissions?.any { it == Manifest.permission.INTERNET } ?: false
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
37
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/JsonUtil.kt
Normal file
37
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/JsonUtil.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonPrimitive
|
||||||
|
import com.google.gson.JsonSerializationContext
|
||||||
|
import com.google.gson.JsonSerializer
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
object JsonUtil {
|
||||||
|
private var gson = Gson()
|
||||||
|
|
||||||
|
fun toJson(src: Any?): String {
|
||||||
|
return gson.toJson(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> fromJson(json: String, cls: Class<T>): T {
|
||||||
|
return gson.fromJson(json, cls)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJsonPretty(src: Any?): String {
|
||||||
|
val gsonPre = GsonBuilder()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.disableHtmlEscaping()
|
||||||
|
.registerTypeAdapter( // custom serializer 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()
|
||||||
|
return gsonPre.toJson(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
package com.v2ray.ang.util
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.dto.AssetUrlItem
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
|
||||||
import com.v2ray.ang.dto.ServerAffiliationInfo
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object MmkvManager {
|
|
||||||
const val ID_MAIN = "MAIN"
|
|
||||||
const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
|
||||||
const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
|
|
||||||
const val ID_SERVER_RAW = "SERVER_RAW"
|
|
||||||
const val ID_SERVER_AFF = "SERVER_AFF"
|
|
||||||
const val ID_SUB = "SUB"
|
|
||||||
const val ID_ASSET = "ASSET"
|
|
||||||
const val ID_SETTING = "SETTING"
|
|
||||||
const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
|
|
||||||
const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
|
|
||||||
|
|
||||||
val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
fun decodeServerList(): MutableList<String> {
|
|
||||||
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
|
|
||||||
return if (json.isNullOrBlank()) {
|
|
||||||
mutableListOf()
|
|
||||||
} else {
|
|
||||||
Gson().fromJson(json, Array<String>::class.java).toMutableList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decodeServerConfig(guid: String): ServerConfig? {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val json = serverStorage?.decodeString(guid)
|
|
||||||
if (json.isNullOrBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return Gson().fromJson(json, ServerConfig::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decodeProfileConfig(guid: String): ProfileItem? {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val json = profileStorage?.decodeString(guid)
|
|
||||||
if (json.isNullOrBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return Gson().fromJson(json, ProfileItem::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun encodeServerConfig(guid: String, config: ServerConfig): String {
|
|
||||||
val key = guid.ifBlank { Utils.getUuid() }
|
|
||||||
serverStorage?.encode(key, Gson().toJson(config))
|
|
||||||
val serverList = decodeServerList()
|
|
||||||
if (!serverList.contains(key)) {
|
|
||||||
serverList.add(0, key)
|
|
||||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
|
||||||
if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) {
|
|
||||||
mainStorage?.encode(KEY_SELECTED_SERVER, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val profile = ProfileItem(
|
|
||||||
configType = config.configType,
|
|
||||||
subscriptionId = config.subscriptionId,
|
|
||||||
remarks = config.remarks,
|
|
||||||
server = config.getProxyOutbound()?.getServerAddress(),
|
|
||||||
serverPort = config.getProxyOutbound()?.getServerPort(),
|
|
||||||
)
|
|
||||||
profileStorage?.encode(key, Gson().toJson(profile))
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeServer(guid: String) {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (mainStorage?.decodeString(KEY_SELECTED_SERVER) == guid) {
|
|
||||||
mainStorage?.remove(KEY_SELECTED_SERVER)
|
|
||||||
}
|
|
||||||
val serverList = decodeServerList()
|
|
||||||
serverList.remove(guid)
|
|
||||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
|
||||||
serverStorage?.remove(guid)
|
|
||||||
profileStorage?.remove(guid)
|
|
||||||
serverAffStorage?.remove(guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeServerViaSubid(subid: String) {
|
|
||||||
if (subid.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serverStorage?.allKeys()?.forEach { key ->
|
|
||||||
decodeServerConfig(key)?.let { config ->
|
|
||||||
if (config.subscriptionId == subid) {
|
|
||||||
removeServer(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val json = serverAffStorage?.decodeString(guid)
|
|
||||||
if (json.isNullOrBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return Gson().fromJson(json, ServerAffiliationInfo::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
|
|
||||||
if (guid.isBlank()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
|
|
||||||
aff.testDelayMillis = testResult
|
|
||||||
serverAffStorage?.encode(guid, Gson().toJson(aff))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearAllTestDelayResults(keys: List<String>?) {
|
|
||||||
keys?.forEach { key ->
|
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
|
||||||
aff.testDelayMillis = 0
|
|
||||||
serverAffStorage?.encode(key, Gson().toJson(aff))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importUrlAsSubscription(url: String): Int {
|
|
||||||
val subscriptions = decodeSubscriptions()
|
|
||||||
subscriptions.forEach {
|
|
||||||
if (it.second.url == url) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(url))
|
|
||||||
val subItem = SubscriptionItem()
|
|
||||||
subItem.remarks = uri.fragment ?: "import sub"
|
|
||||||
subItem.url = url
|
|
||||||
subStorage?.encode(Utils.getUuid(), Gson().toJson(subItem))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
|
|
||||||
val subscriptions = mutableListOf<Pair<String, SubscriptionItem>>()
|
|
||||||
subStorage?.allKeys()?.forEach { key ->
|
|
||||||
val json = subStorage?.decodeString(key)
|
|
||||||
if (!json.isNullOrBlank()) {
|
|
||||||
subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return subscriptions.sortedBy { (_, value) -> value.addedTime }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeSubscription(subid: String) {
|
|
||||||
subStorage?.remove(subid)
|
|
||||||
removeServerViaSubid(subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
|
|
||||||
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
|
|
||||||
assetStorage?.allKeys()?.forEach { key ->
|
|
||||||
val json = assetStorage?.decodeString(key)
|
|
||||||
if (!json.isNullOrBlank()) {
|
|
||||||
assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAssetUrl(assetid: String) {
|
|
||||||
assetStorage?.remove(assetid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllServer() {
|
|
||||||
mainStorage?.clearAll()
|
|
||||||
serverStorage?.clearAll()
|
|
||||||
profileStorage?.clearAll()
|
|
||||||
serverAffStorage?.clearAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeInvalidServer(guid: String) {
|
|
||||||
if (guid.isNotEmpty()) {
|
|
||||||
decodeServerAffiliationInfo(guid)?.let { aff ->
|
|
||||||
if (aff.testDelayMillis < 0L) {
|
|
||||||
removeServer(guid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
serverAffStorage?.allKeys()?.forEach { key ->
|
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
|
||||||
if (aff.testDelayMillis < 0L) {
|
|
||||||
removeServer(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sortByTestResults() {
|
|
||||||
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
|
||||||
|
|
||||||
val serverDelays = mutableListOf<ServerDelay>()
|
|
||||||
val serverList = decodeServerList()
|
|
||||||
serverList.forEach { key ->
|
|
||||||
val delay = decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
|
||||||
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
|
|
||||||
}
|
|
||||||
serverDelays.sortBy { it.testDelayMillis }
|
|
||||||
|
|
||||||
serverDelays.forEach {
|
|
||||||
serverList.remove(it.guid)
|
|
||||||
serverList.add(it.guid)
|
|
||||||
}
|
|
||||||
|
|
||||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
98
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/PluginUtil.kt
Normal file
98
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/PluginUtil.kt
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.util.Log
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.fmt.Hysteria2Fmt
|
||||||
|
import com.v2ray.ang.service.ProcessService
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object PluginUtil {
|
||||||
|
//private const val HYSTERIA2 = "hysteria2-plugin"
|
||||||
|
private const val HYSTERIA2 = "libhysteria2.so"
|
||||||
|
private const val TAG = ANG_PACKAGE
|
||||||
|
private val procService: ProcessService by lazy {
|
||||||
|
ProcessService()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun initPlugin(name: String): PluginManager.InitResult {
|
||||||
|
// return PluginManager.init(name)!!
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
|
||||||
|
Log.d(TAG, "runPlugin")
|
||||||
|
|
||||||
|
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
||||||
|
val configFile = genConfigHy2(context, config, domainPort) ?: return
|
||||||
|
val cmd = genCmdHy2(context, configFile)
|
||||||
|
|
||||||
|
procService.runProcess(context, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopPlugin() {
|
||||||
|
stopHy2()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun realPingHy2(context: Context, config: ProfileItem?): Long {
|
||||||
|
Log.d(TAG, "realPingHy2")
|
||||||
|
val retFailure = -1L
|
||||||
|
|
||||||
|
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
|
||||||
|
val socksPort = Utils.findFreePort(listOf(0))
|
||||||
|
val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure
|
||||||
|
val cmd = genCmdHy2(context, configFile)
|
||||||
|
|
||||||
|
val proc = ProcessService()
|
||||||
|
proc.runProcess(context, cmd)
|
||||||
|
Thread.sleep(1000L)
|
||||||
|
val delay = SpeedtestUtil.testConnection(context, socksPort)
|
||||||
|
proc.stopProcess()
|
||||||
|
|
||||||
|
return delay.first
|
||||||
|
}
|
||||||
|
return retFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
|
||||||
|
Log.d(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 configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
|
||||||
|
Log.d(TAG, "runPlugin ${configFile.absolutePath}")
|
||||||
|
|
||||||
|
configFile.parentFile?.mkdirs()
|
||||||
|
configFile.writeText(JsonUtil.toJson(hy2Config))
|
||||||
|
Log.d(TAG, JsonUtil.toJson(hy2Config))
|
||||||
|
|
||||||
|
return configFile
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
|
||||||
|
return mutableListOf(
|
||||||
|
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
|
||||||
|
//initPlugin(HYSTERIA2).path,
|
||||||
|
"--disable-update-check",
|
||||||
|
"--config",
|
||||||
|
configFile.absolutePath,
|
||||||
|
"--log-level",
|
||||||
|
"warn",
|
||||||
|
"client"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopHy2() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "$HYSTERIA2 destroy")
|
||||||
|
procService?.stopProcess()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,36 +23,19 @@ object QRCodeDecoder {
|
|||||||
* create qrcode using zxing
|
* create qrcode using zxing
|
||||||
*/
|
*/
|
||||||
fun createQRCode(text: String, size: Int = 800): Bitmap? {
|
fun createQRCode(text: String, size: Int = 800): Bitmap? {
|
||||||
try {
|
return runCatching {
|
||||||
val hints = HashMap<EncodeHintType, String>()
|
val hints = mapOf(EncodeHintType.CHARACTER_SET to Charsets.UTF_8)
|
||||||
hints[EncodeHintType.CHARACTER_SET] = "utf-8"
|
val bitMatrix = QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size, hints)
|
||||||
val bitMatrix = QRCodeWriter().encode(
|
val pixels = IntArray(size * size) { i ->
|
||||||
text,
|
if (bitMatrix.get(i % size, i / size)) 0xff000000.toInt() else 0xffffffff.toInt()
|
||||||
BarcodeFormat.QR_CODE, size, size, hints
|
|
||||||
)
|
|
||||||
val pixels = IntArray(size * size)
|
|
||||||
for (y in 0 until size) {
|
|
||||||
for (x in 0 until size) {
|
|
||||||
if (bitMatrix.get(x, y)) {
|
|
||||||
pixels[y * size + x] = 0xff000000.toInt()
|
|
||||||
} else {
|
|
||||||
pixels[y * size + x] = 0xffffffff.toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val bitmap = Bitmap.createBitmap(
|
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
|
||||||
size, size,
|
setPixels(pixels, 0, size, 0, 0, size, size)
|
||||||
Bitmap.Config.ARGB_8888
|
}
|
||||||
)
|
}.getOrNull()
|
||||||
bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
|
|
||||||
return bitmap
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
|
||||||
*
|
*
|
||||||
@@ -70,40 +53,24 @@ object QRCodeDecoder {
|
|||||||
* @return 返回二维码图片里的内容 或 null
|
* @return 返回二维码图片里的内容 或 null
|
||||||
*/
|
*/
|
||||||
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
fun syncDecodeQRCode(bitmap: Bitmap?): String? {
|
||||||
if (bitmap == null) {
|
return bitmap?.let {
|
||||||
return null
|
runCatching {
|
||||||
}
|
val pixels = IntArray(it.width * it.height).also { array ->
|
||||||
var source: RGBLuminanceSource? = null
|
it.getPixels(array, 0, it.width, 0, 0, it.width, it.height)
|
||||||
try {
|
|
||||||
val width = bitmap.width
|
|
||||||
val height = bitmap.height
|
|
||||||
val pixels = IntArray(width * height)
|
|
||||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
|
|
||||||
source = RGBLuminanceSource(width, height, pixels)
|
|
||||||
val qrReader = QRCodeReader()
|
|
||||||
try {
|
|
||||||
val result = try {
|
|
||||||
qrReader.decode(
|
|
||||||
BinaryBitmap(GlobalHistogramBinarizer(source)),
|
|
||||||
mapOf(DecodeHintType.TRY_HARDER to true)
|
|
||||||
)
|
|
||||||
} catch (e: NotFoundException) {
|
|
||||||
qrReader.decode(
|
|
||||||
BinaryBitmap(GlobalHistogramBinarizer(source.invert())),
|
|
||||||
mapOf(DecodeHintType.TRY_HARDER to true)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return result.text
|
val source = RGBLuminanceSource(it.width, it.height, pixels)
|
||||||
} catch (e: Exception) {
|
val qrReader = QRCodeReader()
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
try {
|
||||||
|
qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)), mapOf(DecodeHintType.TRY_HARDER to true)).text
|
||||||
|
} catch (e: NotFoundException) {
|
||||||
|
qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source.invert())), mapOf(DecodeHintType.TRY_HARDER to true)).text
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
|
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
|
||||||
*
|
*
|
||||||
@@ -149,6 +116,6 @@ object QRCodeDecoder {
|
|||||||
)
|
)
|
||||||
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
|
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
|
||||||
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
|
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
|
||||||
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8"
|
HINTS[DecodeHintType.CHARACTER_SET] = Charsets.UTF_8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.os.SystemClock
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.extension.responseLength
|
import com.v2ray.ang.extension.responseLength
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
@@ -97,9 +98,9 @@ object SpeedtestUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testConnection(context: Context, port: Int): String {
|
fun testConnection(context: Context, port: Int): Pair<Long, String> {
|
||||||
// return V2RayVpnService.measureV2rayDelay()
|
|
||||||
var result: String
|
var result: String
|
||||||
|
var elapsed = -1L
|
||||||
var conn: HttpURLConnection? = null
|
var conn: HttpURLConnection? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -108,7 +109,7 @@ object SpeedtestUtil {
|
|||||||
conn = url.openConnection(
|
conn = url.openConnection(
|
||||||
Proxy(
|
Proxy(
|
||||||
Proxy.Type.HTTP,
|
Proxy.Type.HTTP,
|
||||||
InetSocketAddress("127.0.0.1", port)
|
InetSocketAddress(LOOPBACK, port)
|
||||||
)
|
)
|
||||||
) as HttpURLConnection
|
) as HttpURLConnection
|
||||||
conn.connectTimeout = 30000
|
conn.connectTimeout = 30000
|
||||||
@@ -119,7 +120,7 @@ object SpeedtestUtil {
|
|||||||
|
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
val code = conn.responseCode
|
val code = conn.responseCode
|
||||||
val elapsed = SystemClock.elapsedRealtime() - start
|
elapsed = SystemClock.elapsedRealtime() - start
|
||||||
|
|
||||||
if (code == 204 || code == 200 && conn.responseLength == 0L) {
|
if (code == 204 || code == 200 && conn.responseLength == 0L) {
|
||||||
result = context.getString(R.string.connection_test_available, elapsed)
|
result = context.getString(R.string.connection_test_available, elapsed)
|
||||||
@@ -133,10 +134,7 @@ object SpeedtestUtil {
|
|||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
// network exception
|
// network exception
|
||||||
Log.d(
|
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
|
||||||
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
|
// library exception, eg sumsung
|
||||||
@@ -146,7 +144,7 @@ object SpeedtestUtil {
|
|||||||
conn?.disconnect()
|
conn?.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return Pair(elapsed, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibVersion(): String {
|
fun getLibVersion(): String {
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ 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.appcompat.app.AppCompatDelegate
|
||||||
import com.tencent.mmkv.MMKV
|
import androidx.core.content.ContextCompat
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
import com.v2ray.ang.BuildConfig
|
import com.v2ray.ang.BuildConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.dto.Language
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.*
|
import java.net.*
|
||||||
@@ -30,9 +33,6 @@ import java.util.*
|
|||||||
|
|
||||||
object Utils {
|
object Utils {
|
||||||
|
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert string to editalbe for kotlin
|
* convert string to editalbe for kotlin
|
||||||
*
|
*
|
||||||
@@ -131,7 +131,7 @@ object Utils {
|
|||||||
* get remote dns servers from preference
|
* get remote dns servers from preference
|
||||||
*/
|
*/
|
||||||
fun getRemoteDnsServers(): List<String> {
|
fun getRemoteDnsServers(): List<String> {
|
||||||
val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
val remoteDns = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
|
||||||
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
||||||
if (ret.isEmpty()) {
|
if (ret.isEmpty()) {
|
||||||
return listOf(AppConfig.DNS_PROXY)
|
return listOf(AppConfig.DNS_PROXY)
|
||||||
@@ -140,7 +140,7 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getVpnDnsServers(): List<String> {
|
fun getVpnDnsServers(): List<String> {
|
||||||
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
|
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
|
||||||
return vpnDns.split(",").filter { isPureIpAddress(it) }
|
return vpnDns.split(",").filter { isPureIpAddress(it) }
|
||||||
// allow empty, in that case dns will use system default
|
// allow empty, in that case dns will use system default
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ object Utils {
|
|||||||
* get remote dns servers from preference
|
* get remote dns servers from preference
|
||||||
*/
|
*/
|
||||||
fun getDomesticDnsServers(): List<String> {
|
fun getDomesticDnsServers(): List<String> {
|
||||||
val domesticDns = settingsStorage?.decodeString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
|
val domesticDns = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
|
||||||
val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
|
||||||
if (ret.isEmpty()) {
|
if (ret.isEmpty()) {
|
||||||
return listOf(AppConfig.DNS_DIRECT)
|
return listOf(AppConfig.DNS_DIRECT)
|
||||||
@@ -160,8 +160,11 @@ object Utils {
|
|||||||
/**
|
/**
|
||||||
* is ip address
|
* is ip address
|
||||||
*/
|
*/
|
||||||
fun isIpAddress(value: String): Boolean {
|
fun isIpAddress(value: String?): Boolean {
|
||||||
try {
|
try {
|
||||||
|
if (value.isNullOrEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
var addr = value
|
var addr = value
|
||||||
if (addr.isEmpty() || addr.isBlank()) {
|
if (addr.isEmpty() || addr.isBlank()) {
|
||||||
return false
|
return false
|
||||||
@@ -246,7 +249,7 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startVServiceFromToggle(context: Context): Boolean {
|
fun startVServiceFromToggle(context: Context): Boolean {
|
||||||
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
context.toast(R.string.app_tile_first_use)
|
context.toast(R.string.app_tile_first_use)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -362,7 +365,7 @@ object Utils {
|
|||||||
url.openConnection(
|
url.openConnection(
|
||||||
Proxy(
|
Proxy(
|
||||||
Proxy.Type.HTTP,
|
Proxy.Type.HTTP,
|
||||||
InetSocketAddress("127.0.0.1", httpPort)
|
InetSocketAddress(LOOPBACK, httpPort)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -388,7 +391,7 @@ object Utils {
|
|||||||
|
|
||||||
|
|
||||||
fun setNightMode(context: Context) {
|
fun setNightMode(context: Context) {
|
||||||
when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
|
||||||
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||||
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||||
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||||
@@ -407,17 +410,18 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLocale(): Locale {
|
fun getLocale(): Locale {
|
||||||
val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto"
|
val langCode = MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
|
||||||
return when (lang) {
|
val language = Language.fromCode(langCode)
|
||||||
"auto" -> getSysLocale()
|
|
||||||
"en" -> Locale.ENGLISH
|
return when (language) {
|
||||||
"zh-rCN" -> Locale.CHINA
|
Language.AUTO -> getSysLocale()
|
||||||
"zh-rTW" -> Locale.TRADITIONAL_CHINESE
|
Language.ENGLISH -> Locale.ENGLISH
|
||||||
"vi" -> Locale("vi")
|
Language.CHINA -> Locale.CHINA
|
||||||
"ru" -> Locale("ru")
|
Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
|
||||||
"fa" -> Locale("fa")
|
Language.VIETNAMESE -> Locale("vi")
|
||||||
"bn" -> Locale("bn")
|
Language.RUSSIAN -> Locale("ru")
|
||||||
else -> getSysLocale()
|
Language.PERSIAN -> Locale("fa")
|
||||||
|
Language.BANGLA -> Locale("bn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,10 +455,39 @@ object Utils {
|
|||||||
return if (second) {
|
return if (second) {
|
||||||
AppConfig.DelayTestUrl2
|
AppConfig.DelayTestUrl2
|
||||||
} else {
|
} else {
|
||||||
settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl
|
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findFreePort(ports: List<Int>): Int {
|
||||||
|
for (port in ports) {
|
||||||
|
try {
|
||||||
|
return ServerSocket(port).use { it.localPort }
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
continue // try next port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the program gets here, no port in the range was found
|
||||||
|
throw IOException("no free port found")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isValidSubUrl(value: String?): Boolean {
|
||||||
|
try {
|
||||||
|
if (value.isNullOrEmpty()) return false
|
||||||
|
if (URLUtil.isHttpsUrl(value)) return true
|
||||||
|
if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
} else {
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,668 +0,0 @@
|
|||||||
package com.v2ray.ang.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
|
||||||
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_BLOCKED
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ERoutingMode
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
|
|
||||||
|
|
||||||
object V2rayConfigUtil {
|
|
||||||
private val serverRawStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SERVER_RAW,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Result(var status: Boolean, var content: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成v2ray的客户端配置文件
|
|
||||||
*/
|
|
||||||
fun getV2rayConfig(context: Context, guid: String): Result {
|
|
||||||
try {
|
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return Result(false, "")
|
|
||||||
if (config.configType == EConfigType.CUSTOM) {
|
|
||||||
val raw = serverRawStorage?.decodeString(guid)
|
|
||||||
val customConfig = if (raw.isNullOrBlank()) {
|
|
||||||
config.fullConfig?.toPrettyPrinting() ?: return Result(false, "")
|
|
||||||
} else {
|
|
||||||
raw
|
|
||||||
}
|
|
||||||
//Log.d(ANG_PACKAGE, customConfig)
|
|
||||||
return Result(true, customConfig)
|
|
||||||
}
|
|
||||||
val outbound = config.getProxyOutbound() ?: return Result(false, "")
|
|
||||||
val address = outbound.getServerAddress() ?: return Result(false, "")
|
|
||||||
if (!Utils.isIpAddress(address)) {
|
|
||||||
if (!Utils.isValidUrl(address)) {
|
|
||||||
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
|
|
||||||
return Result(false, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = getV2rayNonCustomConfig(context, outbound, config.remarks)
|
|
||||||
//Log.d(ANG_PACKAGE, result.content)
|
|
||||||
return result
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return Result(false, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成v2ray的客户端配置文件
|
|
||||||
*/
|
|
||||||
private fun getV2rayNonCustomConfig(
|
|
||||||
context: Context,
|
|
||||||
outbound: V2rayConfig.OutboundBean,
|
|
||||||
remarks: String,
|
|
||||||
): Result {
|
|
||||||
val result = Result(false, "")
|
|
||||||
//取得默认配置
|
|
||||||
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
|
||||||
if (TextUtils.isEmpty(assets)) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
//转成Json
|
|
||||||
val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result
|
|
||||||
|
|
||||||
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL)
|
|
||||||
?: "warning"
|
|
||||||
|
|
||||||
inbounds(v2rayConfig)
|
|
||||||
|
|
||||||
updateOutboundWithGlobalSettings(outbound)
|
|
||||||
v2rayConfig.outbounds[0] = outbound
|
|
||||||
|
|
||||||
updateOutboundFragment(v2rayConfig)
|
|
||||||
|
|
||||||
routing(v2rayConfig)
|
|
||||||
|
|
||||||
fakedns(v2rayConfig)
|
|
||||||
|
|
||||||
dns(v2rayConfig)
|
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
|
||||||
customLocalDns(v2rayConfig)
|
|
||||||
}
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) != true) {
|
|
||||||
v2rayConfig.stats = null
|
|
||||||
v2rayConfig.policy = null
|
|
||||||
}
|
|
||||||
|
|
||||||
v2rayConfig.remarks = remarks
|
|
||||||
|
|
||||||
result.status = true
|
|
||||||
result.content = v2rayConfig.toPrettyPrinting()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
val socksPort = Utils.parseInt(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT),
|
|
||||||
AppConfig.PORT_SOCKS.toInt()
|
|
||||||
)
|
|
||||||
val httpPort = Utils.parseInt(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
|
|
||||||
AppConfig.PORT_HTTP.toInt()
|
|
||||||
)
|
|
||||||
|
|
||||||
v2rayConfig.inbounds.forEach { curInbound ->
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
|
||||||
//bind all inbounds to localhost if the user requests
|
|
||||||
curInbound.listen = "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v2rayConfig.inbounds[0].port = socksPort
|
|
||||||
val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED)
|
|
||||||
?: false
|
|
||||||
val sniffAllTlsAndHttp =
|
|
||||||
settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
|
|
||||||
?: true
|
|
||||||
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
|
|
||||||
v2rayConfig.inbounds[0].sniffing?.routeOnly =
|
|
||||||
settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
|
|
||||||
if (!sniffAllTlsAndHttp) {
|
|
||||||
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
|
|
||||||
}
|
|
||||||
if (fakedns) {
|
|
||||||
v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
|
|
||||||
}
|
|
||||||
|
|
||||||
v2rayConfig.inbounds[1].port = httpPort
|
|
||||||
|
|
||||||
// if (httpPort > 0) {
|
|
||||||
// val httpCopy = v2rayConfig.inbounds[0].copy()
|
|
||||||
// httpCopy.port = httpPort
|
|
||||||
// httpCopy.protocol = "http"
|
|
||||||
// v2rayConfig.inbounds.add(httpCopy)
|
|
||||||
// }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fakedns(v2rayConfig: V2rayConfig) {
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
|
||||||
&& settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
|
||||||
) {
|
|
||||||
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* routing
|
|
||||||
*/
|
|
||||||
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
|
||||||
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
|
||||||
|
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
|
||||||
.orEmpty(), TAG_BLOCKED, v2rayConfig
|
|
||||||
)
|
|
||||||
if (routingMode == ERoutingMode.GLOBAL_DIRECT.value) {
|
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
|
||||||
.orEmpty(), TAG_DIRECT, v2rayConfig
|
|
||||||
)
|
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
|
||||||
.orEmpty(), TAG_PROXY, v2rayConfig
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
|
||||||
.orEmpty(), TAG_PROXY, v2rayConfig
|
|
||||||
)
|
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
|
||||||
.orEmpty(), TAG_DIRECT, v2rayConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
v2rayConfig.routing.domainStrategy =
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
|
||||||
?: "IPIfNonMatch"
|
|
||||||
|
|
||||||
// Hardcode googleapis.cn gstatic.com
|
|
||||||
val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
outboundTag = TAG_PROXY,
|
|
||||||
domain = arrayListOf("domain:googleapis.cn", "domain:gstatic.com")
|
|
||||||
)
|
|
||||||
|
|
||||||
when (routingMode) {
|
|
||||||
ERoutingMode.BYPASS_LAN.value -> {
|
|
||||||
routingGeo("", "private", TAG_DIRECT, v2rayConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
ERoutingMode.BYPASS_MAINLAND.value -> {
|
|
||||||
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
|
|
||||||
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
ERoutingMode.BYPASS_LAN_MAINLAND.value -> {
|
|
||||||
routingGeo("", "private", TAG_DIRECT, v2rayConfig)
|
|
||||||
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
|
|
||||||
v2rayConfig.routing.rules.add(0, googleapisRoute)
|
|
||||||
}
|
|
||||||
|
|
||||||
ERoutingMode.GLOBAL_DIRECT.value -> {
|
|
||||||
val globalDirect = V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
outboundTag = TAG_DIRECT,
|
|
||||||
)
|
|
||||||
if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") {
|
|
||||||
globalDirect.port = "0-65535"
|
|
||||||
} else {
|
|
||||||
globalDirect.ip = arrayListOf("0.0.0.0/0", "::/0")
|
|
||||||
}
|
|
||||||
v2rayConfig.routing.rules.add(globalDirect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routingMode != ERoutingMode.GLOBAL_DIRECT.value) {
|
|
||||||
val globalProxy = V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
outboundTag = TAG_PROXY,
|
|
||||||
)
|
|
||||||
if (v2rayConfig.routing.domainStrategy != "IPIfNonMatch") {
|
|
||||||
globalProxy.port = "0-65535"
|
|
||||||
} else {
|
|
||||||
globalProxy.ip = arrayListOf("0.0.0.0/0", "::/0")
|
|
||||||
}
|
|
||||||
v2rayConfig.routing.rules.add(globalProxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun routingGeo(
|
|
||||||
ipOrDomain: String,
|
|
||||||
code: String,
|
|
||||||
tag: String,
|
|
||||||
v2rayConfig: V2rayConfig
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (!TextUtils.isEmpty(code)) {
|
|
||||||
//IP
|
|
||||||
if (ipOrDomain == "ip" || ipOrDomain == "") {
|
|
||||||
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
|
||||||
rulesIP.outboundTag = tag
|
|
||||||
rulesIP.ip = ArrayList()
|
|
||||||
rulesIP.ip?.add("geoip:$code")
|
|
||||||
v2rayConfig.routing.rules.add(rulesIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ipOrDomain == "domain" || ipOrDomain == "") {
|
|
||||||
//Domain
|
|
||||||
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
|
||||||
rulesDomain.outboundTag = tag
|
|
||||||
rulesDomain.domain = ArrayList()
|
|
||||||
rulesDomain.domain?.add("geosite:$code")
|
|
||||||
v2rayConfig.routing.rules.add(rulesDomain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun routingUserRule(userRule: String, tag: String, v2rayConfig: V2rayConfig) {
|
|
||||||
try {
|
|
||||||
if (!TextUtils.isEmpty(userRule)) {
|
|
||||||
//Domain
|
|
||||||
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
|
||||||
rulesDomain.outboundTag = tag
|
|
||||||
rulesDomain.domain = ArrayList()
|
|
||||||
|
|
||||||
//IP
|
|
||||||
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
|
||||||
rulesIP.outboundTag = tag
|
|
||||||
rulesIP.ip = ArrayList()
|
|
||||||
|
|
||||||
userRule.split(",").map { it.trim() }.forEach {
|
|
||||||
if (it.startsWith("ext:") && it.contains("geoip")) {
|
|
||||||
rulesIP.ip?.add(it)
|
|
||||||
} else if (Utils.isIpAddress(it) || it.startsWith("geoip:")) {
|
|
||||||
rulesIP.ip?.add(it)
|
|
||||||
} else if (it.isNotEmpty()) {
|
|
||||||
rulesDomain.domain?.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((rulesDomain.domain?.size ?: 0) > 0) {
|
|
||||||
v2rayConfig.routing.rules.add(rulesDomain)
|
|
||||||
}
|
|
||||||
if ((rulesIP.ip?.size ?: 0) > 0) {
|
|
||||||
v2rayConfig.routing.rules.add(rulesIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun userRule2Domain(userRule: String): ArrayList<String> {
|
|
||||||
val domain = ArrayList<String>()
|
|
||||||
userRule.split(",").map { it.trim() }.forEach {
|
|
||||||
if (it.startsWith("geosite:") || it.startsWith("domain:")) {
|
|
||||||
domain.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return domain
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom Dns
|
|
||||||
*/
|
|
||||||
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
|
||||||
val geositeCn = arrayListOf("geosite:cn")
|
|
||||||
val proxyDomain = userRule2Domain(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
|
||||||
.orEmpty()
|
|
||||||
)
|
|
||||||
val directDomain = userRule2Domain(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
|
||||||
.orEmpty()
|
|
||||||
)
|
|
||||||
// fakedns with all domains to make it always top priority
|
|
||||||
v2rayConfig.dns.servers?.add(
|
|
||||||
0,
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
address = "fakedns",
|
|
||||||
domains = geositeCn.plus(proxyDomain).plus(directDomain)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS inbound对象
|
|
||||||
val remoteDns = Utils.getRemoteDnsServers()
|
|
||||||
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
|
|
||||||
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
|
|
||||||
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
|
|
||||||
port = 53,
|
|
||||||
network = "tcp,udp"
|
|
||||||
)
|
|
||||||
|
|
||||||
val localDnsPort = Utils.parseInt(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT),
|
|
||||||
AppConfig.PORT_LOCAL_DNS.toInt()
|
|
||||||
)
|
|
||||||
v2rayConfig.inbounds.add(
|
|
||||||
V2rayConfig.InboundBean(
|
|
||||||
tag = "dns-in",
|
|
||||||
port = localDnsPort,
|
|
||||||
listen = "127.0.0.1",
|
|
||||||
protocol = "dokodemo-door",
|
|
||||||
settings = dnsInboundSettings,
|
|
||||||
sniffing = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS outbound对象
|
|
||||||
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
|
|
||||||
v2rayConfig.outbounds.add(
|
|
||||||
V2rayConfig.OutboundBean(
|
|
||||||
protocol = "dns",
|
|
||||||
tag = "dns-out",
|
|
||||||
settings = null,
|
|
||||||
streamSettings = null,
|
|
||||||
mux = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS routing tag
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
0, V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
inboundTag = arrayListOf("dns-in"),
|
|
||||||
outboundTag = "dns-out",
|
|
||||||
domain = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dns(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
val hosts = mutableMapOf<String, Any>()
|
|
||||||
val servers = ArrayList<Any>()
|
|
||||||
|
|
||||||
//remote Dns
|
|
||||||
val remoteDns = Utils.getRemoteDnsServers()
|
|
||||||
val proxyDomain = userRule2Domain(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
|
||||||
.orEmpty()
|
|
||||||
)
|
|
||||||
remoteDns.forEach {
|
|
||||||
servers.add(it)
|
|
||||||
}
|
|
||||||
if (proxyDomain.size > 0) {
|
|
||||||
servers.add(
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
remoteDns.first(),
|
|
||||||
53,
|
|
||||||
proxyDomain,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// domestic DNS
|
|
||||||
val domesticDns = Utils.getDomesticDnsServers()
|
|
||||||
val directDomain = userRule2Domain(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
|
||||||
.orEmpty()
|
|
||||||
)
|
|
||||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
|
||||||
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
|
||||||
val isCnRoutingMode =
|
|
||||||
(routingMode == ERoutingMode.BYPASS_MAINLAND.value || routingMode == ERoutingMode.BYPASS_LAN_MAINLAND.value)
|
|
||||||
val geoipCn = arrayListOf("geoip:cn")
|
|
||||||
|
|
||||||
if (directDomain.size > 0) {
|
|
||||||
servers.add(
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
domesticDns.first(),
|
|
||||||
53,
|
|
||||||
directDomain,
|
|
||||||
if (isCnRoutingMode) geoipCn else null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (isCnRoutingMode) {
|
|
||||||
val geositeCn = arrayListOf("geosite:cn")
|
|
||||||
servers.add(
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
domesticDns.first(),
|
|
||||||
53,
|
|
||||||
geositeCn,
|
|
||||||
geoipCn
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utils.isPureIpAddress(domesticDns.first())) {
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
0, V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
outboundTag = TAG_DIRECT,
|
|
||||||
port = "53",
|
|
||||||
ip = arrayListOf(domesticDns.first()),
|
|
||||||
domain = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//block dns
|
|
||||||
val blkDomain = userRule2Domain(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
|
||||||
.orEmpty()
|
|
||||||
)
|
|
||||||
if (blkDomain.size > 0) {
|
|
||||||
hosts.putAll(blkDomain.map { it to "127.0.0.1" })
|
|
||||||
}
|
|
||||||
|
|
||||||
// hardcode googleapi rule to fix play store problems
|
|
||||||
hosts["domain:googleapis.cn"] = "googleapis.com"
|
|
||||||
|
|
||||||
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
|
||||||
hosts["dns.pub"] = arrayListOf("1.12.12.12", "120.53.53.53")
|
|
||||||
hosts["dns.alidns.com"] = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
|
||||||
hosts["one.one.one.one"] = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
|
||||||
hosts["dns.google"] = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
|
||||||
|
|
||||||
// DNS dns对象
|
|
||||||
v2rayConfig.dns = V2rayConfig.DnsBean(
|
|
||||||
servers = servers,
|
|
||||||
hosts = hosts
|
|
||||||
)
|
|
||||||
|
|
||||||
// DNS routing
|
|
||||||
if (Utils.isPureIpAddress(remoteDns.first())) {
|
|
||||||
v2rayConfig.routing.rules.add(
|
|
||||||
0, V2rayConfig.RoutingBean.RulesBean(
|
|
||||||
outboundTag = TAG_PROXY,
|
|
||||||
port = "53",
|
|
||||||
ip = arrayListOf(remoteDns.first()),
|
|
||||||
domain = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
|
|
||||||
try {
|
|
||||||
var muxEnabled = settingsStorage?.decodeBool(AppConfig.PREF_MUX_ENABLED, false)
|
|
||||||
val protocol = outbound.protocol
|
|
||||||
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.SOCKS.name, true)
|
|
||||||
|| protocol.equals(EConfigType.TROJAN.name, true)
|
|
||||||
|| protocol.equals(EConfigType.WIREGUARD.name, true)
|
|
||||||
) {
|
|
||||||
muxEnabled = false
|
|
||||||
} else if (protocol.equals(EConfigType.VLESS.name, true)
|
|
||||||
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true
|
|
||||||
) {
|
|
||||||
muxEnabled = false
|
|
||||||
}
|
|
||||||
if (muxEnabled == true) {
|
|
||||||
outbound.mux?.enabled = true
|
|
||||||
outbound.mux?.concurrency =
|
|
||||||
settingsStorage?.decodeInt(AppConfig.PREF_MUX_CONCURRENCY) ?: 8
|
|
||||||
outbound.mux?.xudpConcurrency =
|
|
||||||
settingsStorage?.decodeInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY) ?: 8
|
|
||||||
outbound.mux?.xudpProxyUDP443 =
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
|
|
||||||
} else {
|
|
||||||
outbound.mux?.enabled = false
|
|
||||||
outbound.mux?.concurrency = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
|
||||||
var localTunAddr = if (outbound.settings?.address == null) {
|
|
||||||
listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6)
|
|
||||||
} else {
|
|
||||||
outbound.settings?.address as List<*>
|
|
||||||
}
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) != true) {
|
|
||||||
localTunAddr = listOf(localTunAddr.first())
|
|
||||||
}
|
|
||||||
outbound.settings?.address = localTunAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outbound.streamSettings?.network == DEFAULT_NETWORK
|
|
||||||
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP
|
|
||||||
) {
|
|
||||||
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
|
|
||||||
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
|
|
||||||
|
|
||||||
val requestString: String by lazy {
|
|
||||||
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
|
|
||||||
}
|
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request = Gson().fromJson(
|
|
||||||
requestString,
|
|
||||||
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
|
||||||
)
|
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request?.path =
|
|
||||||
if (path.isNullOrEmpty()) {
|
|
||||||
listOf("/")
|
|
||||||
} else {
|
|
||||||
path
|
|
||||||
}
|
|
||||||
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
|
|
||||||
try {
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.TLS
|
|
||||||
&& v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.REALITY
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val fragmentOutbound =
|
|
||||||
V2rayConfig.OutboundBean(
|
|
||||||
protocol = PROTOCOL_FREEDOM,
|
|
||||||
tag = TAG_FRAGMENT,
|
|
||||||
mux = null
|
|
||||||
)
|
|
||||||
|
|
||||||
var packets =
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
|
||||||
if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY
|
|
||||||
&& packets == "tlshello"
|
|
||||||
) {
|
|
||||||
packets = "1-3"
|
|
||||||
} else if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.TLS
|
|
||||||
&& packets != "tlshello"
|
|
||||||
) {
|
|
||||||
packets = "tlshello"
|
|
||||||
}
|
|
||||||
|
|
||||||
fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
|
||||||
fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
|
|
||||||
packets = packets,
|
|
||||||
length = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_LENGTH)
|
|
||||||
?: "50-100",
|
|
||||||
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
|
||||||
?: "10-20"
|
|
||||||
),
|
|
||||||
noise = V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
|
|
||||||
packet = "rand:100-200",
|
|
||||||
delay = "10-20",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
|
|
||||||
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
|
||||||
TcpNoDelay = true,
|
|
||||||
mark = 255
|
|
||||||
)
|
|
||||||
)
|
|
||||||
v2rayConfig.outbounds.add(fragmentOutbound)
|
|
||||||
|
|
||||||
//proxy chain
|
|
||||||
v2rayConfig.outbounds[0].streamSettings?.sockopt =
|
|
||||||
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
|
||||||
dialerProxy = TAG_FRAGMENT
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
package com.v2ray.ang.util.fmt
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object ShadowsocksFmt {
|
|
||||||
fun parseShadowsocks(str: String): ServerConfig? {
|
|
||||||
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
|
|
||||||
if (!tryResolveResolveSip002(str, config)) {
|
|
||||||
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
|
||||||
val indexSplit = result.indexOf("#")
|
|
||||||
if (indexSplit > 0) {
|
|
||||||
try {
|
|
||||||
config.remarks =
|
|
||||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
result = result.substring(0, indexSplit)
|
|
||||||
}
|
|
||||||
|
|
||||||
//part decode
|
|
||||||
val indexS = result.indexOf("@")
|
|
||||||
result = if (indexS > 0) {
|
|
||||||
Utils.decode(result.substring(0, indexS)) + result.substring(
|
|
||||||
indexS,
|
|
||||||
result.length
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Utils.decode(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
|
|
||||||
val match = legacyPattern.matchEntire(result)
|
|
||||||
?: return null
|
|
||||||
|
|
||||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
server.address = match.groupValues[3].removeSurrounding("[", "]")
|
|
||||||
server.port = match.groupValues[4].toInt()
|
|
||||||
server.password = match.groupValues[2]
|
|
||||||
server.method = match.groupValues[1].lowercase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ServerConfig): String {
|
|
||||||
val outbound = config.getProxyOutbound() ?: return ""
|
|
||||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
|
||||||
val pw =
|
|
||||||
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
|
|
||||||
val url = String.format(
|
|
||||||
"%s@%s:%s",
|
|
||||||
pw,
|
|
||||||
Utils.getIpv6Address(outbound.getServerAddress()),
|
|
||||||
outbound.getServerPort()
|
|
||||||
)
|
|
||||||
return url + remark
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
|
|
||||||
try {
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
|
|
||||||
val method: String
|
|
||||||
val password: String
|
|
||||||
if (uri.userInfo.contains(":")) {
|
|
||||||
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
|
|
||||||
if (arrUserInfo.count() != 2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
method = arrUserInfo[0]
|
|
||||||
password = Utils.urlDecode(arrUserInfo[1])
|
|
||||||
} else {
|
|
||||||
val base64Decode = Utils.decode(uri.userInfo)
|
|
||||||
val arrUserInfo = base64Decode.split(":").map { it.trim() }
|
|
||||||
if (arrUserInfo.count() < 2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
method = arrUserInfo[0]
|
|
||||||
password = base64Decode.substringAfter(":")
|
|
||||||
}
|
|
||||||
|
|
||||||
val query = Utils.urlDecode(uri.query.orEmpty())
|
|
||||||
if (query != "") {
|
|
||||||
val queryPairs = HashMap<String, String>()
|
|
||||||
val pairs = query.split(";")
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
|
|
||||||
for (pair in pairs) {
|
|
||||||
val idx = pair.indexOf("=")
|
|
||||||
if (idx == -1) {
|
|
||||||
queryPairs[Utils.urlDecode(pair)] = ""
|
|
||||||
} else {
|
|
||||||
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
|
|
||||||
Utils.urlDecode(pair.substring(idx + 1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
|
|
||||||
var sni: String? = ""
|
|
||||||
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
|
|
||||||
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
|
||||||
"tcp",
|
|
||||||
"http",
|
|
||||||
queryPairs["obfs-host"],
|
|
||||||
queryPairs["path"],
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
} else if (queryPairs["plugin"] == "v2ray-plugin") {
|
|
||||||
var network = "ws"
|
|
||||||
if (queryPairs["mode"] == "quic") {
|
|
||||||
network = "quic"
|
|
||||||
}
|
|
||||||
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
|
||||||
network,
|
|
||||||
null,
|
|
||||||
queryPairs["host"],
|
|
||||||
queryPairs["path"],
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if ("tls" in queryPairs) {
|
|
||||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
|
||||||
"tls", false, sni.orEmpty(), null, null, null, null, null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
server.address = uri.idnHost
|
|
||||||
server.port = uri.port
|
|
||||||
server.password = password
|
|
||||||
server.method = method
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, e.toString())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package com.v2ray.ang.util.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
|
|
||||||
object SocksFmt {
|
|
||||||
fun parseSocks(str: String): ServerConfig? {
|
|
||||||
val config = ServerConfig.create(EConfigType.SOCKS)
|
|
||||||
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
|
|
||||||
val indexSplit = result.indexOf("#")
|
|
||||||
|
|
||||||
if (indexSplit > 0) {
|
|
||||||
try {
|
|
||||||
config.remarks =
|
|
||||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
result = result.substring(0, indexSplit)
|
|
||||||
}
|
|
||||||
|
|
||||||
//part decode
|
|
||||||
val indexS = result.indexOf("@")
|
|
||||||
if (indexS > 0) {
|
|
||||||
result = Utils.decode(result.substring(0, indexS)) + result.substring(
|
|
||||||
indexS,
|
|
||||||
result.length
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
result = Utils.decode(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
|
|
||||||
val match =
|
|
||||||
legacyPattern.matchEntire(result) ?: return null
|
|
||||||
|
|
||||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
server.address = match.groupValues[3].removeSurrounding("[", "]")
|
|
||||||
server.port = match.groupValues[4].toInt()
|
|
||||||
val socksUsersBean =
|
|
||||||
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
|
||||||
socksUsersBean.user = match.groupValues[1]
|
|
||||||
socksUsersBean.pass = match.groupValues[2]
|
|
||||||
server.users = listOf(socksUsersBean)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ServerConfig): String {
|
|
||||||
val outbound = config.getProxyOutbound() ?: return ""
|
|
||||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
|
||||||
val pw =
|
|
||||||
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
|
|
||||||
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
|
|
||||||
else
|
|
||||||
":"
|
|
||||||
val url = String.format(
|
|
||||||
"%s@%s:%s",
|
|
||||||
Utils.encode(pw),
|
|
||||||
Utils.getIpv6Address(outbound.getServerAddress()),
|
|
||||||
outbound.getServerPort()
|
|
||||||
)
|
|
||||||
return url + remark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
package com.v2ray.ang.util.fmt
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object TrojanFmt {
|
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseTrojan(str: String): ServerConfig {
|
|
||||||
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
|
||||||
val config = ServerConfig.create(EConfigType.TROJAN)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
|
|
||||||
var flow = ""
|
|
||||||
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) {
|
|
||||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
|
||||||
V2rayConfig.TLS,
|
|
||||||
allowInsecure,
|
|
||||||
"",
|
|
||||||
fingerprint,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val queryParam = uri.rawQuery.split("&")
|
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
|
||||||
|
|
||||||
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
|
||||||
queryParam["type"] ?: "tcp",
|
|
||||||
queryParam["headerType"],
|
|
||||||
queryParam["host"],
|
|
||||||
queryParam["path"],
|
|
||||||
queryParam["seed"],
|
|
||||||
queryParam["quicSecurity"],
|
|
||||||
queryParam["key"],
|
|
||||||
queryParam["mode"],
|
|
||||||
queryParam["serviceName"],
|
|
||||||
queryParam["authority"]
|
|
||||||
)
|
|
||||||
fingerprint = queryParam["fp"].orEmpty()
|
|
||||||
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
|
||||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
|
||||||
queryParam["security"] ?: V2rayConfig.TLS,
|
|
||||||
allowInsecure,
|
|
||||||
queryParam["sni"] ?: sni.orEmpty(),
|
|
||||||
fingerprint,
|
|
||||||
queryParam["alpn"],
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
flow = queryParam["flow"].orEmpty()
|
|
||||||
}
|
|
||||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
|
||||||
server.address = uri.idnHost
|
|
||||||
server.port = uri.port
|
|
||||||
server.password = uri.userInfo
|
|
||||||
server.flow = flow
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ServerConfig): String {
|
|
||||||
val outbound = config.getProxyOutbound() ?: return ""
|
|
||||||
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
|
||||||
|
|
||||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
|
|
||||||
if (!TextUtils.isEmpty(it)) {
|
|
||||||
dicQuery["flow"] = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
|
||||||
(streamSetting.tlsSettings
|
|
||||||
?: streamSetting.realitySettings)?.let { tlsSetting ->
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
|
|
||||||
dicQuery["sni"] = tlsSetting.serverName
|
|
||||||
}
|
|
||||||
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
|
||||||
dicQuery["alpn"] =
|
|
||||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
|
||||||
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
|
||||||
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
|
||||||
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
|
||||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dicQuery["type"] =
|
|
||||||
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
|
|
||||||
|
|
||||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
|
||||||
when (streamSetting.network) {
|
|
||||||
"tcp" -> {
|
|
||||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
|
||||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"kcp" -> {
|
|
||||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
|
||||||
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"ws", "httpupgrade", "splithttp" -> {
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
|
||||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
|
||||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"http", "h2" -> {
|
|
||||||
dicQuery["type"] = "http"
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
|
||||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
|
||||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"quic" -> {
|
|
||||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
|
||||||
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
"grpc" -> {
|
|
||||||
dicQuery["mode"] = transportDetails[0]
|
|
||||||
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val query = "?" + dicQuery.toList().joinToString(
|
|
||||||
separator = "&",
|
|
||||||
transform = { it.first + "=" + it.second })
|
|
||||||
|
|
||||||
val url = String.format(
|
|
||||||
"%s@%s:%s",
|
|
||||||
outbound.getPassword(),
|
|
||||||
Utils.getIpv6Address(outbound.getServerAddress()),
|
|
||||||
outbound.getServerPort()
|
|
||||||
)
|
|
||||||
return url + query + remark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
package com.v2ray.ang.util.fmt
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object VlessFmt {
|
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseVless(str: String): ServerConfig? {
|
|
||||||
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
|
||||||
val config = ServerConfig.create(EConfigType.VLESS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
|
||||||
val queryParam = uri.rawQuery.split("&")
|
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
|
||||||
|
|
||||||
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
|
||||||
vnext.address = uri.idnHost
|
|
||||||
vnext.port = uri.port
|
|
||||||
vnext.users[0].id = uri.userInfo
|
|
||||||
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
|
|
||||||
vnext.users[0].flow = queryParam["flow"].orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
val sni = streamSetting.populateTransportSettings(
|
|
||||||
queryParam["type"] ?: "tcp",
|
|
||||||
queryParam["headerType"],
|
|
||||||
queryParam["host"],
|
|
||||||
queryParam["path"],
|
|
||||||
queryParam["seed"],
|
|
||||||
queryParam["quicSecurity"],
|
|
||||||
queryParam["key"],
|
|
||||||
queryParam["mode"],
|
|
||||||
queryParam["serviceName"],
|
|
||||||
queryParam["authority"]
|
|
||||||
)
|
|
||||||
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
|
||||||
streamSetting.populateTlsSettings(
|
|
||||||
queryParam["security"].orEmpty(),
|
|
||||||
allowInsecure,
|
|
||||||
queryParam["sni"] ?: sni,
|
|
||||||
queryParam["fp"].orEmpty(),
|
|
||||||
queryParam["alpn"],
|
|
||||||
queryParam["pbk"].orEmpty(),
|
|
||||||
queryParam["sid"].orEmpty(),
|
|
||||||
queryParam["spx"].orEmpty()
|
|
||||||
)
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ServerConfig): String {
|
|
||||||
val outbound = config.getProxyOutbound() ?: return ""
|
|
||||||
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
|
||||||
|
|
||||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
|
|
||||||
if (!TextUtils.isEmpty(it)) {
|
|
||||||
dicQuery["flow"] = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dicQuery["encryption"] =
|
|
||||||
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
|
|
||||||
else outbound.getSecurityEncryption().orEmpty()
|
|
||||||
|
|
||||||
|
|
||||||
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
|
||||||
(streamSetting.tlsSettings
|
|
||||||
?: streamSetting.realitySettings)?.let { tlsSetting ->
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
|
|
||||||
dicQuery["sni"] = tlsSetting.serverName
|
|
||||||
}
|
|
||||||
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
|
||||||
dicQuery["alpn"] =
|
|
||||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
|
||||||
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
|
||||||
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
|
||||||
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
|
||||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dicQuery["type"] =
|
|
||||||
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
|
|
||||||
|
|
||||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
|
||||||
when (streamSetting.network) {
|
|
||||||
"tcp" -> {
|
|
||||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
|
||||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"kcp" -> {
|
|
||||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
|
||||||
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"ws", "httpupgrade", "splithttp" -> {
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
|
||||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
|
||||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"http", "h2" -> {
|
|
||||||
dicQuery["type"] = "http"
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
|
||||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
}
|
|
||||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
|
||||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"quic" -> {
|
|
||||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
|
||||||
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
"grpc" -> {
|
|
||||||
dicQuery["mode"] = transportDetails[0]
|
|
||||||
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
|
|
||||||
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val query = "?" + dicQuery.toList().joinToString(
|
|
||||||
separator = "&",
|
|
||||||
transform = { it.first + "=" + it.second })
|
|
||||||
|
|
||||||
val url = String.format(
|
|
||||||
"%s@%s:%s",
|
|
||||||
outbound.getPassword(),
|
|
||||||
Utils.getIpv6Address(outbound.getServerAddress()),
|
|
||||||
outbound.getServerPort()
|
|
||||||
)
|
|
||||||
return url + query + remark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package com.v2ray.ang.util.fmt
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.dto.VmessQRCode
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object VmessFmt {
|
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseVmess(str: String): ServerConfig? {
|
|
||||||
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
|
||||||
return parseVmessStd(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
|
||||||
val config = ServerConfig.create(EConfigType.VMESS)
|
|
||||||
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
|
||||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
|
||||||
result = Utils.decode(result)
|
|
||||||
if (TextUtils.isEmpty(result)) {
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
|
|
||||||
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
|
||||||
if (TextUtils.isEmpty(vmessQRCode.add)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.port)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.id)
|
|
||||||
|| TextUtils.isEmpty(vmessQRCode.net)
|
|
||||||
) {
|
|
||||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
config.remarks = vmessQRCode.ps
|
|
||||||
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
|
||||||
vnext.address = vmessQRCode.add
|
|
||||||
vnext.port = Utils.parseInt(vmessQRCode.port)
|
|
||||||
vnext.users[0].id = vmessQRCode.id
|
|
||||||
vnext.users[0].security =
|
|
||||||
if (TextUtils.isEmpty(vmessQRCode.scy)) V2rayConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
|
||||||
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
|
|
||||||
}
|
|
||||||
val sni = streamSetting.populateTransportSettings(
|
|
||||||
vmessQRCode.net,
|
|
||||||
vmessQRCode.type,
|
|
||||||
vmessQRCode.host,
|
|
||||||
vmessQRCode.path,
|
|
||||||
vmessQRCode.path,
|
|
||||||
vmessQRCode.host,
|
|
||||||
vmessQRCode.path,
|
|
||||||
vmessQRCode.type,
|
|
||||||
vmessQRCode.path,
|
|
||||||
vmessQRCode.host
|
|
||||||
)
|
|
||||||
|
|
||||||
val fingerprint = vmessQRCode.fp
|
|
||||||
streamSetting.populateTlsSettings(
|
|
||||||
vmessQRCode.tls,
|
|
||||||
allowInsecure,
|
|
||||||
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
|
|
||||||
fingerprint,
|
|
||||||
vmessQRCode.alpn,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toUri(config: ServerConfig): String {
|
|
||||||
val outbound = config.getProxyOutbound() ?: return ""
|
|
||||||
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
|
||||||
|
|
||||||
val vmessQRCode = VmessQRCode()
|
|
||||||
vmessQRCode.v = "2"
|
|
||||||
vmessQRCode.ps = config.remarks
|
|
||||||
vmessQRCode.add = outbound.getServerAddress().orEmpty()
|
|
||||||
vmessQRCode.port = outbound.getServerPort().toString()
|
|
||||||
vmessQRCode.id = outbound.getPassword().orEmpty()
|
|
||||||
vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
|
|
||||||
vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
|
|
||||||
vmessQRCode.net = streamSetting.network
|
|
||||||
vmessQRCode.tls = streamSetting.security
|
|
||||||
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
|
|
||||||
vmessQRCode.alpn =
|
|
||||||
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty()
|
|
||||||
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
|
|
||||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
|
||||||
vmessQRCode.type = transportDetails[0]
|
|
||||||
vmessQRCode.host = transportDetails[1]
|
|
||||||
vmessQRCode.path = transportDetails[2]
|
|
||||||
}
|
|
||||||
val json = Gson().toJson(vmessQRCode)
|
|
||||||
return Utils.encode(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseVmessStd(str: String): ServerConfig? {
|
|
||||||
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
|
||||||
val config = ServerConfig.create(EConfigType.VMESS)
|
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
|
||||||
val queryParam = uri.rawQuery.split("&")
|
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
|
||||||
|
|
||||||
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
|
||||||
vnext.address = uri.idnHost
|
|
||||||
vnext.port = uri.port
|
|
||||||
vnext.users[0].id = uri.userInfo
|
|
||||||
vnext.users[0].security = V2rayConfig.DEFAULT_SECURITY
|
|
||||||
vnext.users[0].alterId = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val sni = streamSetting.populateTransportSettings(
|
|
||||||
queryParam["type"] ?: "tcp",
|
|
||||||
queryParam["headerType"],
|
|
||||||
queryParam["host"],
|
|
||||||
queryParam["path"],
|
|
||||||
queryParam["seed"],
|
|
||||||
queryParam["quicSecurity"],
|
|
||||||
queryParam["key"],
|
|
||||||
queryParam["mode"],
|
|
||||||
queryParam["serviceName"],
|
|
||||||
queryParam["authority"]
|
|
||||||
)
|
|
||||||
|
|
||||||
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
|
||||||
streamSetting.populateTlsSettings(
|
|
||||||
queryParam["security"].orEmpty(),
|
|
||||||
allowInsecure,
|
|
||||||
queryParam["sni"] ?: sni,
|
|
||||||
queryParam["fp"].orEmpty(),
|
|
||||||
queryParam["alpn"],
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package com.v2ray.ang.util.fmt
|
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.extension.idnHost
|
|
||||||
import com.v2ray.ang.extension.removeWhiteSpace
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object WireguardFmt {
|
|
||||||
fun parseWireguard(str: String): ServerConfig? {
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
|
||||||
if (uri.rawQuery != null) {
|
|
||||||
val config = ServerConfig.create(EConfigType.WIREGUARD)
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
|
||||||
|
|
||||||
val queryParam = uri.rawQuery.split("&")
|
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
|
||||||
|
|
||||||
config.outboundBean?.settings?.let { wireguard ->
|
|
||||||
wireguard.secretKey = uri.userInfo
|
|
||||||
wireguard.address =
|
|
||||||
(queryParam["address"]
|
|
||||||
?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
|
|
||||||
.split(",")
|
|
||||||
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"].orEmpty()
|
|
||||||
wireguard.peers?.get(0)?.endpoint =
|
|
||||||
Utils.getIpv6Address(uri.idnHost) + ":${uri.port}"
|
|
||||||
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
|
||||||
wireguard.reserved =
|
|
||||||
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
|
|
||||||
.map { it.toInt() }
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseWireguardConfFile(str: String): ServerConfig? {
|
|
||||||
val config = ServerConfig.create(EConfigType.WIREGUARD)
|
|
||||||
val queryParam: MutableMap<String, String> = mutableMapOf()
|
|
||||||
|
|
||||||
var currentSection: String? = null
|
|
||||||
|
|
||||||
str.lines().forEach { line ->
|
|
||||||
val trimmedLine = line.trim()
|
|
||||||
|
|
||||||
when {
|
|
||||||
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
|
|
||||||
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
|
|
||||||
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
|
|
||||||
currentSection != null -> {
|
|
||||||
val (key, value) = trimmedLine.split("=").map { it.trim() }
|
|
||||||
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.outboundBean?.settings?.let { wireguard ->
|
|
||||||
wireguard.secretKey = queryParam["privatekey"].orEmpty()
|
|
||||||
wireguard.address = (queryParam["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace().split(",")
|
|
||||||
wireguard.peers?.getOrNull(0)?.publicKey = queryParam["publickey"].orEmpty()
|
|
||||||
wireguard.peers?.getOrNull(0)?.endpoint = queryParam["endpoint"].orEmpty()
|
|
||||||
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
|
||||||
wireguard.reserved = (queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",").map { it.toInt() }
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun toUri(config: ServerConfig): String {
|
|
||||||
val outbound = config.getProxyOutbound() ?: return ""
|
|
||||||
|
|
||||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
|
||||||
val dicQuery = HashMap<String, String>()
|
|
||||||
dicQuery["publickey"] =
|
|
||||||
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
|
|
||||||
if (outbound.settings?.reserved != null) {
|
|
||||||
dicQuery["reserved"] = Utils.urlEncode(
|
|
||||||
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
|
|
||||||
.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
dicQuery["address"] = Utils.urlEncode(
|
|
||||||
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
|
|
||||||
.toString()
|
|
||||||
)
|
|
||||||
if (outbound.settings?.mtu != null) {
|
|
||||||
dicQuery["mtu"] = outbound.settings?.mtu.toString()
|
|
||||||
}
|
|
||||||
val query = "?" + dicQuery.toList().joinToString(
|
|
||||||
separator = "&",
|
|
||||||
transform = { it.first + "=" + it.second })
|
|
||||||
|
|
||||||
val url = String.format(
|
|
||||||
"%s@%s:%s",
|
|
||||||
Utils.urlEncode(outbound.getPassword().toString()),
|
|
||||||
Utils.getIpv6Address(outbound.getServerAddress()),
|
|
||||||
outbound.getServerPort()
|
|
||||||
)
|
|
||||||
return url + query + remark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,44 +8,37 @@ import android.content.IntentFilter
|
|||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.gson.Gson
|
|
||||||
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.AppConfig.ANG_PACKAGE
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
|
||||||
import com.v2ray.ang.dto.ServersCache
|
import com.v2ray.ang.dto.ServersCache
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS
|
|
||||||
import com.v2ray.ang.util.MmkvManager.subStorage
|
|
||||||
import com.v2ray.ang.util.SpeedtestUtil
|
import com.v2ray.ang.util.SpeedtestUtil
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import com.v2ray.ang.util.V2rayConfigUtil
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private var serverList = MmkvManager.decodeServerList()
|
private var serverList = MmkvManager.decodeServerList()
|
||||||
var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
||||||
|
|
||||||
//var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
|
//var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
|
||||||
var keywordFilter = ""
|
var keywordFilter = ""
|
||||||
val serversCache = mutableListOf<ServersCache>()
|
val serversCache = mutableListOf<ServersCache>()
|
||||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||||
@@ -53,20 +46,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||||
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
|
*/
|
||||||
|
|
||||||
fun startListenBroadcast() {
|
fun startListenBroadcast() {
|
||||||
isRunning.value = false
|
isRunning.value = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
||||||
getApplication<AngApplication>().registerReceiver(
|
ContextCompat.registerReceiver(getApplication(), mMsgReceiver, mFilter, Utils.receiverFlags())
|
||||||
mMsgReceiver,
|
|
||||||
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY),
|
|
||||||
Context.RECEIVER_EXPORTED
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
getApplication<AngApplication>().registerReceiver(
|
|
||||||
mMsgReceiver,
|
|
||||||
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,21 +87,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
&& server.contains("routing")
|
&& server.contains("routing")
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
val config = CustomFmt.parse(server) ?: return false
|
||||||
config.subscriptionId = subscriptionId
|
config.subscriptionId = subscriptionId
|
||||||
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
|
|
||||||
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.serverRawStorage?.encode(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
serverList.add(0, key)
|
serverList.add(0, key)
|
||||||
val profile = ProfileItem(
|
// val profile = ProfileLiteItem(
|
||||||
configType = config.configType,
|
// configType = config.configType,
|
||||||
subscriptionId = config.subscriptionId,
|
// subscriptionId = config.subscriptionId,
|
||||||
remarks = config.remarks,
|
// remarks = config.remarks,
|
||||||
server = config.getProxyOutbound()?.getServerAddress(),
|
// server = config.getProxyOutbound()?.getServerAddress(),
|
||||||
serverPort = config.getProxyOutbound()?.getServerPort(),
|
// serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||||
)
|
// )
|
||||||
serversCache.add(0, ServersCache(key, profile))
|
serversCache.add(0, ServersCache(key, config))
|
||||||
return true
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -123,27 +109,34 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun swapServer(fromPosition: Int, toPosition: Int) {
|
fun swapServer(fromPosition: Int, toPosition: Int) {
|
||||||
Collections.swap(serverList, fromPosition, toPosition)
|
if (subscriptionId.isEmpty()) {
|
||||||
|
Collections.swap(serverList, fromPosition, toPosition)
|
||||||
|
} else {
|
||||||
|
val fromPosition2 = serverList.indexOf(serversCache[fromPosition].guid)
|
||||||
|
val toPosition2 = serverList.indexOf(serversCache[toPosition].guid)
|
||||||
|
Collections.swap(serverList, fromPosition2, toPosition2)
|
||||||
|
}
|
||||||
Collections.swap(serversCache, fromPosition, toPosition)
|
Collections.swap(serversCache, fromPosition, toPosition)
|
||||||
MmkvManager.mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
MmkvManager.encodeServerList(serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun updateCache() {
|
fun updateCache() {
|
||||||
serversCache.clear()
|
serversCache.clear()
|
||||||
for (guid in serverList) {
|
for (guid in serverList) {
|
||||||
var profile = MmkvManager.decodeProfileConfig(guid)
|
var profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||||
if (profile == null) {
|
// var profile = MmkvManager.decodeProfileConfig(guid)
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: continue
|
// if (profile == null) {
|
||||||
profile = ProfileItem(
|
// val config = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||||
configType = config.configType,
|
// profile = ProfileLiteItem(
|
||||||
subscriptionId = config.subscriptionId,
|
// configType = config.configType,
|
||||||
remarks = config.remarks,
|
// subscriptionId = config.subscriptionId,
|
||||||
server = config.getProxyOutbound()?.getServerAddress(),
|
// remarks = config.remarks,
|
||||||
serverPort = config.getProxyOutbound()?.getServerPort(),
|
// server = config.getProxyOutbound()?.getServerAddress(),
|
||||||
)
|
// serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||||
MmkvManager.encodeServerConfig(guid, config)
|
// )
|
||||||
}
|
// MmkvManager.encodeServerConfig(guid, config)
|
||||||
|
// }
|
||||||
|
|
||||||
if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
|
if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
|
||||||
continue
|
continue
|
||||||
@@ -156,21 +149,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateConfigViaSubAll(): Int {
|
fun updateConfigViaSubAll(): Int {
|
||||||
if (subscriptionId.isNullOrEmpty()) {
|
if (subscriptionId.isEmpty()) {
|
||||||
return AngConfigManager.updateConfigViaSubAll()
|
return AngConfigManager.updateConfigViaSubAll()
|
||||||
} else {
|
} else {
|
||||||
val json = subStorage?.decodeString(subscriptionId)
|
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0
|
||||||
if (!json.isNullOrBlank()) {
|
return AngConfigManager.updateConfigViaSub(Pair(subscriptionId, subItem))
|
||||||
return updateConfigViaSub(Pair(subscriptionId, Gson().fromJson(json, SubscriptionItem::class.java)))
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportAllServer(): Int {
|
fun exportAllServer(): Int {
|
||||||
val serverListCopy =
|
val serverListCopy =
|
||||||
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
serverList
|
serverList
|
||||||
} else {
|
} else {
|
||||||
serversCache.map { it.guid }.toList()
|
serversCache.map { it.guid }.toList()
|
||||||
@@ -188,16 +177,16 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
SpeedtestUtil.closeAllTcpSockets()
|
SpeedtestUtil.closeAllTcpSockets()
|
||||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||||
updateListAction.value = -1 // update all
|
//updateListAction.value = -1 // update all
|
||||||
|
|
||||||
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
val serversCopy = serversCache.toList() // Create a copy of the list
|
||||||
for (item in serversCache) {
|
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)
|
val testResult = SpeedtestUtil.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)
|
||||||
@@ -214,18 +203,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
updateListAction.value = -1 // update all
|
updateListAction.value = -1 // update all
|
||||||
|
|
||||||
val serversCopy = serversCache.toList() // Create a copy of the list
|
val serversCopy = serversCache.toList() // Create a copy of the list
|
||||||
|
|
||||||
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
|
||||||
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
|
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
|
||||||
for (item in serversCopy) {
|
for (item in serversCopy) {
|
||||||
val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
|
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
|
||||||
if (config.status) {
|
|
||||||
MessageUtil.sendMsg2TestService(
|
|
||||||
getApplication(),
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG,
|
|
||||||
Pair(item.guid, config.content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +217,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun subscriptionIdChanged(id: String) {
|
fun subscriptionIdChanged(id: String) {
|
||||||
if (subscriptionId != id) {
|
if (subscriptionId != id) {
|
||||||
subscriptionId = id
|
subscriptionId = id
|
||||||
MmkvManager.settingsStorage.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
|
MmkvManager.encodeSettings(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
|
||||||
reloadServerList()
|
reloadServerList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,7 +249,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeDuplicateServer(): Int {
|
fun removeDuplicateServer(): Int {
|
||||||
val serversCacheCopy = mutableListOf<Pair<String, ServerConfig>>()
|
val serversCacheCopy = mutableListOf<Pair<String, ProfileItem>>()
|
||||||
for (it in serversCache) {
|
for (it in serversCache) {
|
||||||
val config = MmkvManager.decodeServerConfig(it.guid) ?: continue
|
val config = MmkvManager.decodeServerConfig(it.guid) ?: continue
|
||||||
serversCacheCopy.add(Pair(it.guid, config))
|
serversCacheCopy.add(Pair(it.guid, config))
|
||||||
@@ -277,11 +257,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
val deleteServer = mutableListOf<String>()
|
val deleteServer = mutableListOf<String>()
|
||||||
serversCacheCopy.forEachIndexed { index, it ->
|
serversCacheCopy.forEachIndexed { index, it ->
|
||||||
val outbound = it.second.getProxyOutbound()
|
val outbound = it.second.getKeyProperty()
|
||||||
serversCacheCopy.forEachIndexed { index2, it2 ->
|
serversCacheCopy.forEachIndexed { index2, it2 ->
|
||||||
if (index2 > index) {
|
if (index2 > index) {
|
||||||
val outbound2 = it2.second.getProxyOutbound()
|
val outbound2 = it2.second.getKeyProperty()
|
||||||
if (outbound == outbound2 && !deleteServer.contains(it2.first)) {
|
if (outbound.equals(outbound2) && !deleteServer.contains(it2.first)) {
|
||||||
deleteServer.add(it2.first)
|
deleteServer.add(it2.first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +275,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllServer() {
|
fun removeAllServer() {
|
||||||
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
MmkvManager.removeAllServer()
|
MmkvManager.removeAllServer()
|
||||||
} else {
|
} else {
|
||||||
val serversCopy = serversCache.toList()
|
val serversCopy = serversCache.toList()
|
||||||
@@ -306,7 +286,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeInvalidServer() {
|
fun removeInvalidServer() {
|
||||||
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
MmkvManager.removeInvalidServer("")
|
MmkvManager.removeInvalidServer("")
|
||||||
} else {
|
} else {
|
||||||
val serversCopy = serversCache.toList()
|
val serversCopy = serversCache.toList()
|
||||||
@@ -317,33 +297,27 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sortByTestResults() {
|
fun sortByTestResults() {
|
||||||
MmkvManager.sortByTestResults()
|
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
||||||
|
|
||||||
|
val serverDelays = mutableListOf<ServerDelay>()
|
||||||
|
val serverList = MmkvManager.decodeServerList()
|
||||||
|
serverList.forEach { key ->
|
||||||
|
val delay = MmkvManager.decodeServerAffiliationInfo(key)?.testDelayMillis ?: 0L
|
||||||
|
serverDelays.add(ServerDelay(key, if (delay <= 0L) 999999 else delay))
|
||||||
|
}
|
||||||
|
serverDelays.sortBy { it.testDelayMillis }
|
||||||
|
|
||||||
|
serverDelays.forEach {
|
||||||
|
serverList.remove(it.guid)
|
||||||
|
serverList.add(it.guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
MmkvManager.encodeServerList(serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun initAssets(assets: AssetManager) {
|
||||||
fun copyAssets(assets: AssetManager) {
|
|
||||||
val extFolder = Utils.userAssetPath(getApplication<AngApplication>())
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
try {
|
SettingsManager.initAssets(getApplication<AngApplication>(), assets)
|
||||||
val geo = arrayOf("geosite.dat", "geoip.dat")
|
|
||||||
assets.list("")
|
|
||||||
?.filter { geo.contains(it) }
|
|
||||||
?.filter { !File(extFolder, it).exists() }
|
|
||||||
?.forEach {
|
|
||||||
val target = File(extFolder, it)
|
|
||||||
assets.open(it).use { input ->
|
|
||||||
FileOutputStream(target).use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.i(
|
|
||||||
ANG_PACKAGE,
|
|
||||||
"Copied from apk assets folder to ${target.absolutePath}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(ANG_PACKAGE, "asset copy failed", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +326,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
keywordFilter = keyword
|
keywordFilter = keyword
|
||||||
MmkvManager.settingsStorage.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
|
MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
|
||||||
reloadServerList()
|
reloadServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,15 +360,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
||||||
val resultPair: Pair<String, Long> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
|
||||||
intent.getSerializableExtra("content", Pair::class.java) as Pair<String, Long>
|
|
||||||
} else {
|
|
||||||
intent.getSerializableExtra("content") as Pair<String, Long>
|
|
||||||
}
|
|
||||||
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
||||||
updateListAction.value = getPosition(resultPair.first)
|
updateListAction.value = getPosition(resultPair.first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,13 @@ import android.content.SharedPreferences
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class SettingsViewModel(application: Application) : AndroidViewModel(application),
|
class SettingsViewModel(application: Application) : AndroidViewModel(application),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startListenPreferenceChange() {
|
fun startListenPreferenceChange() {
|
||||||
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
PreferenceManager.getDefaultSharedPreferences(getApplication())
|
||||||
.registerOnSharedPreferenceChangeListener(this)
|
.registerOnSharedPreferenceChangeListener(this)
|
||||||
@@ -47,20 +39,17 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
AppConfig.PREF_LANGUAGE,
|
AppConfig.PREF_LANGUAGE,
|
||||||
AppConfig.PREF_UI_MODE_NIGHT,
|
AppConfig.PREF_UI_MODE_NIGHT,
|
||||||
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
|
||||||
AppConfig.PREF_ROUTING_MODE,
|
|
||||||
AppConfig.PREF_V2RAY_ROUTING_AGENT,
|
|
||||||
AppConfig.PREF_V2RAY_ROUTING_BLOCKED,
|
|
||||||
AppConfig.PREF_V2RAY_ROUTING_DIRECT,
|
|
||||||
AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,
|
AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,
|
||||||
AppConfig.PREF_FRAGMENT_PACKETS,
|
AppConfig.PREF_FRAGMENT_PACKETS,
|
||||||
AppConfig.PREF_FRAGMENT_LENGTH,
|
AppConfig.PREF_FRAGMENT_LENGTH,
|
||||||
AppConfig.PREF_FRAGMENT_INTERVAL,
|
AppConfig.PREF_FRAGMENT_INTERVAL,
|
||||||
AppConfig.PREF_MUX_XUDP_QUIC,
|
AppConfig.PREF_MUX_XUDP_QUIC,
|
||||||
-> {
|
-> {
|
||||||
settingsStorage?.encode(key, sharedPreferences.getString(key, ""))
|
MmkvManager.encodeSettings(key, sharedPreferences.getString(key, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.PREF_ROUTE_ONLY_ENABLED,
|
AppConfig.PREF_ROUTE_ONLY_ENABLED,
|
||||||
|
AppConfig.PREF_IS_BOOTED,
|
||||||
AppConfig.PREF_SPEED_ENABLED,
|
AppConfig.PREF_SPEED_ENABLED,
|
||||||
AppConfig.PREF_PROXY_SHARING,
|
AppConfig.PREF_PROXY_SHARING,
|
||||||
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
AppConfig.PREF_LOCAL_DNS_ENABLED,
|
||||||
@@ -74,21 +63,21 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
AppConfig.SUBSCRIPTION_AUTO_UPDATE,
|
AppConfig.SUBSCRIPTION_AUTO_UPDATE,
|
||||||
AppConfig.PREF_FRAGMENT_ENABLED,
|
AppConfig.PREF_FRAGMENT_ENABLED,
|
||||||
AppConfig.PREF_MUX_ENABLED,
|
AppConfig.PREF_MUX_ENABLED,
|
||||||
-> {
|
-> {
|
||||||
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
|
MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.PREF_SNIFFING_ENABLED -> {
|
AppConfig.PREF_SNIFFING_ENABLED -> {
|
||||||
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true))
|
MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.PREF_MUX_CONCURRENCY,
|
AppConfig.PREF_MUX_CONCURRENCY,
|
||||||
AppConfig.PREF_MUX_XUDP_CONCURRENCY -> {
|
AppConfig.PREF_MUX_XUDP_CONCURRENCY -> {
|
||||||
settingsStorage?.encode(key, sharedPreferences.getString(key, "8"))
|
MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppConfig.PREF_PER_APP_PROXY_SET -> {
|
// AppConfig.PREF_PER_APP_PROXY_SET -> {
|
||||||
// settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf()))
|
// MmkvManager.encodeSettings(key, sharedPreferences.getStringSet(key, setOf()))
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
|
if (key == AppConfig.PREF_UI_MODE_NIGHT) {
|
||||||
|
|||||||
11
V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml
Normal file
11
V2rayNG/app/src/main/res/drawable-night/ic_lock_24dp.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M742.4,409.6L716.8,409.6L716.8,332.8C716.8,205.8 613.4,102.4 486.4,102.4S256,205.8 256,332.8L256,409.6h-25.6C188.1,409.6 153.6,444.1 153.6,486.4v409.6c0,42.3 34.5,76.8 76.8,76.8h512c42.3,0 76.8,-34.5 76.8,-76.8v-409.6c0,-42.3 -34.5,-76.8 -76.8,-76.8zM307.2,332.8C307.2,234 387.6,153.6 486.4,153.6S665.6,234 665.6,332.8L665.6,409.6L307.2,409.6L307.2,332.8zM768,896a25.6,25.6 0,0 1,-25.6 25.6h-512a25.6,25.6 0,0 1,-25.6 -25.6v-409.6a25.6,25.6 0,0 1,25.6 -25.6h512a25.6,25.6 0,0 1,25.6 25.6v409.6z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user