Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
9a9d315e62 | ||
|
|
c42aa93bf7 | ||
|
|
a7664f03aa | ||
|
|
fa341c9a5a | ||
|
|
a15ab4759e | ||
|
|
b8939763d4 | ||
|
|
51adca8568 | ||
|
|
f646eff048 | ||
|
|
fee0a016d8 | ||
|
|
f040fa5c08 | ||
|
|
7f3c6b4665 | ||
|
|
8b806fe0be | ||
|
|
b37d8c2369 | ||
|
|
b5451e9d3d | ||
|
|
b2235d4c38 | ||
|
|
c8ba5d727e | ||
|
|
a3de44cd0a |
@@ -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)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 582
|
versionCode = 599
|
||||||
versionName = "1.8.37"
|
versionName = "1.9.6"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
@@ -68,12 +68,9 @@ android {
|
|||||||
"universal"
|
"universal"
|
||||||
|
|
||||||
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
||||||
if(versionCodes.containsKey(abi))
|
if (versionCodes.containsKey(abi)) {
|
||||||
{
|
|
||||||
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +89,7 @@ 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)
|
||||||
|
|
||||||
implementation(libs.flexbox)
|
implementation(libs.flexbox)
|
||||||
@@ -130,7 +127,6 @@ dependencies {
|
|||||||
implementation(libs.language.json)
|
implementation(libs.language.json)
|
||||||
implementation(libs.quickie.bundled)
|
implementation(libs.quickie.bundled)
|
||||||
implementation(libs.core)
|
implementation(libs.core)
|
||||||
// Updating these 2 dependencies may cause some errors. Be careful.
|
|
||||||
implementation(libs.work.runtime.ktx)
|
implementation(libs.work.runtime.ktx)
|
||||||
implementation(libs.work.multiprocess)
|
implementation(libs.work.multiprocess)
|
||||||
}
|
}
|
||||||
@@ -5,21 +5,32 @@
|
|||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
android:smallScreens="true"
|
|
||||||
android:normalScreens="true"
|
|
||||||
android:largeScreens="true"
|
android:largeScreens="true"
|
||||||
android:xlargeScreens="true"/>
|
android:normalScreens="true"
|
||||||
|
android:smallScreens="true"
|
||||||
|
android:xlargeScreens="true" />
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="21" tools:overrideLibrary="com.blacksquircle.ui.editorkit"/>
|
<uses-sdk
|
||||||
|
android:minSdkVersion="21"
|
||||||
|
tools:overrideLibrary="com.blacksquircle.ui.editorkit" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
<uses-feature
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
android:name="android.hardware.camera"
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
android:required="false" />
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera.autofocus"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
<uses-permission
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@@ -31,15 +42,15 @@
|
|||||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||||
android:minSdkVersion="34" />
|
android:minSdkVersion="34" />
|
||||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AngApplication"
|
android:name=".AngApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android: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"
|
||||||
@@ -53,91 +64,97 @@
|
|||||||
android:theme="@style/AppThemeDayNight.NoActionBar">
|
android:theme="@style/AppThemeDayNight.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
|
||||||
android:name=".ui.ServerActivity"
|
android:name=".ui.ServerActivity"
|
||||||
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:exported="false"
|
|
||||||
android:name=".ui.ServerCustomConfigActivity"
|
android:name=".ui.ServerCustomConfigActivity"
|
||||||
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android: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" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="v2rayng"/>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:host="install-config"/>
|
|
||||||
<data android:host="install-sub"/>
|
<data android:scheme="v2rayng" />
|
||||||
|
<data android:host="install-config" />
|
||||||
|
<data android:host="install-sub" />
|
||||||
</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" />
|
||||||
@@ -150,43 +167,52 @@
|
|||||||
android:value="vpn" />
|
android:value="vpn" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".service.V2RayProxyOnlyService"
|
<service
|
||||||
android:exported="false"
|
android:name=".service.V2RayProxyOnlyService"
|
||||||
android:label="@string/app_name"
|
android:exported="false"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:label="@string/app_name"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<property
|
<property
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
android:value="proxy" />
|
android:value="proxy" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".service.V2RayTestService"
|
<service
|
||||||
|
android:name=".service.V2RayTestService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon" />
|
||||||
</service>
|
|
||||||
|
|
||||||
<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"
|
||||||
android:resource="@xml/app_widget_provider" />
|
android:resource="@xml/app_widget_provider" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
<action android:name="com.v2ray.ang.action.widget.click" />
|
<action android:name="com.v2ray.ang.action.widget.click" />
|
||||||
<action android:name="com.v2ray.ang.action.activity" />
|
<action android:name="com.v2ray.ang.action.activity" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.BootReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="BootReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:exported="true"
|
android:name=".service.QSTileService"
|
||||||
android:name=".service.QSTileService"
|
android:exported="true"
|
||||||
android:icon="@drawable/ic_stat_name"
|
android:foregroundServiceType="specialUse"
|
||||||
android:label="@string/app_tile_name"
|
android:icon="@drawable/ic_stat_name"
|
||||||
android:foregroundServiceType="specialUse"
|
android:label="@string/app_tile_name"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -196,8 +222,8 @@
|
|||||||
</service>
|
</service>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<activity
|
<activity
|
||||||
android:exported="true"
|
|
||||||
android:name=".ui.TaskerActivity"
|
android:name=".ui.TaskerActivity"
|
||||||
|
android:exported="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -206,9 +232,9 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:exported="true"
|
|
||||||
android:name=".receiver.TaskerReceiver"
|
android:name=".receiver.TaskerReceiver"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:exported="true"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -234,7 +260,7 @@
|
|||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/cache_paths"/>
|
android:resource="@xml/cache_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
73
V2rayNG/app/src/main/assets/custom_routing_black
Normal file
73
V2rayNG/app/src/main/assets/custom_routing_black
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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": "代理GFW",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"geosite:gfw",
|
||||||
|
"geosite:greatfire"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理Google等",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"ip": [
|
||||||
|
"1.0.0.1",
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8",
|
||||||
|
"8.8.4.4",
|
||||||
|
"geoip:facebook",
|
||||||
|
"geoip:fastly",
|
||||||
|
"geoip:google",
|
||||||
|
"geoip:netflix",
|
||||||
|
"geoip:telegram",
|
||||||
|
"geoip:twitter"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
|
||||||
81
V2rayNG/app/src/main/assets/custom_routing_white
Normal file
81
V2rayNG/app/src/main/assets/custom_routing_white
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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": "绕过中国域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"domain:dns.alidns.com",
|
||||||
|
"domain:doh.pub",
|
||||||
|
"domain:dot.pub",
|
||||||
|
"domain:doh.360.cn",
|
||||||
|
"domain:dot.360.cn",
|
||||||
|
"geosite:cn",
|
||||||
|
"geosite:geolocation-cn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"223.5.5.5/32",
|
||||||
|
"223.6.6.6/32",
|
||||||
|
"2400:3200::1/128",
|
||||||
|
"2400:3200:baba::1/128",
|
||||||
|
"119.29.29.29/32",
|
||||||
|
"1.12.12.12/32",
|
||||||
|
"120.53.53.53/32",
|
||||||
|
"2402:4e00::/128",
|
||||||
|
"2402:4e00:1::/128",
|
||||||
|
"180.76.76.76/32",
|
||||||
|
"2400:da00::6666/128",
|
||||||
|
"114.114.114.114/32",
|
||||||
|
"114.114.115.115/32",
|
||||||
|
"180.184.1.1/32",
|
||||||
|
"180.184.2.2/32",
|
||||||
|
"101.226.4.6/32",
|
||||||
|
"218.30.118.6/32",
|
||||||
|
"123.125.81.6/32",
|
||||||
|
"140.207.198.6/32",
|
||||||
|
"geoip:cn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "最终代理",
|
||||||
|
"port": "0-65535",
|
||||||
|
"outboundTag": "proxy"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
|
||||||
@@ -36,7 +36,6 @@ public interface ItemTouchHelperAdapter {
|
|||||||
* @param fromPosition The start position of the moved item.
|
* @param fromPosition The start position of the moved item.
|
||||||
* @param toPosition Then resolved position of the moved item.
|
* @param toPosition Then resolved position of the moved item.
|
||||||
* @return True if the item was moved to the new adapter position.
|
* @return True if the item was moved to the new adapter position.
|
||||||
*
|
|
||||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||||
*/
|
*/
|
||||||
@@ -52,7 +51,6 @@ public interface ItemTouchHelperAdapter {
|
|||||||
* adjusting the underlying data to reflect this removal.
|
* adjusting the underlying data to reflect this removal.
|
||||||
*
|
*
|
||||||
* @param position The position of the item dismissed.
|
* @param position The position of the item dismissed.
|
||||||
*
|
|
||||||
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
|
||||||
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
* @see RecyclerView.ViewHolder#getAdapterPosition()
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -17,9 +17,10 @@
|
|||||||
package com.v2ray.ang.helper;
|
package com.v2ray.ang.helper;
|
||||||
|
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
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 com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.util.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class AngApplication : MultiDexApplication(), Configuration.Provider {
|
class AngApplication : MultiDexApplication() {
|
||||||
companion object {
|
companion object {
|
||||||
//const val PREF_LAST_VERSION = "pref_last_version"
|
//const val PREF_LAST_VERSION = "pref_last_version"
|
||||||
lateinit var application: AngApplication
|
lateinit var application: AngApplication
|
||||||
@@ -17,6 +21,10 @@ class AngApplication : MultiDexApplication(), Configuration.Provider {
|
|||||||
application = this
|
application = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val workManagerConfiguration: Configuration = Configuration.Builder()
|
||||||
|
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
||||||
|
.build()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
@@ -27,15 +35,18 @@ class AngApplication : MultiDexApplication(), Configuration.Provider {
|
|||||||
// 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
|
||||||
|
WorkManager.initialize(this, workManagerConfiguration)
|
||||||
|
|
||||||
|
SettingsManager.initRoutingRulesets(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWorkManagerConfiguration(): Configuration {
|
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(
|
||||||
return Configuration.Builder()
|
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
|
||||||
.build()
|
)!!
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +48,12 @@ 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"
|
||||||
@@ -109,6 +105,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 +117,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,15 +138,24 @@ 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"
|
||||||
|
|
||||||
/** Protocols Scheme **/
|
/** Protocols Scheme **/
|
||||||
const val VMESS = "vmess://"
|
const val VMESS = "vmess://"
|
||||||
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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package com.v2ray.ang.dto
|
|||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
|
||||||
data class AppInfo(val appName: String,
|
data class AppInfo(
|
||||||
val packageName: String,
|
val appName: String,
|
||||||
val appIcon: Drawable,
|
val packageName: String,
|
||||||
val isSystemApp: Boolean,
|
val appIcon: Drawable,
|
||||||
var isSelected: Int)
|
val isSystemApp: Boolean,
|
||||||
|
var isSelected: Int
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|
||||||
@@ -6,11 +6,14 @@ import com.v2ray.ang.AppConfig
|
|||||||
enum class EConfigType(val value: Int, val protocolScheme: String) {
|
enum class EConfigType(val value: Int, val protocolScheme: String) {
|
||||||
VMESS(1, AppConfig.VMESS),
|
VMESS(1, AppConfig.VMESS),
|
||||||
CUSTOM(2, AppConfig.CUSTOM),
|
CUSTOM(2, AppConfig.CUSTOM),
|
||||||
SHADOWSOCKS(3,AppConfig.SHADOWSOCKS),
|
SHADOWSOCKS(3, AppConfig.SHADOWSOCKS),
|
||||||
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) = values().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,29 @@
|
|||||||
|
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,
|
||||||
|
) {
|
||||||
|
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?,
|
||||||
|
)
|
||||||
|
}
|
||||||
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,
|
||||||
|
)
|
||||||
@@ -1,50 +1,68 @@
|
|||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
|
||||||
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.util.Utils
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
|
|
||||||
data class ServerConfig(
|
data class ServerConfig(
|
||||||
val configVersion: Int = 3,
|
val configVersion: Int = 3,
|
||||||
val configType: EConfigType,
|
val configType: EConfigType,
|
||||||
var subscriptionId: String = "",
|
var subscriptionId: String = "",
|
||||||
val addedTime: Long = System.currentTimeMillis(),
|
val addedTime: Long = System.currentTimeMillis(),
|
||||||
var remarks: String = "",
|
var remarks: String = "",
|
||||||
val outboundBean: V2rayConfig.OutboundBean? = null,
|
val outboundBean: V2rayConfig.OutboundBean? = null,
|
||||||
var fullConfig: V2rayConfig? = null
|
var fullConfig: V2rayConfig? = null
|
||||||
) {
|
) {
|
||||||
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(
|
||||||
protocol = configType.name.lowercase(),
|
protocol = configType.name.lowercase(),
|
||||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||||
vnext = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
vnext = listOf(
|
||||||
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())))),
|
V2rayConfig.OutboundBean.OutSettingsBean.VnextBean(
|
||||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
users = listOf(V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
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(
|
||||||
protocol = configType.name.lowercase(),
|
protocol = configType.name.lowercase(),
|
||||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||||
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())),
|
servers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.ServersBean())
|
||||||
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()))
|
),
|
||||||
|
streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
EConfigType.WIREGUARD ->
|
EConfigType.WIREGUARD ->
|
||||||
return ServerConfig(
|
return ServerConfig(
|
||||||
configType = configType,
|
configType = configType,
|
||||||
outboundBean = V2rayConfig.OutboundBean(
|
outboundBean = V2rayConfig.OutboundBean(
|
||||||
protocol = configType.name.lowercase(),
|
protocol = configType.name.lowercase(),
|
||||||
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
settings = V2rayConfig.OutboundBean.OutSettingsBean(
|
||||||
secretKey = "",
|
secretKey = "",
|
||||||
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
|
peers = listOf(V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean())
|
||||||
)))
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
data class ServersCache(val guid: String,
|
data class ServersCache(
|
||||||
val profile: ProfileItem)
|
val guid: String,
|
||||||
|
val profile: ProfileItem
|
||||||
|
)
|
||||||
@@ -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,24 +7,26 @@ 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.util.Utils
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
data class V2rayConfig(
|
data class V2rayConfig(
|
||||||
var remarks: String? = null,
|
var remarks: String? = null,
|
||||||
var stats: Any? = null,
|
var stats: Any? = null,
|
||||||
val log: LogBean,
|
val log: LogBean,
|
||||||
var policy: PolicyBean?,
|
var policy: PolicyBean?,
|
||||||
val inbounds: ArrayList<InboundBean>,
|
val inbounds: ArrayList<InboundBean>,
|
||||||
var outbounds: ArrayList<OutboundBean>,
|
var outbounds: ArrayList<OutboundBean>,
|
||||||
var dns: DnsBean,
|
var dns: DnsBean,
|
||||||
val routing: RoutingBean,
|
val routing: RoutingBean,
|
||||||
val api: Any? = null,
|
val api: Any? = null,
|
||||||
val transport: Any? = null,
|
val transport: Any? = null,
|
||||||
val reverse: Any? = null,
|
val reverse: Any? = null,
|
||||||
var fakedns: Any? = null,
|
var fakedns: Any? = null,
|
||||||
val browserForwarder: Any? = null,
|
val browserForwarder: Any? = null,
|
||||||
var observatory: Any? = null,
|
var observatory: Any? = null,
|
||||||
var burstObservatory: Any? = null) {
|
var burstObservatory: Any? = null
|
||||||
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_PORT = 443
|
const val DEFAULT_PORT = 443
|
||||||
const val DEFAULT_SECURITY = "auto"
|
const val DEFAULT_SECURITY = "auto"
|
||||||
@@ -36,210 +38,280 @@ data class V2rayConfig(
|
|||||||
const val HTTP = "http"
|
const val HTTP = "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
data class LogBean(val access: String,
|
data class LogBean(
|
||||||
val error: String,
|
val access: String,
|
||||||
var loglevel: String?,
|
val error: String,
|
||||||
val dnsLog: Boolean? = null)
|
var loglevel: String?,
|
||||||
|
val dnsLog: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class InboundBean(
|
data class InboundBean(
|
||||||
var tag: String,
|
var tag: String,
|
||||||
var port: Int,
|
var port: Int,
|
||||||
var protocol: String,
|
var protocol: String,
|
||||||
var listen: String? = null,
|
var listen: String? = null,
|
||||||
val settings: Any? = null,
|
val settings: Any? = null,
|
||||||
val sniffing: SniffingBean?,
|
val sniffing: SniffingBean?,
|
||||||
val streamSettings: Any? = null,
|
val streamSettings: Any? = null,
|
||||||
val allocate: Any? = null) {
|
val allocate: Any? = null
|
||||||
|
) {
|
||||||
|
|
||||||
data class InSettingsBean(val auth: String? = null,
|
data class InSettingsBean(
|
||||||
val udp: Boolean? = null,
|
val auth: String? = null,
|
||||||
val userLevel: Int? = null,
|
val udp: Boolean? = null,
|
||||||
val address: String? = null,
|
val userLevel: Int? = null,
|
||||||
val port: Int? = null,
|
val address: String? = null,
|
||||||
val network: String? = null)
|
val port: Int? = null,
|
||||||
|
val network: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class SniffingBean(var enabled: Boolean,
|
data class SniffingBean(
|
||||||
val destOverride: ArrayList<String>,
|
var enabled: Boolean,
|
||||||
val metadataOnly: Boolean? = null,
|
val destOverride: ArrayList<String>,
|
||||||
var routeOnly: Boolean? = null)
|
val metadataOnly: Boolean? = null,
|
||||||
|
var routeOnly: Boolean? = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class OutboundBean(var tag: String = "proxy",
|
data class OutboundBean(
|
||||||
var protocol: String,
|
var tag: String = "proxy",
|
||||||
var settings: OutSettingsBean? = null,
|
var protocol: String,
|
||||||
var streamSettings: StreamSettingsBean? = null,
|
var settings: OutSettingsBean? = null,
|
||||||
val proxySettings: Any? = null,
|
var streamSettings: StreamSettingsBean? = null,
|
||||||
val sendThrough: String? = null,
|
val proxySettings: Any? = null,
|
||||||
var mux: MuxBean? = MuxBean(false)) {
|
val sendThrough: String? = null,
|
||||||
|
var mux: MuxBean? = MuxBean(false)
|
||||||
|
) {
|
||||||
|
|
||||||
data class OutSettingsBean(var vnext: List<VnextBean>? = null,
|
data class OutSettingsBean(
|
||||||
var fragment: FragmentBean? = null,
|
var vnext: List<VnextBean>? = null,
|
||||||
var servers: List<ServersBean>? = null,
|
var fragment: FragmentBean? = null,
|
||||||
/*Blackhole*/
|
var noises: List<NoiseBean>? = null,
|
||||||
var response: Response? = null,
|
var servers: List<ServersBean>? = null,
|
||||||
/*DNS*/
|
/*Blackhole*/
|
||||||
val network: String? = null,
|
var response: Response? = null,
|
||||||
var address: Any? = null,
|
/*DNS*/
|
||||||
val port: Int? = null,
|
val network: String? = null,
|
||||||
/*Freedom*/
|
var address: Any? = null,
|
||||||
var domainStrategy: String? = null,
|
val port: Int? = null,
|
||||||
val redirect: String? = null,
|
/*Freedom*/
|
||||||
val userLevel: Int? = null,
|
var domainStrategy: String? = null,
|
||||||
/*Loopback*/
|
val redirect: String? = null,
|
||||||
val inboundTag: String? = null,
|
val userLevel: Int? = null,
|
||||||
/*Wireguard*/
|
/*Loopback*/
|
||||||
var secretKey: String? = null,
|
val inboundTag: String? = null,
|
||||||
val peers: List<WireGuardBean>? = null,
|
/*Wireguard*/
|
||||||
var reserved: List<Int>? = null,
|
var secretKey: String? = null,
|
||||||
var mtu :Int? = null
|
val peers: List<WireGuardBean>? = null,
|
||||||
|
var reserved: List<Int>? = null,
|
||||||
|
var mtu: Int? = null,
|
||||||
|
var obfsPassword: String? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class VnextBean(var address: String = "",
|
data class VnextBean(
|
||||||
var port: Int = DEFAULT_PORT,
|
var address: String = "",
|
||||||
var users: List<UsersBean>) {
|
var port: Int = DEFAULT_PORT,
|
||||||
|
var users: List<UsersBean>
|
||||||
|
) {
|
||||||
|
|
||||||
data class UsersBean(var id: String = "",
|
data class UsersBean(
|
||||||
var alterId: Int? = null,
|
var id: String = "",
|
||||||
var security: String = DEFAULT_SECURITY,
|
var alterId: Int? = null,
|
||||||
var level: Int = DEFAULT_LEVEL,
|
var security: String = DEFAULT_SECURITY,
|
||||||
var encryption: String = "",
|
var level: Int = DEFAULT_LEVEL,
|
||||||
var flow: String = "")
|
var encryption: String = "",
|
||||||
|
var flow: String = ""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FragmentBean(var packets: String? = null,
|
data class FragmentBean(
|
||||||
var length: String? = null,
|
var packets: String? = null,
|
||||||
var interval: String? = null)
|
var length: String? = null,
|
||||||
|
var interval: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class ServersBean(var address: String = "",
|
data class NoiseBean(
|
||||||
var method: String = "chacha20-poly1305",
|
var type: String? = null,
|
||||||
var ota: Boolean = false,
|
var packet: String? = null,
|
||||||
var password: String = "",
|
var delay: String? = null
|
||||||
var port: Int = DEFAULT_PORT,
|
)
|
||||||
var level: Int = DEFAULT_LEVEL,
|
|
||||||
val email: String? = null,
|
|
||||||
var flow: String? = null,
|
|
||||||
val ivCheck: Boolean? = null,
|
|
||||||
var users: List<SocksUsersBean>? = null) {
|
|
||||||
|
|
||||||
|
data class ServersBean(
|
||||||
data class SocksUsersBean(var user: String = "",
|
var address: String = "",
|
||||||
var pass: String = "",
|
var method: String? = null,
|
||||||
var level: Int = DEFAULT_LEVEL)
|
var ota: Boolean = false,
|
||||||
|
var password: String? = null,
|
||||||
|
var port: Int = DEFAULT_PORT,
|
||||||
|
var level: Int = DEFAULT_LEVEL,
|
||||||
|
val email: String? = null,
|
||||||
|
var flow: String? = null,
|
||||||
|
val ivCheck: Boolean? = null,
|
||||||
|
var users: List<SocksUsersBean>? = null
|
||||||
|
) {
|
||||||
|
data class SocksUsersBean(
|
||||||
|
var user: String = "",
|
||||||
|
var pass: String = "",
|
||||||
|
var level: Int = DEFAULT_LEVEL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Response(var type: String)
|
data class Response(var type: String)
|
||||||
|
|
||||||
data class WireGuardBean(var publicKey: String = "",
|
data class WireGuardBean(
|
||||||
var endpoint: String = "")
|
var publicKey: String = "",
|
||||||
|
var endpoint: String = ""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class StreamSettingsBean(var network: String = DEFAULT_NETWORK,
|
data class StreamSettingsBean(
|
||||||
var security: String = "",
|
var network: String = DEFAULT_NETWORK,
|
||||||
var tcpSettings: TcpSettingsBean? = null,
|
var security: String = "",
|
||||||
var kcpSettings: KcpSettingsBean? = null,
|
var tcpSettings: TcpSettingsBean? = null,
|
||||||
var wsSettings: WsSettingsBean? = null,
|
var kcpSettings: KcpSettingsBean? = null,
|
||||||
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
var wsSettings: WsSettingsBean? = null,
|
||||||
var splithttpSettings: SplithttpSettingsBean? = null,
|
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
||||||
var httpSettings: HttpSettingsBean? = null,
|
var splithttpSettings: SplithttpSettingsBean? = null,
|
||||||
var tlsSettings: TlsSettingsBean? = null,
|
var httpSettings: HttpSettingsBean? = null,
|
||||||
var quicSettings: QuicSettingBean? = null,
|
var tlsSettings: TlsSettingsBean? = null,
|
||||||
var realitySettings: TlsSettingsBean? = null,
|
var quicSettings: QuicSettingBean? = null,
|
||||||
var grpcSettings: GrpcSettingsBean? = null,
|
var realitySettings: TlsSettingsBean? = null,
|
||||||
val dsSettings: Any? = null,
|
var grpcSettings: GrpcSettingsBean? = null,
|
||||||
var sockopt: SockoptBean? = null
|
var hy2steriaSettings: Hy2steriaSettingsBean? = null,
|
||||||
|
val dsSettings: Any? = null,
|
||||||
|
var sockopt: SockoptBean? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class TcpSettingsBean(var header: HeaderBean = HeaderBean(),
|
data class TcpSettingsBean(
|
||||||
val acceptProxyProtocol: Boolean? = null) {
|
var header: HeaderBean = HeaderBean(),
|
||||||
data class HeaderBean(var type: String = "none",
|
val acceptProxyProtocol: Boolean? = null
|
||||||
var request: RequestBean? = null,
|
) {
|
||||||
var response: Any? = null) {
|
data class HeaderBean(
|
||||||
data class RequestBean(var path: List<String> = ArrayList(),
|
var type: String = "none",
|
||||||
var headers: HeadersBean = HeadersBean(),
|
var request: RequestBean? = null,
|
||||||
val version: String? = null,
|
var response: Any? = null
|
||||||
val method: String? = null) {
|
) {
|
||||||
data class HeadersBean(var Host: List<String>? = ArrayList(),
|
data class RequestBean(
|
||||||
@SerializedName("User-Agent")
|
var path: List<String> = ArrayList(),
|
||||||
val userAgent: List<String>? = null,
|
var headers: HeadersBean = HeadersBean(),
|
||||||
@SerializedName("Accept-Encoding")
|
val version: String? = null,
|
||||||
val acceptEncoding: List<String>? = null,
|
val method: String? = null
|
||||||
val Connection: List<String>? = null,
|
) {
|
||||||
val Pragma: String? = null)
|
data class HeadersBean(
|
||||||
|
var Host: List<String>? = ArrayList(),
|
||||||
|
@SerializedName("User-Agent")
|
||||||
|
val userAgent: List<String>? = null,
|
||||||
|
@SerializedName("Accept-Encoding")
|
||||||
|
val acceptEncoding: List<String>? = null,
|
||||||
|
val Connection: List<String>? = null,
|
||||||
|
val Pragma: String? = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class KcpSettingsBean(var mtu: Int = 1350,
|
data class KcpSettingsBean(
|
||||||
var tti: Int = 50,
|
var mtu: Int = 1350,
|
||||||
var uplinkCapacity: Int = 12,
|
var tti: Int = 50,
|
||||||
var downlinkCapacity: Int = 100,
|
var uplinkCapacity: Int = 12,
|
||||||
var congestion: Boolean = false,
|
var downlinkCapacity: Int = 100,
|
||||||
var readBufferSize: Int = 1,
|
var congestion: Boolean = false,
|
||||||
var writeBufferSize: Int = 1,
|
var readBufferSize: Int = 1,
|
||||||
var header: HeaderBean = HeaderBean(),
|
var writeBufferSize: Int = 1,
|
||||||
var seed: String? = null) {
|
var header: HeaderBean = HeaderBean(),
|
||||||
|
var seed: String? = null
|
||||||
|
) {
|
||||||
data class HeaderBean(var type: String = "none")
|
data class HeaderBean(var type: String = "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class WsSettingsBean(var path: String = "",
|
data class WsSettingsBean(
|
||||||
var headers: HeadersBean = HeadersBean(),
|
var path: String = "",
|
||||||
val maxEarlyData: Int? = null,
|
var headers: HeadersBean = HeadersBean(),
|
||||||
val useBrowserForwarding: Boolean? = null,
|
val maxEarlyData: Int? = null,
|
||||||
val acceptProxyProtocol: Boolean? = null) {
|
val useBrowserForwarding: Boolean? = null,
|
||||||
|
val acceptProxyProtocol: Boolean? = null
|
||||||
|
) {
|
||||||
data class HeadersBean(var Host: String = "")
|
data class HeadersBean(var Host: String = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class HttpupgradeSettingsBean(var path: String = "",
|
data class HttpupgradeSettingsBean(
|
||||||
var host: String = "",
|
var path: String = "",
|
||||||
val acceptProxyProtocol: Boolean? = null)
|
var host: String = "",
|
||||||
|
val acceptProxyProtocol: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class SplithttpSettingsBean(var path: String = "",
|
data class SplithttpSettingsBean(
|
||||||
var host: String = "",
|
var path: String = "",
|
||||||
val maxUploadSize: Int? = null,
|
var host: String = "",
|
||||||
val maxConcurrentUploads: Int? = null)
|
val maxUploadSize: Int? = null,
|
||||||
data class HttpSettingsBean(var host: List<String> = ArrayList(),
|
val maxConcurrentUploads: Int? = null
|
||||||
var path: String = "")
|
)
|
||||||
|
|
||||||
data class SockoptBean(var TcpNoDelay: Boolean? = null,
|
data class HttpSettingsBean(
|
||||||
var tcpKeepAliveIdle: Int? = null,
|
var host: List<String> = ArrayList(),
|
||||||
var tcpFastOpen: Boolean? = null,
|
var path: String = ""
|
||||||
var tproxy: String? = null,
|
)
|
||||||
var mark: Int? = null,
|
|
||||||
var dialerProxy: String? = null)
|
|
||||||
|
|
||||||
data class TlsSettingsBean(var allowInsecure: Boolean = false,
|
data class SockoptBean(
|
||||||
var serverName: String = "",
|
var TcpNoDelay: Boolean? = null,
|
||||||
val alpn: List<String>? = null,
|
var tcpKeepAliveIdle: Int? = null,
|
||||||
val minVersion: String? = null,
|
var tcpFastOpen: Boolean? = null,
|
||||||
val maxVersion: String? = null,
|
var tproxy: String? = null,
|
||||||
val preferServerCipherSuites: Boolean? = null,
|
var mark: Int? = null,
|
||||||
val cipherSuites: String? = null,
|
var dialerProxy: String? = null
|
||||||
val fingerprint: String? = null,
|
)
|
||||||
val certificates: List<Any>? = null,
|
|
||||||
val disableSystemRoot: Boolean? = null,
|
|
||||||
val enableSessionResumption: Boolean? = null,
|
|
||||||
// REALITY settings
|
|
||||||
val show: Boolean = false,
|
|
||||||
var publicKey: String? = null,
|
|
||||||
var shortId: String? = null,
|
|
||||||
var spiderX: String? = null)
|
|
||||||
|
|
||||||
data class QuicSettingBean(var security: String = "none",
|
data class TlsSettingsBean(
|
||||||
var key: String = "",
|
var allowInsecure: Boolean = false,
|
||||||
var header: HeaderBean = HeaderBean()) {
|
var serverName: String = "",
|
||||||
|
val alpn: List<String>? = null,
|
||||||
|
val minVersion: String? = null,
|
||||||
|
val maxVersion: String? = null,
|
||||||
|
val preferServerCipherSuites: Boolean? = null,
|
||||||
|
val cipherSuites: String? = null,
|
||||||
|
val fingerprint: String? = null,
|
||||||
|
val certificates: List<Any>? = null,
|
||||||
|
val disableSystemRoot: Boolean? = null,
|
||||||
|
val enableSessionResumption: Boolean? = null,
|
||||||
|
// REALITY settings
|
||||||
|
val show: Boolean = false,
|
||||||
|
var publicKey: String? = null,
|
||||||
|
var shortId: String? = null,
|
||||||
|
var spiderX: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class QuicSettingBean(
|
||||||
|
var security: String = "none",
|
||||||
|
var key: String = "",
|
||||||
|
var header: HeaderBean = HeaderBean()
|
||||||
|
) {
|
||||||
data class HeaderBean(var type: String = "none")
|
data class HeaderBean(var type: String = "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GrpcSettingsBean(var serviceName: String = "",
|
data class GrpcSettingsBean(
|
||||||
var authority: String? = null,
|
var serviceName: String = "",
|
||||||
var multiMode: Boolean? = null,
|
var authority: String? = null,
|
||||||
var idle_timeout: Int? = null,
|
var multiMode: Boolean? = null,
|
||||||
var health_check_timeout: Int? = null
|
var idle_timeout: Int? = null,
|
||||||
)
|
var health_check_timeout: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
fun populateTransportSettings(transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
data class Hy2steriaSettingsBean(
|
||||||
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
var password: String? = null,
|
||||||
authority: String?): String {
|
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(
|
||||||
|
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
|
||||||
|
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
|
||||||
|
authority: String?
|
||||||
|
): String {
|
||||||
var sni = ""
|
var sni = ""
|
||||||
network = transport
|
network = transport
|
||||||
when (network) {
|
when (network) {
|
||||||
@@ -249,17 +321,18 @@ data class V2rayConfig(
|
|||||||
tcpSetting.header.type = HTTP
|
tcpSetting.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 ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
requestObj.headers.Host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
requestObj.path = (path ?: "").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
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tcpSetting.header.type = "none"
|
tcpSetting.header.type = "none"
|
||||||
sni = host ?: ""
|
sni = host.orEmpty()
|
||||||
}
|
}
|
||||||
tcpSettings = tcpSetting
|
tcpSettings = tcpSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"kcp" -> {
|
"kcp" -> {
|
||||||
val kcpsetting = KcpSettingsBean()
|
val kcpsetting = KcpSettingsBean()
|
||||||
kcpsetting.header.type = headerType ?: "none"
|
kcpsetting.header.type = headerType ?: "none"
|
||||||
@@ -270,67 +343,75 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
kcpSettings = kcpsetting
|
kcpSettings = kcpsetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"ws" -> {
|
"ws" -> {
|
||||||
val wssetting = WsSettingsBean()
|
val wssetting = WsSettingsBean()
|
||||||
wssetting.headers.Host = host ?: ""
|
wssetting.headers.Host = host.orEmpty()
|
||||||
sni = wssetting.headers.Host
|
sni = wssetting.headers.Host
|
||||||
wssetting.path = path ?: "/"
|
wssetting.path = path ?: "/"
|
||||||
wsSettings = wssetting
|
wsSettings = wssetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"httpupgrade" -> {
|
"httpupgrade" -> {
|
||||||
val httpupgradeSetting = HttpupgradeSettingsBean()
|
val httpupgradeSetting = HttpupgradeSettingsBean()
|
||||||
httpupgradeSetting.host = host ?: ""
|
httpupgradeSetting.host = host.orEmpty()
|
||||||
sni = httpupgradeSetting.host
|
sni = httpupgradeSetting.host
|
||||||
httpupgradeSetting.path = path ?: "/"
|
httpupgradeSetting.path = path ?: "/"
|
||||||
httpupgradeSettings = httpupgradeSetting
|
httpupgradeSettings = httpupgradeSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"splithttp" -> {
|
"splithttp" -> {
|
||||||
val splithttpSetting = SplithttpSettingsBean()
|
val splithttpSetting = SplithttpSettingsBean()
|
||||||
splithttpSetting.host = host ?: ""
|
splithttpSetting.host = host.orEmpty()
|
||||||
sni = splithttpSetting.host
|
sni = splithttpSetting.host
|
||||||
splithttpSetting.path = path ?: "/"
|
splithttpSetting.path = path ?: "/"
|
||||||
splithttpSettings = splithttpSetting
|
splithttpSettings = splithttpSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"h2", "http" -> {
|
"h2", "http" -> {
|
||||||
network = "h2"
|
network = "h2"
|
||||||
val h2Setting = HttpSettingsBean()
|
val h2Setting = HttpSettingsBean()
|
||||||
h2Setting.host = (host ?: "").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
|
||||||
}
|
}
|
||||||
|
|
||||||
"quic" -> {
|
"quic" -> {
|
||||||
val quicsetting = QuicSettingBean()
|
val quicsetting = QuicSettingBean()
|
||||||
quicsetting.security = quicSecurity ?: "none"
|
quicsetting.security = quicSecurity ?: "none"
|
||||||
quicsetting.key = key ?: ""
|
quicsetting.key = key.orEmpty()
|
||||||
quicsetting.header.type = headerType ?: "none"
|
quicsetting.header.type = headerType ?: "none"
|
||||||
quicSettings = quicsetting
|
quicSettings = quicsetting
|
||||||
}
|
}
|
||||||
|
|
||||||
"grpc" -> {
|
"grpc" -> {
|
||||||
val grpcSetting = GrpcSettingsBean()
|
val grpcSetting = GrpcSettingsBean()
|
||||||
grpcSetting.multiMode = mode == "multi"
|
grpcSetting.multiMode = mode == "multi"
|
||||||
grpcSetting.serviceName = serviceName ?: ""
|
grpcSetting.serviceName = serviceName.orEmpty()
|
||||||
grpcSetting.authority = authority ?: ""
|
grpcSetting.authority = authority.orEmpty()
|
||||||
grpcSetting.idle_timeout = 60
|
grpcSetting.idle_timeout = 60
|
||||||
grpcSetting.health_check_timeout = 20
|
grpcSetting.health_check_timeout = 20
|
||||||
sni = authority ?: ""
|
sni = authority.orEmpty()
|
||||||
grpcSettings = grpcSetting
|
grpcSettings = grpcSetting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sni
|
return sni
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateTlsSettings(streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
|
fun populateTlsSettings(
|
||||||
publicKey: String?, shortId: String?, spiderX: String?) {
|
streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
|
||||||
|
publicKey: String?, shortId: String?, spiderX: String?
|
||||||
|
) {
|
||||||
security = streamSecurity
|
security = streamSecurity
|
||||||
val tlsSetting = TlsSettingsBean(
|
val tlsSetting = TlsSettingsBean(
|
||||||
allowInsecure = allowInsecure,
|
allowInsecure = allowInsecure,
|
||||||
serverName = sni,
|
serverName = sni,
|
||||||
fingerprint = fingerprint,
|
fingerprint = fingerprint,
|
||||||
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
shortId = shortId,
|
shortId = shortId,
|
||||||
spiderX = spiderX
|
spiderX = spiderX
|
||||||
)
|
)
|
||||||
if (security == TLS) {
|
if (security == TLS) {
|
||||||
tlsSettings = tlsSetting
|
tlsSettings = tlsSetting
|
||||||
@@ -342,18 +423,24 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MuxBean(var enabled: Boolean,
|
data class MuxBean(
|
||||||
var concurrency: Int = 8,
|
var enabled: Boolean,
|
||||||
var xudpConcurrency: Int = 8,
|
var concurrency: Int = 8,
|
||||||
var xudpProxyUDP443: String = "",)
|
var xudpConcurrency: Int = 8,
|
||||||
|
var xudpProxyUDP443: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
fun getServerAddress(): String? {
|
fun getServerAddress(): String? {
|
||||||
if (protocol.equals(EConfigType.VMESS.name, true)
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|| protocol.equals(EConfigType.VLESS.name, true)) {
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
|
) {
|
||||||
return settings?.vnext?.get(0)?.address
|
return settings?.vnext?.get(0)?.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.TROJAN.name, true)) {
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
|
) {
|
||||||
return settings?.servers?.get(0)?.address
|
return settings?.servers?.get(0)?.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?.get(0)?.endpoint?.substringBeforeLast(":")
|
||||||
@@ -363,11 +450,15 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
fun getServerPort(): Int? {
|
fun getServerPort(): Int? {
|
||||||
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?.get(0)?.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.TROJAN.name, true)) {
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
|
) {
|
||||||
return settings?.servers?.get(0)?.port
|
return settings?.servers?.get(0)?.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?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
|
||||||
@@ -375,14 +466,25 @@ data class V2rayConfig(
|
|||||||
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?.get(0)?.users?.get(0)?.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?.get(0)?.password
|
||||||
} else if (protocol.equals(EConfigType.SOCKS.name, true)) {
|
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|
) {
|
||||||
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
return settings?.servers?.get(0)?.users?.get(0)?.pass
|
||||||
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
return settings?.secretKey
|
return settings?.secretKey
|
||||||
@@ -401,59 +503,84 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
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)
|
||||||
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)) {
|
|| protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|
) {
|
||||||
val transport = streamSettings?.network ?: return null
|
val transport = streamSettings?.network ?: return null
|
||||||
return when (transport) {
|
return when (transport) {
|
||||||
"tcp" -> {
|
"tcp" -> {
|
||||||
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
val tcpSetting = streamSettings?.tcpSettings ?: return null
|
||||||
listOf(tcpSetting.header.type,
|
listOf(
|
||||||
tcpSetting.header.request?.headers?.Host?.joinToString().orEmpty(),
|
tcpSetting.header.type,
|
||||||
tcpSetting.header.request?.path?.joinToString().orEmpty())
|
tcpSetting.header.request?.headers?.Host?.joinToString(",").orEmpty(),
|
||||||
|
tcpSetting.header.request?.path?.joinToString(",").orEmpty()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"kcp" -> {
|
"kcp" -> {
|
||||||
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
val kcpSetting = streamSettings?.kcpSettings ?: return null
|
||||||
listOf(kcpSetting.header.type,
|
listOf(
|
||||||
"",
|
kcpSetting.header.type,
|
||||||
kcpSetting.seed.orEmpty())
|
"",
|
||||||
|
kcpSetting.seed.orEmpty()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"ws" -> {
|
"ws" -> {
|
||||||
val wsSetting = streamSettings?.wsSettings ?: return null
|
val wsSetting = streamSettings?.wsSettings ?: return null
|
||||||
listOf("",
|
listOf(
|
||||||
wsSetting.headers.Host,
|
"",
|
||||||
wsSetting.path)
|
wsSetting.headers.Host,
|
||||||
|
wsSetting.path
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"httpupgrade" -> {
|
"httpupgrade" -> {
|
||||||
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
|
||||||
listOf("",
|
listOf(
|
||||||
|
"",
|
||||||
httpupgradeSetting.host,
|
httpupgradeSetting.host,
|
||||||
httpupgradeSetting.path)
|
httpupgradeSetting.path
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"splithttp" -> {
|
"splithttp" -> {
|
||||||
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
|
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
|
||||||
listOf("",
|
listOf(
|
||||||
|
"",
|
||||||
splithttpSetting.host,
|
splithttpSetting.host,
|
||||||
splithttpSetting.path)
|
splithttpSetting.path
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"h2" -> {
|
"h2" -> {
|
||||||
val h2Setting = streamSettings?.httpSettings ?: return null
|
val h2Setting = streamSettings?.httpSettings ?: return null
|
||||||
listOf("",
|
listOf(
|
||||||
h2Setting.host.joinToString(),
|
"",
|
||||||
h2Setting.path)
|
h2Setting.host.joinToString(","),
|
||||||
|
h2Setting.path
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"quic" -> {
|
"quic" -> {
|
||||||
val quicSetting = streamSettings?.quicSettings ?: return null
|
val quicSetting = streamSettings?.quicSettings ?: return null
|
||||||
listOf(quicSetting.header.type,
|
listOf(
|
||||||
quicSetting.security,
|
quicSetting.header.type,
|
||||||
quicSetting.key)
|
quicSetting.security,
|
||||||
|
quicSetting.key
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"grpc" -> {
|
"grpc" -> {
|
||||||
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
val grpcSetting = streamSettings?.grpcSettings ?: return null
|
||||||
listOf(if (grpcSetting.multiMode == true) "multi" else "gun",
|
listOf(
|
||||||
grpcSetting.authority ?: "",
|
if (grpcSetting.multiMode == true) "multi" else "gun",
|
||||||
grpcSetting.serviceName)
|
grpcSetting.authority.orEmpty(),
|
||||||
|
grpcSetting.serviceName
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,59 +588,70 @@ data class V2rayConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DnsBean(var servers: ArrayList<Any>? = null,
|
data class DnsBean(
|
||||||
var hosts: Map<String, Any>? = null,
|
var servers: ArrayList<Any>? = null,
|
||||||
val clientIp: String? = null,
|
var hosts: Map<String, Any>? = null,
|
||||||
val disableCache: Boolean? = null,
|
val clientIp: String? = null,
|
||||||
val queryStrategy: String? = null,
|
val disableCache: Boolean? = null,
|
||||||
val tag: String? = null
|
val queryStrategy: String? = null,
|
||||||
|
val tag: String? = null
|
||||||
) {
|
) {
|
||||||
data class ServersBean(var address: String = "",
|
data class ServersBean(
|
||||||
var port: Int? = null,
|
var address: String = "",
|
||||||
var domains: List<String>? = null,
|
var port: Int? = null,
|
||||||
var expectIPs: List<String>? = null,
|
var domains: List<String>? = null,
|
||||||
val clientIp: String? = null)
|
var expectIPs: List<String>? = null,
|
||||||
}
|
val clientIp: String? = null
|
||||||
|
|
||||||
data class RoutingBean(var domainStrategy: String,
|
|
||||||
var domainMatcher: String? = null,
|
|
||||||
var rules: ArrayList<RulesBean>,
|
|
||||||
val balancers: List<Any>? = null) {
|
|
||||||
|
|
||||||
data class RulesBean(
|
|
||||||
var ip: ArrayList<String>? = null,
|
|
||||||
var domain: ArrayList<String>? = null,
|
|
||||||
var outboundTag: String = "",
|
|
||||||
var balancerTag: String? = null,
|
|
||||||
var port: String? = null,
|
|
||||||
val sourcePort: String? = null,
|
|
||||||
val network: String? = null,
|
|
||||||
val source: List<String>? = null,
|
|
||||||
val user: List<String>? = null,
|
|
||||||
var inboundTag: List<String>? = null,
|
|
||||||
val protocol: List<String>? = null,
|
|
||||||
val attrs: String? = null,
|
|
||||||
val domainMatcher: String? = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PolicyBean(var levels: Map<String, LevelBean>,
|
data class RoutingBean(
|
||||||
var system: Any? = null) {
|
var domainStrategy: String,
|
||||||
data class LevelBean(
|
var domainMatcher: String? = null,
|
||||||
var handshake: Int? = null,
|
var rules: ArrayList<RulesBean>,
|
||||||
var connIdle: Int? = null,
|
val balancers: List<Any>? = null
|
||||||
var uplinkOnly: Int? = null,
|
) {
|
||||||
var downlinkOnly: Int? = null,
|
|
||||||
val statsUserUplink: Boolean? = null,
|
data class RulesBean(
|
||||||
val statsUserDownlink: Boolean? = null,
|
var type: String = "field",
|
||||||
var bufferSize: Int? = null)
|
var ip: ArrayList<String>? = null,
|
||||||
|
var domain: ArrayList<String>? = null,
|
||||||
|
var outboundTag: String = "",
|
||||||
|
var balancerTag: String? = null,
|
||||||
|
var port: String? = null,
|
||||||
|
val sourcePort: String? = null,
|
||||||
|
val network: String? = null,
|
||||||
|
val source: List<String>? = null,
|
||||||
|
val user: List<String>? = null,
|
||||||
|
var inboundTag: List<String>? = null,
|
||||||
|
val protocol: List<String>? = null,
|
||||||
|
val attrs: String? = null,
|
||||||
|
val domainMatcher: String? = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FakednsBean(var ipPool: String = "198.18.0.0/15",
|
data class PolicyBean(
|
||||||
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
|
var levels: Map<String, LevelBean>,
|
||||||
|
var system: Any? = null
|
||||||
|
) {
|
||||||
|
data class LevelBean(
|
||||||
|
var handshake: Int? = null,
|
||||||
|
var connIdle: Int? = null,
|
||||||
|
var uplinkOnly: Int? = null,
|
||||||
|
var downlinkOnly: Int? = null,
|
||||||
|
val statsUserUplink: Boolean? = null,
|
||||||
|
val statsUserDownlink: Boolean? = null,
|
||||||
|
var bufferSize: Int? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FakednsBean(
|
||||||
|
var ipPool: String = "198.18.0.0/15",
|
||||||
|
var poolSize: Int = 10000
|
||||||
|
) // roughly 10 times smaller than total ip pool
|
||||||
|
|
||||||
fun getProxyOutbound(): OutboundBean? {
|
fun getProxyOutbound(): OutboundBean? {
|
||||||
outbounds?.forEach { outbound ->
|
outbounds.forEach { outbound ->
|
||||||
EConfigType.entries.forEach {
|
EConfigType.entries.forEach {
|
||||||
if (outbound.protocol.equals(it.name, true)) {
|
if (outbound.protocol.equals(it.name, true)) {
|
||||||
return outbound
|
return outbound
|
||||||
@@ -525,13 +663,13 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
fun toPrettyPrinting(): String {
|
fun toPrettyPrinting(): String {
|
||||||
return GsonBuilder()
|
return GsonBuilder()
|
||||||
.setPrettyPrinting()
|
.setPrettyPrinting()
|
||||||
.disableHtmlEscaping()
|
.disableHtmlEscaping()
|
||||||
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
|
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
|
||||||
object : TypeToken<Double>() {}.type,
|
object : TypeToken<Double>() {}.type,
|
||||||
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
|
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
|
||||||
)
|
)
|
||||||
.create()
|
.create()
|
||||||
.toJson(this)
|
.toJson(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
package com.v2ray.ang.dto
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
data class VmessQRCode(var v: String = "",
|
data class VmessQRCode(
|
||||||
var ps: String = "",
|
var v: String = "",
|
||||||
var add: String = "",
|
var ps: String = "",
|
||||||
var port: String = "",
|
var add: String = "",
|
||||||
var id: String = "",
|
var port: String = "",
|
||||||
var aid: String = "0",
|
var id: String = "",
|
||||||
var scy: String = "",
|
var aid: String = "0",
|
||||||
var net: String = "",
|
var scy: String = "",
|
||||||
var type: String = "",
|
var net: String = "",
|
||||||
var host: String = "",
|
var type: String = "",
|
||||||
var path: String = "",
|
var host: String = "",
|
||||||
var tls: String = "",
|
var path: String = "",
|
||||||
var sni: String = "",
|
var tls: String = "",
|
||||||
var alpn: String = "",
|
var sni: String = "",
|
||||||
var fp: String = "")
|
var alpn: String = "",
|
||||||
|
var fp: String = ""
|
||||||
|
)
|
||||||
@@ -1,13 +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.net.ConnectivityManager
|
import android.content.Intent
|
||||||
import android.net.NetworkCapabilities
|
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,22 +57,40 @@ val URLConnection.responseLength: Long
|
|||||||
}
|
}
|
||||||
|
|
||||||
val URI.idnHost: String
|
val URI.idnHost: String
|
||||||
get() = host?.replace("[", "")?.replace("]", "") ?: ""
|
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
||||||
|
|
||||||
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
|
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "")
|
||||||
|
|
||||||
val Context.isNetworkConnected: Boolean
|
fun String.toLongEx(): Long = toLongOrNull() ?: 0
|
||||||
get() {
|
|
||||||
val manager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
object : BroadcastReceiver() {
|
||||||
manager.getNetworkCapabilities(manager.activeNetwork)?.let {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
callback()
|
||||||
it.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
|
if (onetime) context.unregisterReceiver(this)
|
||||||
it.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
|
}
|
||||||
it.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
|
}.apply {
|
||||||
it.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
} ?: false
|
registerReceiver(this, IntentFilter().apply {
|
||||||
else
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
@Suppress("DEPRECATION")
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
manager.activeNetworkInfo?.isConnectedOrConnecting == true
|
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
|
||||||
|
}
|
||||||
@@ -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,53 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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,219 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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.service.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
if (Intent.ACTION_BOOT_COMPLETED == intent?.action && MmkvManager.decodeStartOnBoot()) {
|
||||||
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
V2RayServiceManager.startV2Ray(context!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,12 @@ 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.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
import com.v2ray.ang.util.MmkvManager
|
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?) {
|
||||||
|
|
||||||
@@ -27,7 +24,7 @@ class TaskerReceiver : BroadcastReceiver() {
|
|||||||
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 {
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
} else {
|
} else {
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
})
|
}
|
||||||
|
)
|
||||||
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
|
remoteViews.setOnClickPendingIntent(R.id.layout_switch, pendingIntent)
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
|
remoteViews.setInt(R.id.image_switch, "setImageResource", R.drawable.ic_stop_24dp)
|
||||||
@@ -65,12 +66,17 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
AppWidgetManager.getInstance(context)?.let { manager ->
|
AppWidgetManager.getInstance(context)?.let { manager ->
|
||||||
when (intent.getIntExtra("key", 0)) {
|
when (intent.getIntExtra("key", 0)) {
|
||||||
AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
|
AppConfig.MSG_STATE_RUNNING, AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||||
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
updateWidgetBackground(
|
||||||
true)
|
context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||||
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
AppConfig.MSG_STATE_NOT_RUNNING, AppConfig.MSG_STATE_START_FAILURE, AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||||
updateWidgetBackground(context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
updateWidgetBackground(
|
||||||
false)
|
context, manager, manager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)),
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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 val TAG = ANG_PACKAGE
|
||||||
|
private lateinit var process: Process
|
||||||
|
|
||||||
|
fun runProcess(context: Context, cmd: MutableList<String>) {
|
||||||
|
Log.d(TAG, cmd.toString())
|
||||||
|
|
||||||
|
try {
|
||||||
|
val proBuilder = ProcessBuilder(cmd)
|
||||||
|
proBuilder.redirectErrorStream(true)
|
||||||
|
process = proBuilder
|
||||||
|
.directory(context.filesDir)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
Thread.sleep(50L)
|
||||||
|
Log.d(TAG, "runProcess check")
|
||||||
|
process.waitFor()
|
||||||
|
Log.d(TAG, "runProcess exited")
|
||||||
|
}
|
||||||
|
Log.d(TAG, process.toString())
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopProcess() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "runProcess destroy")
|
||||||
|
process?.destroy()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ class QSTileService : TileService() {
|
|||||||
Tile.STATE_INACTIVE -> {
|
Tile.STATE_INACTIVE -> {
|
||||||
Utils.startVServiceFromToggle(this)
|
Utils.startVServiceFromToggle(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
Tile.STATE_ACTIVE -> {
|
Tile.STATE_ACTIVE -> {
|
||||||
Utils.stopVService(this)
|
Utils.stopVService(this)
|
||||||
}
|
}
|
||||||
@@ -74,15 +75,19 @@ class QSTileService : TileService() {
|
|||||||
AppConfig.MSG_STATE_RUNNING -> {
|
AppConfig.MSG_STATE_RUNNING -> {
|
||||||
context?.setState(Tile.STATE_ACTIVE)
|
context?.setState(Tile.STATE_ACTIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
AppConfig.MSG_STATE_NOT_RUNNING -> {
|
||||||
context?.setState(Tile.STATE_INACTIVE)
|
context?.setState(Tile.STATE_INACTIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START_SUCCESS -> {
|
AppConfig.MSG_STATE_START_SUCCESS -> {
|
||||||
context?.setState(Tile.STATE_ACTIVE)
|
context?.setState(Tile.STATE_ACTIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START_FAILURE -> {
|
AppConfig.MSG_STATE_START_FAILURE -> {
|
||||||
context?.setState(Tile.STATE_INACTIVE)
|
context?.setState(Tile.STATE_INACTIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
AppConfig.MSG_STATE_STOP_SUCCESS -> {
|
||||||
context?.setState(Tile.STATE_INACTIVE)
|
context?.setState(Tile.STATE_INACTIVE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ 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.util.AngConfigManager
|
||||||
|
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
object SubscriptionUpdater {
|
object SubscriptionUpdater {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateTask(context: Context, params: WorkerParameters) :
|
class UpdateTask(context: Context, params: WorkerParameters) :
|
||||||
CoroutineWorker(context, params) {
|
CoroutineWorker(context, params) {
|
||||||
|
|
||||||
@@ -41,8 +41,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)
|
||||||
@@ -57,11 +57,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()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
|
|||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val context = newBase?.let {
|
val context = newBase?.let {
|
||||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||||
}
|
}
|
||||||
super.attachBaseContext(context)
|
super.attachBaseContext(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ 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 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.ServerConfig
|
||||||
import com.v2ray.ang.extension.toSpeedString
|
import com.v2ray.ang.extension.toSpeedString
|
||||||
@@ -24,17 +24,19 @@ import com.v2ray.ang.extension.toast
|
|||||||
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.MmkvManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
|
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 com.v2ray.ang.util.V2rayConfigUtil
|
||||||
import go.Seq
|
import go.Seq
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import libv2ray.V2RayPoint
|
import libv2ray.V2RayPoint
|
||||||
import libv2ray.V2RayVPNServiceSupportsSet
|
import libv2ray.V2RayVPNServiceSupportsSet
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -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) {
|
||||||
@@ -64,7 +64,7 @@ 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 result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||||
if (!result.status) return
|
if (!result.status) return
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ object V2RayServiceManager {
|
|||||||
} 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 ((settingsStorage?.decodeString(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 +108,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()
|
||||||
@@ -129,7 +127,7 @@ object V2RayServiceManager {
|
|||||||
|
|
||||||
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
|
||||||
@@ -153,7 +151,7 @@ object V2RayServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
v2rayPoint.configureFileContent = result.content
|
v2rayPoint.configureFileContent = result.content
|
||||||
v2rayPoint.domainName = config.getV2rayPointDomainAndPort()
|
v2rayPoint.domainName = result.domainPort
|
||||||
currentConfig = config
|
currentConfig = config
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -165,6 +163,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 +192,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,25 +200,29 @@ 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 {
|
||||||
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_START -> {
|
AppConfig.MSG_STATE_START -> {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_STOP -> {
|
AppConfig.MSG_STATE_STOP -> {
|
||||||
serviceControl.stopService()
|
serviceControl.stopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_STATE_RESTART -> {
|
AppConfig.MSG_STATE_RESTART -> {
|
||||||
startV2rayPoint()
|
startV2rayPoint()
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_DELAY -> {
|
AppConfig.MSG_MEASURE_DELAY -> {
|
||||||
measureV2rayDelay()
|
measureV2rayDelay()
|
||||||
}
|
}
|
||||||
@@ -228,6 +233,7 @@ object V2RayServiceManager {
|
|||||||
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats")
|
||||||
stopSpeedNotification()
|
stopSpeedNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent.ACTION_SCREEN_ON -> {
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats")
|
||||||
startSpeedNotification()
|
startSpeedNotification()
|
||||||
@@ -270,46 +276,52 @@ object V2RayServiceManager {
|
|||||||
private fun showNotification() {
|
private fun showNotification() {
|
||||||
val service = serviceControl?.get()?.getService() ?: return
|
val service = serviceControl?.get()?.getService() ?: return
|
||||||
val startMainIntent = Intent(service, MainActivity::class.java)
|
val startMainIntent = Intent(service, MainActivity::class.java)
|
||||||
val contentPendingIntent = PendingIntent.getActivity(service,
|
val contentPendingIntent = PendingIntent.getActivity(
|
||||||
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
|
service,
|
||||||
|
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
} else {
|
} else {
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||||
stopV2RayIntent.`package` = ANG_PACKAGE
|
stopV2RayIntent.`package` = ANG_PACKAGE
|
||||||
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
|
||||||
|
|
||||||
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service,
|
val stopV2RayPendingIntent = PendingIntent.getBroadcast(
|
||||||
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
|
service,
|
||||||
|
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
} else {
|
} else {
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val channelId =
|
val channelId =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
} else {
|
} else {
|
||||||
// If earlier version channel ID is not used
|
// If earlier version channel ID is not used
|
||||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
mBuilder = NotificationCompat.Builder(service, channelId)
|
mBuilder = NotificationCompat.Builder(service, channelId)
|
||||||
.setSmallIcon(R.drawable.ic_stat_name)
|
.setSmallIcon(R.drawable.ic_stat_name)
|
||||||
.setContentTitle(currentConfig?.remarks)
|
.setContentTitle(currentConfig?.remarks)
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setShowWhen(false)
|
.setShowWhen(false)
|
||||||
.setOnlyAlertOnce(true)
|
.setOnlyAlertOnce(true)
|
||||||
.setContentIntent(contentPendingIntent)
|
.setContentIntent(contentPendingIntent)
|
||||||
.addAction(R.drawable.ic_delete_24dp,
|
.addAction(
|
||||||
service.getString(R.string.notification_action_stop_v2ray),
|
R.drawable.ic_delete_24dp,
|
||||||
stopV2RayPendingIntent)
|
service.getString(R.string.notification_action_stop_v2ray),
|
||||||
|
stopV2RayPendingIntent
|
||||||
|
)
|
||||||
//.build()
|
//.build()
|
||||||
|
|
||||||
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
|
||||||
@@ -321,8 +333,10 @@ object V2RayServiceManager {
|
|||||||
private fun createNotificationChannel(): String {
|
private fun createNotificationChannel(): String {
|
||||||
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
||||||
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
||||||
val chan = NotificationChannel(channelId,
|
val chan = NotificationChannel(
|
||||||
channelName, NotificationManager.IMPORTANCE_HIGH)
|
channelId,
|
||||||
|
channelName, NotificationManager.IMPORTANCE_HIGH
|
||||||
|
)
|
||||||
chan.lightColor = Color.DKGRAY
|
chan.lightColor = Color.DKGRAY
|
||||||
chan.importance = NotificationManager.IMPORTANCE_NONE
|
chan.importance = NotificationManager.IMPORTANCE_NONE
|
||||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||||
@@ -363,40 +377,43 @@ 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) {
|
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true
|
||||||
|
) {
|
||||||
var lastZeroSpeed = false
|
var lastZeroSpeed = false
|
||||||
val outboundTags = currentConfig?.getAllOutboundTags()
|
val outboundTags = currentConfig?.getAllOutboundTags()
|
||||||
outboundTags?.remove(TAG_DIRECT)
|
outboundTags?.remove(TAG_DIRECT)
|
||||||
|
|
||||||
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
val queryTime = System.currentTimeMillis()
|
val queryTime = System.currentTimeMillis()
|
||||||
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
|
||||||
var proxyTotal = 0L
|
var proxyTotal = 0L
|
||||||
val text = StringBuilder()
|
val text = StringBuilder()
|
||||||
outboundTags?.forEach {
|
outboundTags?.forEach {
|
||||||
val up = v2rayPoint.queryStats(it, AppConfig.UPLINK)
|
val up = v2rayPoint.queryStats(it, AppConfig.UPLINK)
|
||||||
val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK)
|
val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK)
|
||||||
if (up + down > 0) {
|
if (up + down > 0) {
|
||||||
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
|
||||||
proxyTotal += up + down
|
proxyTotal += up + down
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
|
||||||
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
|
|
||||||
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
|
||||||
if (!zeroSpeed || !lastZeroSpeed) {
|
|
||||||
if (proxyTotal == 0L) {
|
|
||||||
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
|
||||||
}
|
|
||||||
appendSpeedString(text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
|
||||||
directDownlink / sinceLastQueryInSeconds)
|
|
||||||
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
|
||||||
}
|
|
||||||
lastZeroSpeed = zeroSpeed
|
|
||||||
lastQueryTime = queryTime
|
|
||||||
}
|
}
|
||||||
|
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.UPLINK)
|
||||||
|
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
|
||||||
|
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
|
||||||
|
if (!zeroSpeed || !lastZeroSpeed) {
|
||||||
|
if (proxyTotal == 0L) {
|
||||||
|
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
|
||||||
|
}
|
||||||
|
appendSpeedString(
|
||||||
|
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
|
||||||
|
directDownlink / sinceLastQueryInSeconds
|
||||||
|
)
|
||||||
|
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
|
||||||
|
}
|
||||||
|
lastZeroSpeed = zeroSpeed
|
||||||
|
lastQueryTime = queryTime
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,20 @@ 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.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.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 go.Seq
|
import go.Seq
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.cancelChildren
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import libv2ray.Libv2ray
|
import libv2ray.Libv2ray
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@@ -26,12 +35,13 @@ 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MSG_MEASURE_CONFIG_CANCEL -> {
|
MSG_MEASURE_CONFIG_CANCEL -> {
|
||||||
realTestScope.coroutineContext[Job]?.cancelChildren()
|
realTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
}
|
}
|
||||||
@@ -42,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 server = MmkvManager.decodeServerConfig(guid) ?: return retFailure
|
||||||
|
if (server.getProxyOutbound()?.protocol?.equals(EConfigType.HYSTERIA2.name, true) == true) {
|
||||||
|
val delay = PluginUtil.realPingHy2(this, server)
|
||||||
|
return delay
|
||||||
|
} else {
|
||||||
|
val config = V2rayConfigUtil.getV2rayConfig(this, guid)
|
||||||
|
if (!config.status) {
|
||||||
|
return retFailure
|
||||||
|
}
|
||||||
|
return SpeedtestUtil.realPing(config.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,22 +4,28 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.*
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.LocalSocket
|
||||||
|
import android.net.LocalSocketAddress
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.net.VpnService
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
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.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.util.MmkvManager.settingsStorage
|
||||||
import com.v2ray.ang.util.MmkvManager
|
|
||||||
import com.v2ray.ang.util.MyContextWrapper
|
import com.v2ray.ang.util.MyContextWrapper
|
||||||
|
import com.v2ray.ang.util.SettingsManager
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
@@ -34,7 +40,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
|
||||||
@@ -54,9 +59,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||||
private val defaultNetworkRequest by lazy {
|
private val defaultNetworkRequest by lazy {
|
||||||
NetworkRequest.Builder()
|
NetworkRequest.Builder()
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
||||||
@@ -112,13 +117,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())
|
||||||
@@ -129,29 +132,32 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
if (settingsStorage?.decodeBool(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 (settingsStorage?.decodeBool(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())
|
||||||
|
|
||||||
|
val selfPackageName = BuildConfig.APPLICATION_ID
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
|
||||||
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
|
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
|
||||||
|
//process self package
|
||||||
|
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
|
||||||
apps?.forEach {
|
apps?.forEach {
|
||||||
try {
|
try {
|
||||||
if (bypassApps)
|
if (bypassApps)
|
||||||
@@ -159,9 +165,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
else
|
else
|
||||||
builder.addAllowedApplication(it)
|
builder.addAllowedApplication(it)
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
//Logger.d(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
builder.addDisallowedApplication(selfPackageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the old interface since the parameters have been changed.
|
// Close the old interface since the parameters have been changed.
|
||||||
@@ -196,15 +203,17 @@ 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(File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
val cmd = arrayListOf(
|
||||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||||
"--netif-netmask", "255.255.255.252",
|
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||||
"--socks-server-addr", "127.0.0.1:${socksPort}",
|
"--netif-netmask", "255.255.255.252",
|
||||||
"--tunmtu", VPN_MTU.toString(),
|
"--socks-server-addr", "$LOOPBACK:${socksPort}",
|
||||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
"--tunmtu", VPN_MTU.toString(),
|
||||||
"--enable-udprelay",
|
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||||
"--loglevel", "notice")
|
"--enable-udprelay",
|
||||||
|
"--loglevel", "notice"
|
||||||
|
)
|
||||||
|
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
|
||||||
cmd.add("--netif-ip6addr")
|
cmd.add("--netif-ip6addr")
|
||||||
@@ -213,7 +222,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
|
||||||
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
|
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(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())
|
||||||
|
|
||||||
@@ -221,14 +230,14 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
val proBuilder = ProcessBuilder(cmd)
|
val proBuilder = ProcessBuilder(cmd)
|
||||||
proBuilder.redirectErrorStream(true)
|
proBuilder.redirectErrorStream(true)
|
||||||
process = proBuilder
|
process = proBuilder
|
||||||
.directory(applicationContext.filesDir)
|
.directory(applicationContext.filesDir)
|
||||||
.start()
|
.start()
|
||||||
Thread(Runnable {
|
Thread(Runnable {
|
||||||
Log.d(packageName,"$TUN2SOCKS check")
|
Log.d(packageName, "$TUN2SOCKS check")
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
Log.d(packageName,"$TUN2SOCKS exited")
|
Log.d(packageName, "$TUN2SOCKS exited")
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
Log.d(packageName,"$TUN2SOCKS restart")
|
Log.d(packageName, "$TUN2SOCKS restart")
|
||||||
runTun2socks()
|
runTun2socks()
|
||||||
}
|
}
|
||||||
}).start()
|
}).start()
|
||||||
@@ -275,7 +284,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
// val emptyInfo = VpnNetworkInfo()
|
// val emptyInfo = VpnNetworkInfo()
|
||||||
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
// val info = loadVpnNetworkInfo(configName, emptyInfo)!! + (lastNetworkInfo ?: emptyInfo)
|
||||||
// saveVpnNetworkInfo(configName, info)
|
// saveVpnNetworkInfo(configName, info)
|
||||||
isRunning = false;
|
isRunning = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
try {
|
try {
|
||||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||||
@@ -328,7 +337,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
|
|||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val context = newBase?.let {
|
val context = newBase?.let {
|
||||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||||
}
|
}
|
||||||
super.attachBaseContext(context)
|
super.attachBaseContext(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class AboutActivity : BaseActivity() {
|
class AboutActivity : BaseActivity() {
|
||||||
private val binding by lazy {ActivityAboutBinding.inflate(layoutInflater)}
|
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
|
||||||
private val extDir by lazy { File(Utils.backupPath(this)) }
|
private val extDir by lazy { File(Utils.backupPath(this)) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -28,13 +28,14 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val context = newBase?.let {
|
val context = newBase?.let {
|
||||||
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
MyContextWrapper.wrap(newBase, Utils.getLocale())
|
||||||
}
|
}
|
||||||
super.attachBaseContext(context)
|
super.attachBaseContext(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.fragment.app.FragmentActivity
|
|||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
|
||||||
class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List<Fragment>) :
|
class FragmentAdapter(fragmentActivity: FragmentActivity, private val mFragments: List<Fragment>) :
|
||||||
FragmentStateAdapter(fragmentActivity) {
|
FragmentStateAdapter(fragmentActivity) {
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
return mFragments[position]
|
return mFragments[position]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -14,11 +14,9 @@ import com.v2ray.ang.databinding.ActivityLogcatBinding
|
|||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.LinkedHashSet
|
|
||||||
|
|
||||||
class LogcatActivity : BaseActivity() {
|
class LogcatActivity : BaseActivity() {
|
||||||
private val binding by lazy {
|
private val binding by lazy {
|
||||||
@@ -44,8 +42,10 @@ class LogcatActivity : BaseActivity() {
|
|||||||
val lst = LinkedHashSet<String>()
|
val lst = LinkedHashSet<String>()
|
||||||
lst.add("logcat")
|
lst.add("logcat")
|
||||||
lst.add("-c")
|
lst.add("-c")
|
||||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
withContext(Dispatchers.IO) {
|
||||||
process.waitFor()
|
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
|
process.waitFor()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val lst = LinkedHashSet<String>()
|
val lst = LinkedHashSet<String>()
|
||||||
lst.add("logcat")
|
lst.add("logcat")
|
||||||
@@ -54,7 +54,9 @@ class LogcatActivity : BaseActivity() {
|
|||||||
lst.add("time")
|
lst.add("time")
|
||||||
lst.add("-s")
|
lst.add("-s")
|
||||||
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
|
||||||
val process = Runtime.getRuntime().exec(lst.toTypedArray())
|
val process = withContext(Dispatchers.IO) {
|
||||||
|
Runtime.getRuntime().exec(lst.toTypedArray())
|
||||||
|
}
|
||||||
// val bufferedReader = BufferedReader(
|
// val bufferedReader = BufferedReader(
|
||||||
// InputStreamReader(process.inputStream))
|
// InputStreamReader(process.inputStream))
|
||||||
// val allText = bufferedReader.use(BufferedReader::readText)
|
// val allText = bufferedReader.use(BufferedReader::readText)
|
||||||
@@ -82,10 +84,12 @@ class LogcatActivity : BaseActivity() {
|
|||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.clear_all -> {
|
R.id.clear_all -> {
|
||||||
logcat(true)
|
logcat(true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,15 +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.isNetworkConnected
|
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
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.AngConfigManager
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
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
|
||||||
@@ -88,7 +89,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 ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
|
||||||
val intent = VpnService.prepare(this)
|
val intent = VpnService.prepare(this)
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
@@ -112,11 +113,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
|
||||||
)
|
)
|
||||||
@@ -199,14 +198,10 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startV2Ray() {
|
fun startV2Ray() {
|
||||||
if (isNetworkConnected) {
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
if (MmkvManager.mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
V2RayServiceManager.startV2Ray(this)
|
|
||||||
} else {
|
|
||||||
ToastCompat.makeText(this, getString(R.string.connection_test_fail), Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
|
V2RayServiceManager.startV2Ray(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartV2Ray() {
|
fun restartV2Ray() {
|
||||||
@@ -236,12 +231,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
if (searchItem != null) {
|
if (searchItem != null) {
|
||||||
val searchView = searchItem.actionView as SearchView
|
val searchView = searchItem.actionView as SearchView
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||||
mainViewModel.filterConfig(query.orEmpty())
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
mainViewModel.filterConfig(newText.orEmpty())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean = false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
searchView.setOnCloseListener {
|
searchView.setOnCloseListener {
|
||||||
@@ -283,6 +278,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
|
||||||
@@ -293,6 +293,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
|
||||||
@@ -711,10 +716,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,20 +16,19 @@ 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.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.AngConfigManager
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
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
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>()
|
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||||
, ItemTouchHelperAdapter {
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val VIEW_TYPE_ITEM = 1
|
private const val VIEW_TYPE_ITEM = 1
|
||||||
private const val VIEW_TYPE_FOOTER = 2
|
private const val VIEW_TYPE_FOOTER = 2
|
||||||
@@ -61,23 +59,18 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
|
|
||||||
holder.itemMainBinding.tvName.text = profile.remarks
|
holder.itemMainBinding.tvName.text = profile.remarks
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString() ?: ""
|
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
||||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
||||||
}
|
}
|
||||||
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) {
|
||||||
@@ -86,16 +79,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
|
||||||
|
|
||||||
@@ -140,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 (settingsStorage?.decodeBool(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)
|
||||||
@@ -159,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()))
|
||||||
}
|
}
|
||||||
@@ -237,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
|
||||||
@@ -19,12 +18,12 @@ 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.util.AppManagerUtil
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
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
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
|
||||||
class PerAppProxyActivity : BaseActivity() {
|
class PerAppProxyActivity : BaseActivity() {
|
||||||
@@ -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)
|
||||||
@@ -46,32 +44,32 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
val blacklist = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
val blacklist = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
|
||||||
|
|
||||||
AppManagerUtil.rxLoadNetworkAppList(this)
|
AppManagerUtil.rxLoadNetworkAppList(this)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.map {
|
.map {
|
||||||
if (blacklist != null) {
|
if (blacklist != null) {
|
||||||
it.forEach { one ->
|
it.forEach { one ->
|
||||||
if (blacklist.contains(one.packageName)) {
|
if (blacklist.contains(one.packageName)) {
|
||||||
one.isSelected = 1
|
one.isSelected = 1
|
||||||
} else {
|
} else {
|
||||||
one.isSelected = 0
|
one.isSelected = 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val comparator = Comparator<AppInfo> { p1, p2 ->
|
|
||||||
when {
|
|
||||||
p1.isSelected > p2.isSelected -> -1
|
|
||||||
p1.isSelected == p2.isSelected -> 0
|
|
||||||
else -> 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.sortedWith(comparator)
|
|
||||||
} else {
|
|
||||||
val comparator = object : Comparator<AppInfo> {
|
|
||||||
val collator = Collator.getInstance()
|
|
||||||
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
|
|
||||||
}
|
|
||||||
it.sortedWith(comparator)
|
|
||||||
}
|
}
|
||||||
|
val comparator = Comparator<AppInfo> { p1, p2 ->
|
||||||
|
when {
|
||||||
|
p1.isSelected > p2.isSelected -> -1
|
||||||
|
p1.isSelected == p2.isSelected -> 0
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.sortedWith(comparator)
|
||||||
|
} else {
|
||||||
|
val comparator = object : Comparator<AppInfo> {
|
||||||
|
val collator = Collator.getInstance()
|
||||||
|
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
|
||||||
|
}
|
||||||
|
it.sortedWith(comparator)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// .map {
|
// .map {
|
||||||
// val comparator = object : Comparator<AppInfo> {
|
// val comparator = object : Comparator<AppInfo> {
|
||||||
// val collator = Collator.getInstance()
|
// val collator = Collator.getInstance()
|
||||||
@@ -79,13 +77,13 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
// }
|
// }
|
||||||
// it.sortedWith(comparator)
|
// it.sortedWith(comparator)
|
||||||
// }
|
// }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe {
|
.subscribe {
|
||||||
appsAll = it
|
appsAll = it
|
||||||
adapter = PerAppProxyAdapter(this, it, blacklist)
|
adapter = PerAppProxyAdapter(this, it, blacklist)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.pbWaiting.visibility = View.GONE
|
binding.pbWaiting.visibility = View.GONE
|
||||||
}
|
}
|
||||||
/***
|
/***
|
||||||
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
var dst = 0
|
var dst = 0
|
||||||
@@ -218,18 +216,22 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
it.notifyDataSetChanged()
|
it.notifyDataSetChanged()
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
R.id.select_proxy_app -> {
|
R.id.select_proxy_app -> {
|
||||||
selectProxyApp()
|
selectProxyApp()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.import_proxy_app -> {
|
R.id.import_proxy_app -> {
|
||||||
importProxyApp()
|
importProxyApp()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.export_proxy_app -> {
|
R.id.export_proxy_app -> {
|
||||||
exportProxyApp()
|
exportProxyApp()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +326,8 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
if (key.isNotEmpty()) {
|
if (key.isNotEmpty()) {
|
||||||
appsAll?.forEach {
|
appsAll?.forEach {
|
||||||
if (it.appName.uppercase().indexOf(key) >= 0
|
if (it.appName.uppercase().indexOf(key) >= 0
|
||||||
|| it.packageName.uppercase().indexOf(key) >= 0) {
|
|| it.packageName.uppercase().indexOf(key) >= 0
|
||||||
|
) {
|
||||||
apps.add(it)
|
apps.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) :
|
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) :
|
||||||
RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val VIEW_TYPE_HEADER = 0
|
private const val VIEW_TYPE_HEADER = 0
|
||||||
@@ -34,8 +33,10 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
|||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
VIEW_TYPE_HEADER -> {
|
VIEW_TYPE_HEADER -> {
|
||||||
val view = View(ctx)
|
val view = View(ctx)
|
||||||
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
view.layoutParams = ViewGroup.LayoutParams(
|
||||||
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0)
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0
|
||||||
|
)
|
||||||
BaseViewHolder(view)
|
BaseViewHolder(view)
|
||||||
}
|
}
|
||||||
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
|
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
|
||||||
@@ -51,7 +52,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
|
|||||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||||
|
|
||||||
inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root),
|
inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root),
|
||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
|
private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
|
||||||
private lateinit var appInfo: AppInfo
|
private lateinit var appInfo: AppInfo
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
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.util.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 ?: false
|
||||||
|
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.remarks = binding.etRemarks.text.toString()
|
||||||
|
rulesetItem.looked = binding.chkLocked.isChecked
|
||||||
|
binding.etDomain.text.toString().let { rulesetItem.domain = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
|
||||||
|
binding.etIp.text.toString().let { rulesetItem.ip = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
|
||||||
|
binding.etProtocol.text.toString().let { rulesetItem.protocol = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } }
|
||||||
|
binding.etPort.text.toString().let { rulesetItem.port = it.ifEmpty { null } }
|
||||||
|
binding.etNetwork.text.toString().let { rulesetItem.network = it.ifEmpty { null } }
|
||||||
|
rulesetItem.outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(rulesetItem.remarks)) {
|
||||||
|
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,158 @@
|
|||||||
|
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.helper.SimpleItemTouchHelperCallback
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
|
import com.v2ray.ang.util.SettingsManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
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, settingsStorage?.decodeString(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) {
|
||||||
|
settingsStorage.encode(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) { _, _ ->
|
||||||
|
try {
|
||||||
|
val clipboard = Utils.getClipboard(this)
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val ret = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (ret) {
|
||||||
|
refreshData()
|
||||||
|
toast(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.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 = MmkvManager.decodeRoutingRulesets() ?: mutableListOf()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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.helper.ItemTouchHelperAdapter
|
||||||
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
|
import com.v2ray.ang.ui.MainRecyclerAdapter.BaseViewHolder
|
||||||
|
import com.v2ray.ang.util.SettingsManager
|
||||||
|
|
||||||
|
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 ?: false
|
||||||
|
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 com.v2ray.ang.R
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import com.v2ray.ang.AppConfig
|
|
||||||
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,158 +0,0 @@
|
|||||||
package com.v2ray.ang.ui
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.*
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,13 +19,13 @@ class ScScannerActivity : BaseActivity() {
|
|||||||
|
|
||||||
fun importQRcode(): Boolean {
|
fun importQRcode(): Boolean {
|
||||||
RxPermissions(this)
|
RxPermissions(this)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (it)
|
if (it)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import com.v2ray.ang.util.Utils
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
class ScSwitchActivity : BaseActivity() {
|
class ScSwitchActivity : BaseActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.tbruyelle.rxpermissions3.RxPermissions
|
import 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.util.MmkvManager.settingsStorage
|
||||||
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
|
||||||
import io.github.g00fy2.quickie.config.ScannerConfig
|
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)
|
||||||
@@ -33,7 +30,7 @@ class ScannerActivity : BaseActivity(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchScan(){
|
private fun launchScan() {
|
||||||
scanQrCode.launch(
|
scanQrCode.launch(
|
||||||
ScannerConfig.build {
|
ScannerConfig.build {
|
||||||
setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected
|
setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected
|
||||||
@@ -44,7 +41,7 @@ class ScannerActivity : BaseActivity(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleResult(result: QRResult) {
|
private fun handleResult(result: QRResult) {
|
||||||
if (result is QRResult.QRSuccess ) {
|
if (result is QRResult.QRSuccess) {
|
||||||
finished(result.content.rawValue.orEmpty())
|
finished(result.content.rawValue.orEmpty())
|
||||||
} else {
|
} else {
|
||||||
finish()
|
finish()
|
||||||
@@ -54,7 +51,7 @@ class ScannerActivity : BaseActivity(){
|
|||||||
private fun finished(text: String) {
|
private fun finished(text: String) {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.putExtra("SCAN_RESULT", text)
|
intent.putExtra("SCAN_RESULT", text)
|
||||||
setResult(AppCompatActivity.RESULT_OK, intent)
|
setResult(RESULT_OK, intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +65,7 @@ class ScannerActivity : BaseActivity(){
|
|||||||
launchScan()
|
launchScan()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.select_photo -> {
|
R.id.select_photo -> {
|
||||||
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
Manifest.permission.READ_MEDIA_IMAGES
|
Manifest.permission.READ_MEDIA_IMAGES
|
||||||
@@ -88,6 +86,7 @@ class ScannerActivity : BaseActivity(){
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ 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.PREF_ALLOW_INSECURE
|
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
|
||||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||||
@@ -27,25 +26,17 @@ import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
|
|||||||
import com.v2ray.ang.extension.removeWhiteSpace
|
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.util.MmkvManager
|
||||||
import com.v2ray.ang.util.MmkvManager.ID_MAIN
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
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
|
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))
|
||||||
@@ -102,11 +93,11 @@ class ServerActivity : BaseActivity() {
|
|||||||
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 +106,19 @@ 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_reserved2: EditText? by lazy { findViewById(R.id.et_reserved2) }
|
||||||
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
|
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) }
|
||||||
|
|
||||||
|
|
||||||
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(
|
||||||
@@ -258,7 +252,9 @@ class ServerActivity : BaseActivity() {
|
|||||||
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
|
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
|
||||||
et_alterId?.text =
|
et_alterId?.text =
|
||||||
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
|
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
|
||||||
if (config.configType == EConfigType.SOCKS) {
|
if (config.configType == EConfigType.SOCKS
|
||||||
|
|| config.configType == EConfigType.HTTP
|
||||||
|
) {
|
||||||
et_security?.text =
|
et_security?.text =
|
||||||
Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
|
Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
|
||||||
} else if (config.configType == EConfigType.VLESS) {
|
} else if (config.configType == EConfigType.VLESS) {
|
||||||
@@ -290,14 +286,17 @@ class ServerActivity : BaseActivity() {
|
|||||||
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<*>
|
val list = outbound.settings?.address as List<*>
|
||||||
et_local_address?.text = Utils.getEditable(list.joinToString())
|
et_local_address?.text = Utils.getEditable(list.joinToString(","))
|
||||||
}
|
}
|
||||||
if (outbound.settings?.mtu == null) {
|
if (outbound.settings?.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(outbound.settings?.mtu.toString())
|
||||||
}
|
}
|
||||||
|
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
|
et_obfs_password?.text = Utils.getEditable(outbound.settings?.obfsPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
val securityEncryptions =
|
val securityEncryptions =
|
||||||
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
|
||||||
val security =
|
val security =
|
||||||
@@ -322,7 +321,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
tlsSetting.alpn?.let {
|
tlsSetting.alpn?.let {
|
||||||
val alpnIndex = Utils.arrayFind(
|
val alpnIndex = Utils.arrayFind(
|
||||||
alpns,
|
alpns,
|
||||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
|
||||||
)
|
)
|
||||||
sp_stream_alpn?.setSelection(alpnIndex)
|
sp_stream_alpn?.setSelection(alpnIndex)
|
||||||
}
|
}
|
||||||
@@ -413,8 +412,14 @@ class ServerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
val config =
|
val config =
|
||||||
MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
|
MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
|
||||||
if (config.configType != EConfigType.SOCKS && TextUtils.isEmpty(et_id.text.toString())) {
|
if (config.configType != EConfigType.SOCKS
|
||||||
if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.SHADOWSOCKS) {
|
&& config.configType != EConfigType.HTTP
|
||||||
|
&& TextUtils.isEmpty(et_id.text.toString())
|
||||||
|
) {
|
||||||
|
if (config.configType == EConfigType.TROJAN
|
||||||
|
|| config.configType == EConfigType.SHADOWSOCKS
|
||||||
|
|| config.configType == EConfigType.HYSTERIA2
|
||||||
|
) {
|
||||||
toast(R.string.server_lab_id3)
|
toast(R.string.server_lab_id3)
|
||||||
} else {
|
} else {
|
||||||
toast(R.string.server_lab_id)
|
toast(R.string.server_lab_id)
|
||||||
@@ -446,12 +451,17 @@ class ServerActivity : BaseActivity() {
|
|||||||
wireguard?.peers?.get(0)?.let { _ ->
|
wireguard?.peers?.get(0)?.let { _ ->
|
||||||
savePeer(wireguard, port)
|
savePeer(wireguard, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.outboundBean?.streamSettings?.let {
|
config.outboundBean?.streamSettings?.let {
|
||||||
saveStreamSettings(it)
|
val sni = saveStreamSettings(it)
|
||||||
|
saveTls(it, sni)
|
||||||
}
|
}
|
||||||
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
|
||||||
config.subscriptionId = subscriptionId.orEmpty()
|
config.subscriptionId = subscriptionId.orEmpty()
|
||||||
}
|
}
|
||||||
|
if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
|
config.outboundBean?.settings?.obfsPassword = et_obfs_password?.text?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
MmkvManager.encodeServerConfig(editGuid, config)
|
MmkvManager.encodeServerConfig(editGuid, config)
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
@@ -486,7 +496,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
if (config.configType == EConfigType.SHADOWSOCKS) {
|
if (config.configType == EConfigType.SHADOWSOCKS) {
|
||||||
server.password = et_id.text.toString().trim()
|
server.password = et_id.text.toString().trim()
|
||||||
server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
|
server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
|
||||||
} else if (config.configType == EConfigType.SOCKS) {
|
} else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
|
||||||
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
|
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
|
||||||
server.users = null
|
server.users = null
|
||||||
} else {
|
} else {
|
||||||
@@ -496,7 +506,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
socksUsersBean.pass = et_id.text.toString().trim()
|
socksUsersBean.pass = et_id.text.toString().trim()
|
||||||
server.users = listOf(socksUsersBean)
|
server.users = listOf(socksUsersBean)
|
||||||
}
|
}
|
||||||
} else if (config.configType == EConfigType.TROJAN) {
|
} else if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.HYSTERIA2) {
|
||||||
server.password = et_id.text.toString().trim()
|
server.password = et_id.text.toString().trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -518,36 +528,39 @@ class ServerActivity : BaseActivity() {
|
|||||||
wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean) {
|
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean): String? {
|
||||||
val network = sp_network?.selectedItemPosition ?: return
|
val network = sp_network?.selectedItemPosition ?: return null
|
||||||
val type = sp_header_type?.selectedItemPosition ?: return
|
val type = sp_header_type?.selectedItemPosition ?: return null
|
||||||
val requestHost = et_request_host?.text?.toString()?.trim() ?: return
|
val requestHost = et_request_host?.text?.toString()?.trim() ?: return null
|
||||||
val path = et_path?.text?.toString()?.trim() ?: return
|
val path = et_path?.text?.toString()?.trim() ?: return null
|
||||||
val sniField = et_sni?.text?.toString()?.trim() ?: return
|
|
||||||
val allowInsecureField = sp_allow_insecure?.selectedItemPosition ?: return
|
|
||||||
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
|
||||||
val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: return
|
|
||||||
val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: return
|
|
||||||
val publicKey = et_public_key?.text?.toString()?.trim() ?: return
|
|
||||||
val shortId = et_short_id?.text?.toString()?.trim() ?: return
|
|
||||||
val spiderX = et_spider_x?.text?.toString()?.trim() ?: return
|
|
||||||
|
|
||||||
var sni = streamSetting.populateTransportSettings(
|
val sni = streamSetting.populateTransportSettings(
|
||||||
transport = networks[network],
|
transport = networks[network],
|
||||||
headerType = transportTypes(networks[network])[type],
|
headerType = transportTypes(networks[network])[type],
|
||||||
host = requestHost,
|
host = requestHost,
|
||||||
path = path,
|
path = path,
|
||||||
seed = path,
|
seed = path,
|
||||||
quicSecurity = requestHost,
|
quicSecurity = requestHost,
|
||||||
key = path,
|
key = path,
|
||||||
mode = transportTypes(networks[network])[type],
|
mode = transportTypes(networks[network])[type],
|
||||||
serviceName = path,
|
serviceName = path,
|
||||||
authority = requestHost,
|
authority = requestHost,
|
||||||
)
|
)
|
||||||
if (sniField.isNotBlank()) {
|
|
||||||
sni = sniField
|
return sni
|
||||||
}
|
}
|
||||||
val allowInsecure = if (allowinsecures[allowInsecureField].isBlank()) {
|
|
||||||
|
private fun saveTls(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean, sni: String?) {
|
||||||
|
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
|
||||||
|
val sniField = et_sni?.text?.toString()?.trim()
|
||||||
|
val allowInsecureField = sp_allow_insecure?.selectedItemPosition
|
||||||
|
val utlsIndex = sp_stream_fingerprint?.selectedItemPosition ?: 0
|
||||||
|
val alpnIndex = sp_stream_alpn?.selectedItemPosition ?: 0
|
||||||
|
val publicKey = et_public_key?.text?.toString()
|
||||||
|
val shortId = et_short_id?.text?.toString()
|
||||||
|
val spiderX = et_spider_x?.text?.toString()
|
||||||
|
|
||||||
|
val allowInsecure = if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
|
||||||
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
|
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
|
||||||
} else {
|
} else {
|
||||||
allowinsecures[allowInsecureField].toBoolean()
|
allowinsecures[allowInsecureField].toBoolean()
|
||||||
@@ -556,7 +569,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
streamSetting.populateTlsSettings(
|
streamSetting.populateTlsSettings(
|
||||||
streamSecurity = streamSecuritys[streamSecurity],
|
streamSecurity = streamSecuritys[streamSecurity],
|
||||||
allowInsecure = allowInsecure,
|
allowInsecure = allowInsecure,
|
||||||
sni = sni,
|
sni = sniField ?: sni ?: "",
|
||||||
fingerprint = uTlsItems[utlsIndex],
|
fingerprint = uTlsItems[utlsIndex],
|
||||||
alpns = alpns[alpnIndex],
|
alpns = alpns[alpnIndex],
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
@@ -590,7 +603,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
*/
|
*/
|
||||||
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 (settingsStorage?.decodeBool(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) { _, _ ->
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ 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.*
|
|
||||||
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.ServerConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.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
|
||||||
@@ -23,13 +23,11 @@ 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?) {
|
||||||
@@ -54,7 +52,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
*/
|
*/
|
||||||
private fun bindingServer(config: ServerConfig): Boolean {
|
private fun bindingServer(config: ServerConfig): 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()) {
|
if (raw.isNullOrBlank()) {
|
||||||
binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty()))
|
binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty()))
|
||||||
} else {
|
} else {
|
||||||
@@ -81,7 +79,7 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val v2rayConfig = try {
|
val v2rayConfig = try {
|
||||||
Gson().fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
|
JsonUtil.fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
|
||||||
} 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()
|
||||||
@@ -89,11 +87,11 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
|
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
|
||||||
config.remarks = v2rayConfig.remarks ?: binding.etRemarks.text.toString().trim()
|
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString()
|
||||||
config.fullConfig = v2rayConfig
|
config.fullConfig = v2rayConfig
|
||||||
|
|
||||||
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
|
||||||
@@ -105,14 +103,14 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
private fun deleteServer(): Boolean {
|
private fun deleteServer(): Boolean {
|
||||||
if (editGuid.isNotEmpty()) {
|
if (editGuid.isNotEmpty()) {
|
||||||
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)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) {_, _ ->
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -139,10 +137,12 @@ class ServerCustomConfigActivity : BaseActivity() {
|
|||||||
deleteServer()
|
deleteServer()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.save_config -> {
|
R.id.save_config -> {
|
||||||
saveServer()
|
saveServer()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.service.SubscriptionUpdater
|
import com.v2ray.ang.service.SubscriptionUpdater
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
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,11 +172,11 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, "VPN"))
|
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, VPN))
|
||||||
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
|
||||||
fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false)
|
||||||
localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
|
||||||
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
|
||||||
|
|
||||||
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false))
|
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false))
|
||||||
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)
|
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)
|
||||||
@@ -198,7 +190,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
|
||||||
|
|
||||||
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
autoUpdateInterval?.summary = settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL,AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
autoUpdateInterval?.summary =
|
||||||
|
settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
|
||||||
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
|
||||||
|
|
||||||
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
|
||||||
@@ -237,6 +230,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -251,7 +245,6 @@ class SettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -266,7 +259,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
|
||||||
localDns?.isEnabled = vpn
|
localDns?.isEnabled = vpn
|
||||||
@@ -350,12 +343,15 @@ class SettingsActivity : BaseActivity() {
|
|||||||
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
|
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFragmentPackets(value: String?) {
|
private fun updateFragmentPackets(value: String?) {
|
||||||
fragmentPackets?.summary = value.toString()
|
fragmentPackets?.summary = value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFragmentLength(value: String?) {
|
private fun updateFragmentLength(value: String?) {
|
||||||
fragmentLength?.summary = value.toString()
|
fragmentLength?.summary = value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFragmentInterval(value: String?) {
|
private fun updateFragmentInterval(value: String?) {
|
||||||
fragmentInterval?.summary = value.toString()
|
fragmentInterval?.summary = value.toString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ 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
|
||||||
@@ -18,12 +16,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SubEditActivity : BaseActivity() {
|
class SubEditActivity : BaseActivity() {
|
||||||
private val binding by lazy {ActivitySubEditBinding.inflate(layoutInflater)}
|
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
|
||||||
|
|
||||||
var del_config: MenuItem? = null
|
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,20 +67,15 @@ 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)
|
||||||
@@ -88,7 +86,7 @@ class SubEditActivity : BaseActivity() {
|
|||||||
// return false
|
// return false
|
||||||
// }
|
// }
|
||||||
|
|
||||||
subStorage?.encode(subId, Gson().toJson(subItem))
|
MmkvManager.encodeSubscription(editSubId, subItem)
|
||||||
toast(R.string.toast_success)
|
toast(R.string.toast_success)
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
@@ -100,18 +98,18 @@ class SubEditActivity : BaseActivity() {
|
|||||||
private fun deleteServer(): Boolean {
|
private fun deleteServer(): Boolean {
|
||||||
if (editSubId.isNotEmpty()) {
|
if (editSubId.isNotEmpty()) {
|
||||||
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
MmkvManager.removeSubscription(editSubId)
|
MmkvManager.removeSubscription(editSubId)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
finish()
|
finish()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) {_, _ ->
|
}
|
||||||
// do nothing
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
}
|
// do nothing
|
||||||
.show()
|
}
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -133,10 +131,12 @@ class SubEditActivity : BaseActivity() {
|
|||||||
deleteServer()
|
deleteServer()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.save_config -> {
|
R.id.save_config -> {
|
||||||
saveServer()
|
saveServer()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ 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.helper.SimpleItemTouchHelperCallback
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -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.helper.ItemTouchHelperAdapter
|
||||||
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager
|
||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
|
import com.v2ray.ang.util.SettingsManager
|
||||||
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) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.ListView
|
|
||||||
import java.util.ArrayList
|
|
||||||
import com.v2ray.ang.R
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import com.tencent.mmkv.MMKV
|
import android.view.View
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.ListView
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
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.util.MmkvManager
|
||||||
|
|
||||||
@@ -23,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)
|
||||||
@@ -33,14 +28,16 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val adapter = ArrayAdapter(this,
|
val adapter = ArrayAdapter(
|
||||||
android.R.layout.simple_list_item_single_choice, lstData)
|
this,
|
||||||
|
android.R.layout.simple_list_item_single_choice, lstData
|
||||||
|
)
|
||||||
listview = findViewById<View>(R.id.listview) as ListView
|
listview = findViewById<View>(R.id.listview) as ListView
|
||||||
listview?.adapter = adapter
|
listview?.adapter = adapter
|
||||||
|
|
||||||
@@ -88,7 +85,7 @@ class TaskerActivity : BaseActivity() {
|
|||||||
|
|
||||||
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
|
intent.putExtra(AppConfig.TASKER_EXTRA_BUNDLE, extraBundle)
|
||||||
intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
|
intent.putExtra(AppConfig.TASKER_EXTRA_STRING_BLURB, blurb)
|
||||||
setResult(AppCompatActivity.RESULT_OK, intent)
|
setResult(RESULT_OK, intent)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,10 +100,12 @@ class TaskerActivity : BaseActivity() {
|
|||||||
R.id.del_config -> {
|
R.id.del_config -> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.save_config -> {
|
R.id.save_config -> {
|
||||||
confirmFinish()
|
confirmFinish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ class UrlSchemeActivity : BaseActivity() {
|
|||||||
when (data?.host) {
|
when (data?.host) {
|
||||||
"install-config" -> {
|
"install-config" -> {
|
||||||
val uri: Uri? = intent.data
|
val uri: Uri? = intent.data
|
||||||
val shareUrl = uri?.getQueryParameter("url") ?: ""
|
val shareUrl = uri?.getQueryParameter("url").orEmpty()
|
||||||
parseUri(shareUrl, uri?.fragment)
|
parseUri(shareUrl, uri?.fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
"install-sub" -> {
|
"install-sub" -> {
|
||||||
val uri: Uri? = intent.data
|
val uri: Uri? = intent.data
|
||||||
val shareUrl = uri?.getQueryParameter("url") ?: ""
|
val shareUrl = uri?.getQueryParameter("url").orEmpty()
|
||||||
parseUri(shareUrl, uri?.fragment)
|
parseUri(shareUrl, uri?.fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -31,6 +30,8 @@ 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.util.MmkvManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
|
import com.v2ray.ang.util.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
|
||||||
@@ -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")
|
||||||
@@ -83,6 +82,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.download_file -> {
|
R.id.download_file -> {
|
||||||
downloadGeoFiles()
|
downloadGeoFiles()
|
||||||
true
|
true
|
||||||
@@ -100,24 +100,24 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
RxPermissions(this)
|
RxPermissions(this)
|
||||||
.request(permission)
|
.request(permission)
|
||||||
.subscribe {
|
.subscribe {
|
||||||
if (it) {
|
if (it) {
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
intent.type = "*/*"
|
intent.type = "*/*"
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
chooseFile.launch(
|
chooseFile.launch(
|
||||||
Intent.createChooser(
|
Intent.createChooser(
|
||||||
intent,
|
intent,
|
||||||
getString(R.string.title_file_chooser)
|
getString(R.string.title_file_chooser)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
} catch (ex: android.content.ActivityNotFoundException) {
|
||||||
} catch (ex: android.content.ActivityNotFoundException) {
|
toast(R.string.toast_require_file_manager)
|
||||||
toast(R.string.toast_require_file_manager)
|
}
|
||||||
}
|
} else
|
||||||
} else
|
toast(R.string.toast_permission_denied)
|
||||||
toast(R.string.toast_permission_denied)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val chooseFile =
|
private val chooseFile =
|
||||||
@@ -137,7 +137,7 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
toast(R.string.msg_remark_is_duplicate)
|
toast(R.string.msg_remark_is_duplicate)
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
assetStorage?.encode(assetId, Gson().toJson(assetItem))
|
MmkvManager.encodeAsset(assetId, assetItem)
|
||||||
copyFile(uri)
|
copyFile(uri)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
toast(R.string.toast_asset_copy_failed)
|
toast(R.string.toast_asset_copy_failed)
|
||||||
@@ -177,7 +177,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)
|
||||||
|
|
||||||
@@ -214,7 +214,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
|
||||||
}
|
}
|
||||||
@@ -237,15 +237,18 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
conn?.disconnect()
|
conn?.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
|
private fun addBuiltInGeoItems(assets: List<Pair<String, AssetUrlItem>>): List<Pair<String, AssetUrlItem>> {
|
||||||
val list = mutableListOf<Pair<String, AssetUrlItem>>()
|
val list = mutableListOf<Pair<String, AssetUrlItem>>()
|
||||||
builtInGeoFiles
|
builtInGeoFiles
|
||||||
.filter { geoFile -> assets.none { it.second.remarks == geoFile } }
|
.filter { geoFile -> assets.none { it.second.remarks == geoFile } }
|
||||||
.forEach {
|
.forEach {
|
||||||
list.add(Utils.getUuid() to AssetUrlItem(
|
list.add(
|
||||||
it,
|
Utils.getUuid() to AssetUrlItem(
|
||||||
AppConfig.GeoUrl + it
|
it,
|
||||||
))
|
AppConfig.GeoUrl + it
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return list + assets
|
return list + assets
|
||||||
@@ -257,14 +260,15 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
ItemRecyclerUserAssetBinding.inflate(
|
ItemRecyclerUserAssetBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
parent,
|
parent,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
|
||||||
var assets = MmkvManager.decodeAssetUrls();
|
var assets = MmkvManager.decodeAssetUrls()
|
||||||
assets = addBuiltInGeoItems(assets);
|
assets = addBuiltInGeoItems(assets)
|
||||||
val item = assets.getOrNull(position) ?: return
|
val item = assets.getOrNull(position) ?: return
|
||||||
// file with name == item.second.remarks
|
// file with name == item.second.remarks
|
||||||
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
val file = extDir.listFiles()?.find { it.name == item.second.remarks }
|
||||||
@@ -300,8 +304,8 @@ class UserAssetActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
var assets = MmkvManager.decodeAssetUrls();
|
var assets = MmkvManager.decodeAssetUrls()
|
||||||
assets = addBuiltInGeoItems(assets);
|
assets = addBuiltInGeoItems(assets)
|
||||||
return assets.size
|
return assets.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ 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
|
||||||
@@ -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
|
||||||
@@ -112,7 +106,7 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
MmkvManager.removeAssetUrl(editAssetId)
|
MmkvManager.removeAssetUrl(editAssetId)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.no) {_, _ ->
|
.setNegativeButton(android.R.string.no) { _, _ ->
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
@@ -137,10 +131,12 @@ class UserAssetUrlActivity : BaseActivity() {
|
|||||||
deleteServer()
|
deleteServer()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.save_config -> {
|
R.id.save_config -> {
|
||||||
saveServer()
|
saveServer()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,17 +4,17 @@ import android.content.Context
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonPrimitive
|
import com.google.gson.JsonPrimitive
|
||||||
import com.google.gson.JsonSerializationContext
|
import com.google.gson.JsonSerializationContext
|
||||||
import com.google.gson.JsonSerializer
|
import com.google.gson.JsonSerializer
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.HY2
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.dto.*
|
import com.v2ray.ang.dto.*
|
||||||
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
|
import com.v2ray.ang.util.fmt.Hysteria2Fmt
|
||||||
import com.v2ray.ang.util.fmt.ShadowsocksFmt
|
import com.v2ray.ang.util.fmt.ShadowsocksFmt
|
||||||
import com.v2ray.ang.util.fmt.SocksFmt
|
import com.v2ray.ang.util.fmt.SocksFmt
|
||||||
import com.v2ray.ang.util.fmt.TrojanFmt
|
import com.v2ray.ang.util.fmt.TrojanFmt
|
||||||
@@ -22,191 +22,17 @@ import com.v2ray.ang.util.fmt.VlessFmt
|
|||||||
import com.v2ray.ang.util.fmt.VmessFmt
|
import com.v2ray.ang.util.fmt.VmessFmt
|
||||||
import com.v2ray.ang.util.fmt.WireguardFmt
|
import com.v2ray.ang.util.fmt.WireguardFmt
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
import java.net.URI
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object AngConfigManager {
|
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...
|
* parse config form qrcode or...
|
||||||
*/
|
*/
|
||||||
private fun parseConfig(
|
private fun parseConfig(
|
||||||
str: String?,
|
str: String?,
|
||||||
subid: String,
|
subid: String,
|
||||||
|
subItem: SubscriptionItem?,
|
||||||
removedSelectedServer: ServerConfig?
|
removedSelectedServer: ServerConfig?
|
||||||
): Int {
|
): Int {
|
||||||
try {
|
try {
|
||||||
@@ -215,17 +41,19 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
||||||
VmessFmt.parseVmess(str)
|
VmessFmt.parse(str)
|
||||||
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
|
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
|
||||||
ShadowsocksFmt.parseShadowsocks(str)
|
ShadowsocksFmt.parse(str)
|
||||||
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
|
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
|
||||||
SocksFmt.parseSocks(str)
|
SocksFmt.parse(str)
|
||||||
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
|
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
|
||||||
TrojanFmt.parseTrojan(str)
|
TrojanFmt.parse(str)
|
||||||
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
|
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
|
||||||
VlessFmt.parseVless(str)
|
VlessFmt.parse(str)
|
||||||
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
|
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
|
||||||
WireguardFmt.parseWireguard(str)
|
WireguardFmt.parse(str)
|
||||||
|
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
|
||||||
|
Hysteria2Fmt.parse(str)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -233,6 +61,13 @@ object AngConfigManager {
|
|||||||
if (config == null) {
|
if (config == null) {
|
||||||
return R.string.toast_incorrect_protocol
|
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
|
config.subscriptionId = subid
|
||||||
val guid = MmkvManager.encodeServerConfig("", config)
|
val guid = MmkvManager.encodeServerConfig("", config)
|
||||||
if (removedSelectedServer != null &&
|
if (removedSelectedServer != null &&
|
||||||
@@ -243,7 +78,7 @@ object AngConfigManager {
|
|||||||
?.getServerPort() == removedSelectedServer.getProxyOutbound()
|
?.getServerPort() == removedSelectedServer.getProxyOutbound()
|
||||||
?.getServerPort()
|
?.getServerPort()
|
||||||
) {
|
) {
|
||||||
mainStorage?.encode(KEY_SELECTED_SERVER, guid)
|
MmkvManager.setSelectServer(guid)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -264,9 +99,11 @@ object AngConfigManager {
|
|||||||
EConfigType.CUSTOM -> ""
|
EConfigType.CUSTOM -> ""
|
||||||
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
|
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
|
||||||
EConfigType.SOCKS -> SocksFmt.toUri(config)
|
EConfigType.SOCKS -> SocksFmt.toUri(config)
|
||||||
|
EConfigType.HTTP -> ""
|
||||||
EConfigType.VLESS -> VlessFmt.toUri(config)
|
EConfigType.VLESS -> VlessFmt.toUri(config)
|
||||||
EConfigType.TROJAN -> TrojanFmt.toUri(config)
|
EConfigType.TROJAN -> TrojanFmt.toUri(config)
|
||||||
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
||||||
|
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -353,40 +190,7 @@ object AngConfigManager {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
|
||||||
// * 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)
|
var count = parseBatchConfig(Utils.decode(server), subid, append)
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
count = parseBatchConfig(server, subid, append)
|
count = parseBatchConfig(server, subid, append)
|
||||||
@@ -416,7 +220,7 @@ object AngConfigManager {
|
|||||||
servers.lines()
|
servers.lines()
|
||||||
.forEach { str ->
|
.forEach { str ->
|
||||||
if (str.startsWith(AppConfig.PROTOCOL_HTTP) || str.startsWith(AppConfig.PROTOCOL_HTTPS)) {
|
if (str.startsWith(AppConfig.PROTOCOL_HTTP) || str.startsWith(AppConfig.PROTOCOL_HTTPS)) {
|
||||||
count += MmkvManager.importUrlAsSubscription(str)
|
count += importUrlAsSubscription(str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
@@ -434,7 +238,7 @@ object AngConfigManager {
|
|||||||
val removedSelectedServer =
|
val removedSelectedServer =
|
||||||
if (!TextUtils.isEmpty(subid) && !append) {
|
if (!TextUtils.isEmpty(subid) && !append) {
|
||||||
MmkvManager.decodeServerConfig(
|
MmkvManager.decodeServerConfig(
|
||||||
mainStorage?.decodeString(KEY_SELECTED_SERVER) ?: ""
|
MmkvManager.getSelectServer().orEmpty()
|
||||||
)?.let {
|
)?.let {
|
||||||
if (it.subscriptionId == subid) {
|
if (it.subscriptionId == subid) {
|
||||||
return@let it
|
return@let it
|
||||||
@@ -448,11 +252,12 @@ object AngConfigManager {
|
|||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
var count = 0
|
var count = 0
|
||||||
servers.lines()
|
servers.lines()
|
||||||
.reversed()
|
.reversed()
|
||||||
.forEach {
|
.forEach {
|
||||||
val resId = parseConfig(it, subid, removedSelectedServer)
|
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
|
||||||
if (resId == 0) {
|
if (resId == 0) {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -473,34 +278,21 @@ object AngConfigManager {
|
|||||||
&& server.contains("routing")
|
&& server.contains("routing")
|
||||||
) {
|
) {
|
||||||
try {
|
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> =
|
val serverList: Array<Any> =
|
||||||
Gson().fromJson(server, Array<Any>::class.java)
|
JsonUtil.fromJson(server, Array<Any>::class.java)
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
if (serverList.isNotEmpty()) {
|
||||||
var count = 0
|
var count = 0
|
||||||
for (srv in serverList.reversed()) {
|
for (srv in serverList.reversed()) {
|
||||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
val config = ServerConfig.create(EConfigType.CUSTOM)
|
||||||
config.fullConfig =
|
config.fullConfig =
|
||||||
Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
|
JsonUtil.fromJson(JsonUtil.toJson(srv), V2rayConfig::class.java)
|
||||||
config.remarks = config.fullConfig?.remarks
|
config.remarks = config.fullConfig?.remarks
|
||||||
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
|
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
|
||||||
.toString())
|
.toString())
|
||||||
config.subscriptionId = subid
|
config.subscriptionId = subid
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
serverRawStorage?.encode(key, gson.toJson(srv))
|
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv))
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
@@ -509,14 +301,31 @@ object AngConfigManager {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
// For compatibility
|
try {
|
||||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
// For compatibility
|
||||||
config.subscriptionId = subid
|
val config = ServerConfig.create(EConfigType.CUSTOM)
|
||||||
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
|
config.subscriptionId = subid
|
||||||
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
||||||
serverRawStorage?.encode(key, server)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
return 1
|
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
|
||||||
|
config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
||||||
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
|
return 1
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -551,19 +360,17 @@ object AngConfigManager {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
Log.d(AppConfig.ANG_PACKAGE, url)
|
Log.d(AppConfig.ANG_PACKAGE, url)
|
||||||
|
|
||||||
var configText = try {
|
var configText = try {
|
||||||
Utils.getUrlContentWithCustomUserAgent(url)
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
|
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
if (configText.isEmpty()) {
|
if (configText.isEmpty()) {
|
||||||
configText = try {
|
configText = try {
|
||||||
val httpPort = Utils.parseInt(
|
Utils.getUrlContentWithCustomUserAgent(url)
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
|
|
||||||
AppConfig.PORT_HTTP.toInt()
|
|
||||||
)
|
|
||||||
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
""
|
""
|
||||||
@@ -589,4 +396,19 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
return count
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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()
|
||||||
|
return gsonPre.toJson(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,63 @@
|
|||||||
package com.v2ray.ang.util
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
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.AssetUrlItem
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
import com.v2ray.ang.dto.ServerAffiliationInfo
|
import com.v2ray.ang.dto.ServerAffiliationInfo
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
object MmkvManager {
|
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) }
|
//region private
|
||||||
|
|
||||||
|
private const val ID_MAIN = "MAIN"
|
||||||
|
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
|
||||||
|
private const val ID_PROFILE_CONFIG = "PROFILE_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 mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||||
val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, 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 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 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) }
|
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 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 assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
|
||||||
val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, 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> {
|
fun decodeServerList(): MutableList<String> {
|
||||||
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
|
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
||||||
return if (json.isNullOrBlank()) {
|
return if (json.isNullOrBlank()) {
|
||||||
mutableListOf()
|
mutableListOf()
|
||||||
} else {
|
} else {
|
||||||
Gson().fromJson(json, Array<String>::class.java).toMutableList()
|
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,33 +65,33 @@ object MmkvManager {
|
|||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val json = serverStorage?.decodeString(guid)
|
val json = serverStorage.decodeString(guid)
|
||||||
if (json.isNullOrBlank()) {
|
if (json.isNullOrBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return Gson().fromJson(json, ServerConfig::class.java)
|
return JsonUtil.fromJson(json, ServerConfig::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decodeProfileConfig(guid: String): ProfileItem? {
|
fun decodeProfileConfig(guid: String): ProfileItem? {
|
||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val json = profileStorage?.decodeString(guid)
|
val json = profileStorage.decodeString(guid)
|
||||||
if (json.isNullOrBlank()) {
|
if (json.isNullOrBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return Gson().fromJson(json, ProfileItem::class.java)
|
return JsonUtil.fromJson(json, ProfileItem::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encodeServerConfig(guid: String, config: ServerConfig): String {
|
fun encodeServerConfig(guid: String, config: ServerConfig): String {
|
||||||
val key = guid.ifBlank { Utils.getUuid() }
|
val key = guid.ifBlank { Utils.getUuid() }
|
||||||
serverStorage?.encode(key, Gson().toJson(config))
|
serverStorage.encode(key, JsonUtil.toJson(config))
|
||||||
val serverList = decodeServerList()
|
val serverList = decodeServerList()
|
||||||
if (!serverList.contains(key)) {
|
if (!serverList.contains(key)) {
|
||||||
serverList.add(0, key)
|
serverList.add(0, key)
|
||||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
encodeServerList(serverList)
|
||||||
if (mainStorage?.decodeString(KEY_SELECTED_SERVER).isNullOrBlank()) {
|
if (getSelectServer().isNullOrBlank()) {
|
||||||
mainStorage?.encode(KEY_SELECTED_SERVER, key)
|
mainStorage.encode(KEY_SELECTED_SERVER, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val profile = ProfileItem(
|
val profile = ProfileItem(
|
||||||
@@ -79,7 +101,7 @@ object MmkvManager {
|
|||||||
server = config.getProxyOutbound()?.getServerAddress(),
|
server = config.getProxyOutbound()?.getServerAddress(),
|
||||||
serverPort = config.getProxyOutbound()?.getServerPort(),
|
serverPort = config.getProxyOutbound()?.getServerPort(),
|
||||||
)
|
)
|
||||||
profileStorage?.encode(key, Gson().toJson(profile))
|
profileStorage.encode(key, JsonUtil.toJson(profile))
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,22 +109,22 @@ object MmkvManager {
|
|||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (mainStorage?.decodeString(KEY_SELECTED_SERVER) == guid) {
|
if (getSelectServer() == guid) {
|
||||||
mainStorage?.remove(KEY_SELECTED_SERVER)
|
mainStorage.remove(KEY_SELECTED_SERVER)
|
||||||
}
|
}
|
||||||
val serverList = decodeServerList()
|
val serverList = decodeServerList()
|
||||||
serverList.remove(guid)
|
serverList.remove(guid)
|
||||||
mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
encodeServerList(serverList)
|
||||||
serverStorage?.remove(guid)
|
serverStorage.remove(guid)
|
||||||
profileStorage?.remove(guid)
|
profileStorage.remove(guid)
|
||||||
serverAffStorage?.remove(guid)
|
serverAffStorage.remove(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeServerViaSubid(subid: String) {
|
fun removeServerViaSubid(subid: String) {
|
||||||
if (subid.isBlank()) {
|
if (subid.isBlank()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
serverStorage?.allKeys()?.forEach { key ->
|
serverStorage.allKeys()?.forEach { key ->
|
||||||
decodeServerConfig(key)?.let { config ->
|
decodeServerConfig(key)?.let { config ->
|
||||||
if (config.subscriptionId == subid) {
|
if (config.subscriptionId == subid) {
|
||||||
removeServer(key)
|
removeServer(key)
|
||||||
@@ -115,11 +137,11 @@ object MmkvManager {
|
|||||||
if (guid.isBlank()) {
|
if (guid.isBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val json = serverAffStorage?.decodeString(guid)
|
val json = serverAffStorage.decodeString(guid)
|
||||||
if (json.isNullOrBlank()) {
|
if (json.isNullOrBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return Gson().fromJson(json, ServerAffiliationInfo::class.java)
|
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
|
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
|
||||||
@@ -128,69 +150,23 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
|
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
|
||||||
aff.testDelayMillis = testResult
|
aff.testDelayMillis = testResult
|
||||||
serverAffStorage?.encode(guid, Gson().toJson(aff))
|
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearAllTestDelayResults(keys: List<String>?) {
|
fun clearAllTestDelayResults(keys: List<String>?) {
|
||||||
keys?.forEach { key ->
|
keys?.forEach { key ->
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
aff.testDelayMillis = 0
|
aff.testDelayMillis = 0
|
||||||
serverAffStorage?.encode(key, Gson().toJson(aff))
|
serverAffStorage.encode(key, JsonUtil.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() {
|
fun removeAllServer() {
|
||||||
mainStorage?.clearAll()
|
mainStorage.clearAll()
|
||||||
serverStorage?.clearAll()
|
serverStorage.clearAll()
|
||||||
profileStorage?.clearAll()
|
profileStorage.clearAll()
|
||||||
serverAffStorage?.clearAll()
|
serverAffStorage.clearAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeInvalidServer(guid: String) {
|
fun removeInvalidServer(guid: String) {
|
||||||
@@ -201,7 +177,7 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
serverAffStorage?.allKeys()?.forEach { key ->
|
serverAffStorage.allKeys()?.forEach { key ->
|
||||||
decodeServerAffiliationInfo(key)?.let { aff ->
|
decodeServerAffiliationInfo(key)?.let { aff ->
|
||||||
if (aff.testDelayMillis < 0L) {
|
if (aff.testDelayMillis < 0L) {
|
||||||
removeServer(key)
|
removeServer(key)
|
||||||
@@ -211,22 +187,138 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortByTestResults() {
|
fun encodeServerRaw(guid: String, config: String) {
|
||||||
data class ServerDelay(var guid: String, var testDelayMillis: Long)
|
serverRawStorage.encode(guid, config)
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeServerRaw(guid: String): String? {
|
||||||
|
return serverRawStorage.decodeString(guid) ?: return null
|
||||||
|
}
|
||||||
|
|
||||||
|
//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())
|
||||||
|
settingsStorage.encode(PREF_ROUTING_RULESET, "")
|
||||||
|
else
|
||||||
|
settingsStorage.encode(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Others
|
||||||
|
|
||||||
|
fun encodeStartOnBoot(startOnBoot: Boolean) {
|
||||||
|
settingsStorage.encode(PREF_IS_BOOTED, startOnBoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeStartOnBoot(): Boolean {
|
||||||
|
return settingsStorage.decodeBool(PREF_IS_BOOTED, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
99
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/PluginUtil.kt
Normal file
99
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/PluginUtil.kt
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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.ServerConfig
|
||||||
|
import com.v2ray.ang.service.ProcessService
|
||||||
|
import com.v2ray.ang.util.fmt.Hysteria2Fmt
|
||||||
|
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 lateinit var procService: ProcessService
|
||||||
|
|
||||||
|
// fun initPlugin(name: String): PluginManager.InitResult {
|
||||||
|
// return PluginManager.init(name)!!
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun runPlugin(context: Context, config: ServerConfig?, domainPort: String?) {
|
||||||
|
Log.d(TAG, "runPlugin")
|
||||||
|
|
||||||
|
val outbound = config?.getProxyOutbound() ?: return
|
||||||
|
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) {
|
||||||
|
val configFile = genConfigHy2(context, config, domainPort) ?: return
|
||||||
|
val cmd = genCmdHy2(context, configFile)
|
||||||
|
|
||||||
|
procService = ProcessService()
|
||||||
|
procService.runProcess(context, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopPlugin() {
|
||||||
|
stopHy2()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun realPingHy2(context: Context, config: ServerConfig?): Long {
|
||||||
|
Log.d(TAG, "realPingHy2")
|
||||||
|
val retFailure = -1L
|
||||||
|
|
||||||
|
val outbound = config?.getProxyOutbound() ?: return retFailure
|
||||||
|
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, 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: ServerConfig, 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.v2ray.ang.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.TextUtils
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
||||||
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
|
import com.v2ray.ang.util.MmkvManager.decodeProfileConfig
|
||||||
|
import com.v2ray.ang.util.MmkvManager.decodeServerConfig
|
||||||
|
import com.v2ray.ang.util.MmkvManager.decodeServerList
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
|
import com.v2ray.ang.util.Utils.parseInt
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
|
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 = when (index) {
|
||||||
|
0 -> "custom_routing_white"
|
||||||
|
1 -> "custom_routing_black"
|
||||||
|
2 -> "custom_routing_global"
|
||||||
|
else -> "custom_routing_white"
|
||||||
|
}
|
||||||
|
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?.any {
|
||||||
|
it.enabled
|
||||||
|
&& (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?): ServerConfig? {
|
||||||
|
if (remarks == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val serverList = decodeServerList()
|
||||||
|
for (guid in serverList) {
|
||||||
|
val profile = decodeProfileConfig(guid)
|
||||||
|
if (profile != null && profile.remarks == remarks) {
|
||||||
|
return decodeServerConfig(guid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSocksPort(): Int {
|
||||||
|
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHttpPort(): Int {
|
||||||
|
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,22 +17,20 @@ 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 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.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.service.V2RayServiceManager
|
import com.v2ray.ang.service.V2RayServiceManager
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.*
|
import java.net.*
|
||||||
import java.util.*
|
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
|
||||||
*
|
*
|
||||||
@@ -97,7 +95,7 @@ object Utils {
|
|||||||
* base64 decode
|
* base64 decode
|
||||||
*/
|
*/
|
||||||
fun decode(text: String?): String {
|
fun decode(text: String?): String {
|
||||||
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) } ?: ""
|
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -140,7 +138,7 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getVpnDnsServers(): List<String> {
|
fun getVpnDnsServers(): List<String> {
|
||||||
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS)?:AppConfig.DNS_VPN
|
val vpnDns = settingsStorage?.decodeString(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
|
||||||
}
|
}
|
||||||
@@ -204,7 +202,8 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isIpv4Address(value: String): Boolean {
|
fun isIpv4Address(value: String): Boolean {
|
||||||
val regV4 = Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
val regV4 =
|
||||||
|
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
|
||||||
return regV4.matches(value)
|
return regV4.matches(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +213,8 @@ object Utils {
|
|||||||
addr = addr.drop(1)
|
addr = addr.drop(1)
|
||||||
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
|
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
|
||||||
}
|
}
|
||||||
val regV6 = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
|
val regV6 =
|
||||||
|
Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
|
||||||
return regV6.matches(addr)
|
return regV6.matches(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +244,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
|
||||||
}
|
}
|
||||||
@@ -300,8 +300,7 @@ object Utils {
|
|||||||
* readTextFromAssets
|
* readTextFromAssets
|
||||||
*/
|
*/
|
||||||
fun readTextFromAssets(context: Context?, fileName: String): String {
|
fun readTextFromAssets(context: Context?, fileName: String): String {
|
||||||
if(context == null)
|
if (context == null) {
|
||||||
{
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
val content = context.assets.open(fileName).bufferedReader().use {
|
val content = context.assets.open(fileName).bufferedReader().use {
|
||||||
@@ -314,7 +313,7 @@ object Utils {
|
|||||||
if (context == null)
|
if (context == null)
|
||||||
return ""
|
return ""
|
||||||
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
|
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
|
||||||
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
|
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
|
||||||
return extDir.absolutePath
|
return extDir.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +360,7 @@ object Utils {
|
|||||||
url.openConnection(
|
url.openConnection(
|
||||||
Proxy(
|
Proxy(
|
||||||
Proxy.Type.HTTP,
|
Proxy.Type.HTTP,
|
||||||
InetSocketAddress("127.0.0.1", httpPort)
|
InetSocketAddress(LOOPBACK, httpPort)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -370,8 +369,10 @@ object Utils {
|
|||||||
conn.setRequestProperty("Connection", "close")
|
conn.setRequestProperty("Connection", "close")
|
||||||
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
|
||||||
url.userInfo?.let {
|
url.userInfo?.let {
|
||||||
conn.setRequestProperty("Authorization",
|
conn.setRequestProperty(
|
||||||
"Basic ${encode(urlDecode(it))}")
|
"Authorization",
|
||||||
|
"Basic ${encode(urlDecode(it))}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
conn.useCaches = false
|
conn.useCaches = false
|
||||||
return conn.inputStream.use {
|
return conn.inputStream.use {
|
||||||
@@ -393,7 +394,7 @@ object Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getIpv6Address(address: String?): String {
|
fun getIpv6Address(address: String?): String {
|
||||||
if(address == null){
|
if (address == null) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
|
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
|
||||||
@@ -427,8 +428,8 @@ object Utils {
|
|||||||
|
|
||||||
fun fixIllegalUrl(str: String): String {
|
fun fixIllegalUrl(str: String): String {
|
||||||
return str
|
return str
|
||||||
.replace(" ","%20")
|
.replace(" ", "%20")
|
||||||
.replace("|","%7C")
|
.replace("|", "%7C")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeWhiteSpace(str: String?): String? {
|
fun removeWhiteSpace(str: String?): String? {
|
||||||
@@ -452,6 +453,17 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package com.v2ray.ang.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
|
||||||
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.AppConfig.GEOIP_CN
|
||||||
|
import com.v2ray.ang.AppConfig.GEOSITE_CN
|
||||||
|
import com.v2ray.ang.AppConfig.LOOPBACK
|
||||||
|
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||||
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
|
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
|
||||||
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
|
||||||
@@ -14,89 +17,69 @@ import com.v2ray.ang.AppConfig.TAG_FRAGMENT
|
|||||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||||
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.dto.ConfigResult
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ERoutingMode
|
import com.v2ray.ang.dto.RulesetItem
|
||||||
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
|
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
|
||||||
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
|
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
|
|
||||||
object V2rayConfigUtil {
|
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)
|
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成v2ray的客户端配置文件
|
|
||||||
*/
|
|
||||||
fun getV2rayConfig(context: Context, guid: String): Result {
|
|
||||||
try {
|
try {
|
||||||
val config = MmkvManager.decodeServerConfig(guid) ?: return Result(false, "")
|
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
|
||||||
if (config.configType == EConfigType.CUSTOM) {
|
if (config.configType == EConfigType.CUSTOM) {
|
||||||
val raw = serverRawStorage?.decodeString(guid)
|
val raw = MmkvManager.decodeServerRaw(guid)
|
||||||
val customConfig = if (raw.isNullOrBlank()) {
|
val customConfig = if (raw.isNullOrBlank()) {
|
||||||
config.fullConfig?.toPrettyPrinting() ?: return Result(false, "")
|
config.fullConfig?.toPrettyPrinting() ?: return ConfigResult(false)
|
||||||
} else {
|
} else {
|
||||||
raw
|
raw
|
||||||
}
|
}
|
||||||
//Log.d(ANG_PACKAGE, customConfig)
|
val domainPort = config.getProxyOutbound()?.getServerAddressAndPort()
|
||||||
return Result(true, customConfig)
|
return ConfigResult(true, guid, customConfig, domainPort)
|
||||||
}
|
|
||||||
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)
|
val result = getV2rayNonCustomConfig(context, config)
|
||||||
//Log.d(ANG_PACKAGE, result.content)
|
//Log.d(ANG_PACKAGE, result.content)
|
||||||
|
result.guid = guid
|
||||||
return result
|
return result
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return Result(false, "")
|
return ConfigResult(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): ConfigResult {
|
||||||
* 生成v2ray的客户端配置文件
|
val result = ConfigResult(false)
|
||||||
*/
|
|
||||||
private fun getV2rayNonCustomConfig(
|
val outbound = config.getProxyOutbound() ?: return result
|
||||||
context: Context,
|
val address = outbound.getServerAddress() ?: return result
|
||||||
outbound: V2rayConfig.OutboundBean,
|
if (!Utils.isIpAddress(address)) {
|
||||||
remarks: String,
|
if (!Utils.isValidUrl(address)) {
|
||||||
): Result {
|
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
|
||||||
val result = Result(false, "")
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//取得默认配置
|
//取得默认配置
|
||||||
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
val assets = Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||||
if (TextUtils.isEmpty(assets)) {
|
if (TextUtils.isEmpty(assets)) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
|
||||||
//转成Json
|
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
||||||
val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result
|
v2rayConfig.remarks = config.remarks
|
||||||
|
|
||||||
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL)
|
|
||||||
?: "warning"
|
|
||||||
|
|
||||||
inbounds(v2rayConfig)
|
inbounds(v2rayConfig)
|
||||||
|
|
||||||
updateOutboundWithGlobalSettings(outbound)
|
val isPlugin = outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
v2rayConfig.outbounds[0] = outbound
|
val retOut = outbounds(v2rayConfig, outbound, isPlugin)
|
||||||
|
|
||||||
updateOutboundFragment(v2rayConfig)
|
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
|
||||||
|
|
||||||
routing(v2rayConfig)
|
routing(v2rayConfig)
|
||||||
|
|
||||||
@@ -112,31 +95,21 @@ object V2rayConfigUtil {
|
|||||||
v2rayConfig.policy = null
|
v2rayConfig.policy = null
|
||||||
}
|
}
|
||||||
|
|
||||||
v2rayConfig.remarks = remarks
|
|
||||||
|
|
||||||
result.status = true
|
result.status = true
|
||||||
result.content = v2rayConfig.toPrettyPrinting()
|
result.content = v2rayConfig.toPrettyPrinting()
|
||||||
|
result.domainPort = if (retMore.first) retMore.second else retOut.second
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
val socksPort = Utils.parseInt(
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT),
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
AppConfig.PORT_SOCKS.toInt()
|
|
||||||
)
|
|
||||||
val httpPort = Utils.parseInt(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
|
|
||||||
AppConfig.PORT_HTTP.toInt()
|
|
||||||
)
|
|
||||||
|
|
||||||
v2rayConfig.inbounds.forEach { curInbound ->
|
v2rayConfig.inbounds.forEach { curInbound ->
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
||||||
//bind all inbounds to localhost if the user requests
|
//bind all inbounds to localhost if the user requests
|
||||||
curInbound.listen = "127.0.0.1"
|
curInbound.listen = LOOPBACK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
v2rayConfig.inbounds[0].port = socksPort
|
v2rayConfig.inbounds[0].port = socksPort
|
||||||
@@ -170,6 +143,42 @@ object V2rayConfigUtil {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun outbounds(v2rayConfig: V2rayConfig, outbound: V2rayConfig.OutboundBean, 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 ret = updateOutboundWithGlobalSettings(outbound)
|
||||||
|
if (!ret) return Pair(false, "")
|
||||||
|
|
||||||
|
if (v2rayConfig.outbounds.isNotEmpty()) {
|
||||||
|
v2rayConfig.outbounds[0] = outbound
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.add(outbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOutboundFragment(v2rayConfig)
|
||||||
|
return Pair(true, outbound.getServerAddressAndPort())
|
||||||
|
}
|
||||||
|
|
||||||
private fun fakedns(v2rayConfig: V2rayConfig) {
|
private fun fakedns(v2rayConfig: V2rayConfig) {
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
||||||
&& settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
&& settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
||||||
@@ -178,89 +187,15 @@ object V2rayConfigUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* routing
|
|
||||||
*/
|
|
||||||
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
private fun routing(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
|
|
||||||
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
|
|
||||||
|
|
||||||
routingUserRule(
|
v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "IPIfNonMatch"
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
|
||||||
?: "", TAG_BLOCKED, v2rayConfig
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
)
|
rulesetItems?.forEach { key ->
|
||||||
if (routingMode == ERoutingMode.GLOBAL_DIRECT.value) {
|
routingUserRule(key, v2rayConfig)
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
|
||||||
?: "", TAG_DIRECT, v2rayConfig
|
|
||||||
)
|
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
|
||||||
?: "", TAG_PROXY, v2rayConfig
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
|
||||||
?: "", TAG_PROXY, v2rayConfig
|
|
||||||
)
|
|
||||||
routingUserRule(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
|
||||||
?: "", 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) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return false
|
return false
|
||||||
@@ -268,96 +203,46 @@ object V2rayConfigUtil {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun routingGeo(
|
private fun routingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
|
||||||
ipOrDomain: String,
|
|
||||||
code: String,
|
|
||||||
tag: String,
|
|
||||||
v2rayConfig: V2rayConfig
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
if (!TextUtils.isEmpty(code)) {
|
if (item == null || !item.enabled) {
|
||||||
//IP
|
return
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
|
||||||
|
|
||||||
|
v2rayConfig.routing.rules.add(rule)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun routingUserRule(userRule: String, tag: String, v2rayConfig: V2rayConfig) {
|
private fun userRule2Domain(tag: String): ArrayList<String> {
|
||||||
try {
|
val domain = ArrayList<String>()
|
||||||
if (!TextUtils.isEmpty(userRule)) {
|
|
||||||
//Domain
|
|
||||||
val rulesDomain = V2rayConfig.RoutingBean.RulesBean()
|
|
||||||
rulesDomain.outboundTag = tag
|
|
||||||
rulesDomain.domain = ArrayList()
|
|
||||||
|
|
||||||
//IP
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
val rulesIP = V2rayConfig.RoutingBean.RulesBean()
|
rulesetItems?.forEach { key ->
|
||||||
rulesIP.outboundTag = tag
|
if (key != null && key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
|
||||||
rulesIP.ip = ArrayList()
|
key.domain?.forEach {
|
||||||
|
if (it != GEOSITE_PRIVATE
|
||||||
userRule.split(",").map { it.trim() }.forEach {
|
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
|
||||||
if (it.startsWith("ext:") && it.contains("geoip")) {
|
) {
|
||||||
rulesIP.ip?.add(it)
|
domain.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
|
return domain
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom Dns
|
|
||||||
*/
|
|
||||||
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
|
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
||||||
val geositeCn = arrayListOf("geosite:cn")
|
val geositeCn = arrayListOf(GEOSITE_CN)
|
||||||
val proxyDomain = userRule2Domain(
|
val proxyDomain = userRule2Domain(TAG_PROXY)
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
val directDomain = userRule2Domain(TAG_DIRECT)
|
||||||
?: ""
|
|
||||||
)
|
|
||||||
val directDomain = userRule2Domain(
|
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
|
||||||
?: ""
|
|
||||||
)
|
|
||||||
// fakedns with all domains to make it always top priority
|
// fakedns with all domains to make it always top priority
|
||||||
v2rayConfig.dns.servers?.add(
|
v2rayConfig.dns.servers?.add(
|
||||||
0,
|
0,
|
||||||
@@ -385,7 +270,7 @@ object V2rayConfigUtil {
|
|||||||
V2rayConfig.InboundBean(
|
V2rayConfig.InboundBean(
|
||||||
tag = "dns-in",
|
tag = "dns-in",
|
||||||
port = localDnsPort,
|
port = localDnsPort,
|
||||||
listen = "127.0.0.1",
|
listen = LOOPBACK,
|
||||||
protocol = "dokodemo-door",
|
protocol = "dokodemo-door",
|
||||||
settings = dnsInboundSettings,
|
settings = dnsInboundSettings,
|
||||||
sniffing = null
|
sniffing = null
|
||||||
@@ -428,10 +313,7 @@ object V2rayConfigUtil {
|
|||||||
|
|
||||||
//remote Dns
|
//remote Dns
|
||||||
val remoteDns = Utils.getRemoteDnsServers()
|
val remoteDns = Utils.getRemoteDnsServers()
|
||||||
val proxyDomain = userRule2Domain(
|
val proxyDomain = userRule2Domain(TAG_PROXY)
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_AGENT)
|
|
||||||
?: ""
|
|
||||||
)
|
|
||||||
remoteDns.forEach {
|
remoteDns.forEach {
|
||||||
servers.add(it)
|
servers.add(it)
|
||||||
}
|
}
|
||||||
@@ -448,16 +330,9 @@ object V2rayConfigUtil {
|
|||||||
|
|
||||||
// domestic DNS
|
// domestic DNS
|
||||||
val domesticDns = Utils.getDomesticDnsServers()
|
val domesticDns = Utils.getDomesticDnsServers()
|
||||||
val directDomain = userRule2Domain(
|
val directDomain = userRule2Domain(TAG_DIRECT)
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
|
val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
|
||||||
?: ""
|
val geoipCn = arrayListOf(GEOIP_CN)
|
||||||
)
|
|
||||||
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) {
|
if (directDomain.size > 0) {
|
||||||
servers.add(
|
servers.add(
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
@@ -468,17 +343,6 @@ object V2rayConfigUtil {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (isCnRoutingMode) {
|
|
||||||
val geositeCn = arrayListOf("geosite:cn")
|
|
||||||
servers.add(
|
|
||||||
V2rayConfig.DnsBean.ServersBean(
|
|
||||||
domesticDns.first(),
|
|
||||||
53,
|
|
||||||
geositeCn,
|
|
||||||
geoipCn
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Utils.isPureIpAddress(domesticDns.first())) {
|
if (Utils.isPureIpAddress(domesticDns.first())) {
|
||||||
v2rayConfig.routing.rules.add(
|
v2rayConfig.routing.rules.add(
|
||||||
@@ -492,12 +356,9 @@ object V2rayConfigUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//block dns
|
//block dns
|
||||||
val blkDomain = userRule2Domain(
|
val blkDomain = userRule2Domain(TAG_BLOCKED)
|
||||||
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
|
|
||||||
?: ""
|
|
||||||
)
|
|
||||||
if (blkDomain.size > 0) {
|
if (blkDomain.size > 0) {
|
||||||
hosts.putAll(blkDomain.map { it to "127.0.0.1" })
|
hosts.putAll(blkDomain.map { it to LOOPBACK })
|
||||||
}
|
}
|
||||||
|
|
||||||
// hardcode googleapi rule to fix play store problems
|
// hardcode googleapi rule to fix play store problems
|
||||||
@@ -539,8 +400,10 @@ object V2rayConfigUtil {
|
|||||||
val protocol = outbound.protocol
|
val protocol = outbound.protocol
|
||||||
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
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.WIREGUARD.name, true)
|
|| protocol.equals(EConfigType.WIREGUARD.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
) {
|
) {
|
||||||
muxEnabled = false
|
muxEnabled = false
|
||||||
} else if (protocol.equals(EConfigType.VLESS.name, true)
|
} else if (protocol.equals(EConfigType.VLESS.name, true)
|
||||||
@@ -582,7 +445,7 @@ object V2rayConfigUtil {
|
|||||||
val requestString: String by lazy {
|
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"}}"""
|
"""{"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(
|
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
|
||||||
requestString,
|
requestString,
|
||||||
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
||||||
)
|
)
|
||||||
@@ -640,7 +503,14 @@ object V2rayConfigUtil {
|
|||||||
?: "50-100",
|
?: "50-100",
|
||||||
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||||
?: "10-20"
|
?: "10-20"
|
||||||
)
|
),
|
||||||
|
noises = listOf(
|
||||||
|
V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
|
||||||
|
type = "rand",
|
||||||
|
packet = "100-200",
|
||||||
|
delay = "10-20",
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
|
fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
|
||||||
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
|
||||||
@@ -661,4 +531,67 @@ object V2rayConfigUtil {
|
|||||||
}
|
}
|
||||||
return true
|
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 (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
|
||||||
|
return returnPair
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptionId.isNullOrEmpty()) {
|
||||||
|
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 = prevNode.getProxyOutbound()
|
||||||
|
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 = prevOutbound.getServerAddressAndPort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Next proxy
|
||||||
|
val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
|
||||||
|
if (nextNode != null) {
|
||||||
|
val nextOutbound = nextNode.getProxyOutbound()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
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.ServerConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.extension.idnHost
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
object Hysteria2Fmt {
|
||||||
|
|
||||||
|
fun parse(str: String): ServerConfig {
|
||||||
|
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||||
|
val config = ServerConfig.create(EConfigType.HYSTERIA2)
|
||||||
|
|
||||||
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
|
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?.streamSettings?.populateTlsSettings(
|
||||||
|
V2rayConfig.TLS,
|
||||||
|
if ((queryParam["insecure"].orEmpty()) == "1") true else allowInsecure,
|
||||||
|
queryParam["sni"] ?: uri.idnHost,
|
||||||
|
null,
|
||||||
|
queryParam["alpn"],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||||
|
server.address = uri.idnHost
|
||||||
|
server.port = uri.port
|
||||||
|
server.password = uri.userInfo
|
||||||
|
}
|
||||||
|
if (!queryParam["obfs-password"].isNullOrEmpty()) {
|
||||||
|
config.outboundBean?.settings?.obfsPassword = queryParam["obfs-password"]
|
||||||
|
}
|
||||||
|
|
||||||
|
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>()
|
||||||
|
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
||||||
|
streamSetting.tlsSettings?.let { tlsSetting ->
|
||||||
|
dicQuery["insecure"] = if (tlsSetting.allowInsecure) "1" else "0"
|
||||||
|
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 (!outbound.settings?.obfsPassword.isNullOrEmpty()) {
|
||||||
|
dicQuery["obfs"] = "salamander"
|
||||||
|
dicQuery["obfs-password"] = outbound.settings?.obfsPassword ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toNativeConfig(config: ServerConfig, socksPort: Int): Hysteria2Bean? {
|
||||||
|
val outbound = config.getProxyOutbound() ?: return null
|
||||||
|
val tls = outbound.streamSettings?.tlsSettings
|
||||||
|
val obfs = if (outbound.settings?.obfsPassword.isNullOrEmpty()) null else
|
||||||
|
Hysteria2Bean.ObfsBean(
|
||||||
|
type = "salamander",
|
||||||
|
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
|
||||||
|
password = outbound.settings?.obfsPassword
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val bean = Hysteria2Bean(
|
||||||
|
server = outbound.getServerAddressAndPort(),
|
||||||
|
auth = outbound.getPassword(),
|
||||||
|
obfs = obfs,
|
||||||
|
socks5 = Hysteria2Bean.Socks5Bean(
|
||||||
|
listen = "$LOOPBACK:${socksPort}",
|
||||||
|
),
|
||||||
|
http = Hysteria2Bean.Socks5Bean(
|
||||||
|
listen = "$LOOPBACK:${socksPort}",
|
||||||
|
),
|
||||||
|
tls = Hysteria2Bean.TlsBean(
|
||||||
|
sni = tls?.serverName ?: outbound.getServerAddress(),
|
||||||
|
insecure = tls?.allowInsecure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return bean
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import com.v2ray.ang.util.Utils
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object ShadowsocksFmt {
|
object ShadowsocksFmt {
|
||||||
fun parseShadowsocks(str: String): ServerConfig? {
|
fun parse(str: String): ServerConfig? {
|
||||||
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
|
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
|
||||||
if (!tryResolveResolveSip002(str, config)) {
|
if (!tryResolveResolveSip002(str, config)) {
|
||||||
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||||
@@ -67,7 +67,7 @@ object ShadowsocksFmt {
|
|||||||
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
|
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
|
||||||
try {
|
try {
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
|
||||||
val method: String
|
val method: String
|
||||||
val password: String
|
val password: String
|
||||||
@@ -88,7 +88,7 @@ object ShadowsocksFmt {
|
|||||||
password = base64Decode.substringAfter(":")
|
password = base64Decode.substringAfter(":")
|
||||||
}
|
}
|
||||||
|
|
||||||
val query = Utils.urlDecode(uri.query ?: "")
|
val query = Utils.urlDecode(uri.query.orEmpty())
|
||||||
if (query != "") {
|
if (query != "") {
|
||||||
val queryPairs = HashMap<String, String>()
|
val queryPairs = HashMap<String, String>()
|
||||||
val pairs = query.split(";")
|
val pairs = query.split(";")
|
||||||
@@ -137,7 +137,7 @@ object ShadowsocksFmt {
|
|||||||
}
|
}
|
||||||
if ("tls" in queryPairs) {
|
if ("tls" in queryPairs) {
|
||||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
"tls", false, sni ?: "", null, null, null, null, null
|
"tls", false, sni.orEmpty(), null, null, null, null, null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import com.v2ray.ang.dto.V2rayConfig
|
|||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
object SocksFmt {
|
object SocksFmt {
|
||||||
fun parseSocks(str: String): ServerConfig? {
|
fun parse(str: String): ServerConfig? {
|
||||||
val config = ServerConfig.create(EConfigType.SOCKS)
|
val config = ServerConfig.create(EConfigType.SOCKS)
|
||||||
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
|
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
|
||||||
val indexSplit = result.indexOf("#")
|
val indexSplit = result.indexOf("#")
|
||||||
|
|||||||
@@ -1,30 +1,23 @@
|
|||||||
package com.v2ray.ang.util.fmt
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
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.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object TrojanFmt {
|
object TrojanFmt {
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseTrojan(str: String): ServerConfig? {
|
fun parse(str: String): ServerConfig {
|
||||||
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||||
val config = ServerConfig.create(EConfigType.TROJAN)
|
val config = ServerConfig.create(EConfigType.TROJAN)
|
||||||
|
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
|
||||||
var flow = ""
|
var flow = ""
|
||||||
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
|
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
|
||||||
@@ -55,19 +48,19 @@ object TrojanFmt {
|
|||||||
queryParam["serviceName"],
|
queryParam["serviceName"],
|
||||||
queryParam["authority"]
|
queryParam["authority"]
|
||||||
)
|
)
|
||||||
fingerprint = queryParam["fp"] ?: ""
|
fingerprint = queryParam["fp"].orEmpty()
|
||||||
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
|
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
||||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||||
queryParam["security"] ?: V2rayConfig.TLS,
|
queryParam["security"] ?: V2rayConfig.TLS,
|
||||||
allowInsecure,
|
allowInsecure,
|
||||||
queryParam["sni"] ?: sni ?: "",
|
queryParam["sni"] ?: sni.orEmpty(),
|
||||||
fingerprint,
|
fingerprint,
|
||||||
queryParam["alpn"],
|
queryParam["alpn"],
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
flow = queryParam["flow"] ?: ""
|
flow = queryParam["flow"].orEmpty()
|
||||||
}
|
}
|
||||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||||
server.address = uri.idnHost
|
server.address = uri.idnHost
|
||||||
@@ -99,19 +92,19 @@ object TrojanFmt {
|
|||||||
}
|
}
|
||||||
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
||||||
dicQuery["alpn"] =
|
dicQuery["alpn"] =
|
||||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
||||||
dicQuery["fp"] = tlsSetting.fingerprint ?: ""
|
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||||
dicQuery["pbk"] = tlsSetting.publicKey ?: ""
|
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||||
dicQuery["sid"] = tlsSetting.shortId ?: ""
|
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX ?: "")
|
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dicQuery["type"] =
|
dicQuery["type"] =
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
package com.v2ray.ang.util.fmt
|
package com.v2ray.ang.util.fmt
|
||||||
|
|
||||||
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.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object VlessFmt {
|
object VlessFmt {
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING,
|
|
||||||
MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseVless(str: String): ServerConfig? {
|
fun parse(str: String): ServerConfig? {
|
||||||
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||||
val config = ServerConfig.create(EConfigType.VLESS)
|
val config = ServerConfig.create(EConfigType.VLESS)
|
||||||
|
|
||||||
@@ -30,13 +23,13 @@ object VlessFmt {
|
|||||||
|
|
||||||
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
||||||
vnext.address = uri.idnHost
|
vnext.address = uri.idnHost
|
||||||
vnext.port = uri.port
|
vnext.port = uri.port
|
||||||
vnext.users[0].id = uri.userInfo
|
vnext.users[0].id = uri.userInfo
|
||||||
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
|
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
|
||||||
vnext.users[0].flow = queryParam["flow"] ?: ""
|
vnext.users[0].flow = queryParam["flow"].orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
val sni = streamSetting.populateTransportSettings(
|
val sni = streamSetting.populateTransportSettings(
|
||||||
@@ -51,16 +44,16 @@ object VlessFmt {
|
|||||||
queryParam["serviceName"],
|
queryParam["serviceName"],
|
||||||
queryParam["authority"]
|
queryParam["authority"]
|
||||||
)
|
)
|
||||||
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
|
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
||||||
streamSetting.populateTlsSettings(
|
streamSetting.populateTlsSettings(
|
||||||
queryParam["security"] ?: "",
|
queryParam["security"].orEmpty(),
|
||||||
allowInsecure,
|
allowInsecure,
|
||||||
queryParam["sni"] ?: sni,
|
queryParam["sni"] ?: sni,
|
||||||
queryParam["fp"] ?: "",
|
queryParam["fp"].orEmpty(),
|
||||||
queryParam["alpn"],
|
queryParam["alpn"],
|
||||||
queryParam["pbk"] ?: "",
|
queryParam["pbk"].orEmpty(),
|
||||||
queryParam["sid"] ?: "",
|
queryParam["sid"].orEmpty(),
|
||||||
queryParam["spx"] ?: ""
|
queryParam["spx"].orEmpty()
|
||||||
)
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@@ -90,19 +83,19 @@ object VlessFmt {
|
|||||||
}
|
}
|
||||||
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
||||||
dicQuery["alpn"] =
|
dicQuery["alpn"] =
|
||||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
||||||
dicQuery["fp"] = tlsSetting.fingerprint ?: ""
|
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||||
dicQuery["pbk"] = tlsSetting.publicKey ?: ""
|
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||||
dicQuery["sid"] = tlsSetting.shortId ?: ""
|
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX ?: "")
|
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dicQuery["type"] =
|
dicQuery["type"] =
|
||||||
|
|||||||
@@ -2,26 +2,21 @@ package com.v2ray.ang.util.fmt
|
|||||||
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.tencent.mmkv.MMKV
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.EConfigType
|
import com.v2ray.ang.dto.EConfigType
|
||||||
import com.v2ray.ang.dto.ServerConfig
|
import com.v2ray.ang.dto.ServerConfig
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
import com.v2ray.ang.dto.VmessQRCode
|
import com.v2ray.ang.dto.VmessQRCode
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
import com.v2ray.ang.util.MmkvManager
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.MmkvManager.settingsStorage
|
||||||
import com.v2ray.ang.util.Utils
|
import com.v2ray.ang.util.Utils
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object VmessFmt {
|
object VmessFmt {
|
||||||
private val settingsStorage by lazy {
|
|
||||||
MMKV.mmkvWithID(
|
|
||||||
MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseVmess(str: String): ServerConfig? {
|
fun parse(str: String): ServerConfig? {
|
||||||
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
|
||||||
return parseVmessStd(str)
|
return parseVmessStd(str)
|
||||||
}
|
}
|
||||||
@@ -35,7 +30,7 @@ object VmessFmt {
|
|||||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
|
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
|
||||||
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
||||||
if (TextUtils.isEmpty(vmessQRCode.add)
|
if (TextUtils.isEmpty(vmessQRCode.add)
|
||||||
|| TextUtils.isEmpty(vmessQRCode.port)
|
|| TextUtils.isEmpty(vmessQRCode.port)
|
||||||
@@ -99,14 +94,14 @@ object VmessFmt {
|
|||||||
vmessQRCode.tls = streamSetting.security
|
vmessQRCode.tls = streamSetting.security
|
||||||
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
|
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
|
||||||
vmessQRCode.alpn =
|
vmessQRCode.alpn =
|
||||||
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty()
|
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString(",")).orEmpty()
|
||||||
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
|
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
|
||||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||||
vmessQRCode.type = transportDetails[0]
|
vmessQRCode.type = transportDetails[0]
|
||||||
vmessQRCode.host = transportDetails[1]
|
vmessQRCode.host = transportDetails[1]
|
||||||
vmessQRCode.path = transportDetails[2]
|
vmessQRCode.path = transportDetails[2]
|
||||||
}
|
}
|
||||||
val json = Gson().toJson(vmessQRCode)
|
val json = JsonUtil.toJson(vmessQRCode)
|
||||||
return Utils.encode(json)
|
return Utils.encode(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +116,7 @@ object VmessFmt {
|
|||||||
|
|
||||||
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
||||||
|
|
||||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
||||||
vnext.address = uri.idnHost
|
vnext.address = uri.idnHost
|
||||||
vnext.port = uri.port
|
vnext.port = uri.port
|
||||||
@@ -143,12 +138,12 @@ object VmessFmt {
|
|||||||
queryParam["authority"]
|
queryParam["authority"]
|
||||||
)
|
)
|
||||||
|
|
||||||
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
|
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
|
||||||
streamSetting.populateTlsSettings(
|
streamSetting.populateTlsSettings(
|
||||||
queryParam["security"] ?: "",
|
queryParam["security"].orEmpty(),
|
||||||
allowInsecure,
|
allowInsecure,
|
||||||
queryParam["sni"] ?: sni,
|
queryParam["sni"] ?: sni,
|
||||||
queryParam["fp"] ?: "",
|
queryParam["fp"].orEmpty(),
|
||||||
queryParam["alpn"],
|
queryParam["alpn"],
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import com.v2ray.ang.util.Utils
|
|||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
object WireguardFmt {
|
object WireguardFmt {
|
||||||
fun parseWireguard(str: String): ServerConfig? {
|
fun parse(str: String): ServerConfig? {
|
||||||
val uri = URI(Utils.fixIllegalUrl(str))
|
val uri = URI(Utils.fixIllegalUrl(str))
|
||||||
if (uri.rawQuery != null) {
|
if (uri.rawQuery != null) {
|
||||||
val config = ServerConfig.create(EConfigType.WIREGUARD)
|
val config = ServerConfig.create(EConfigType.WIREGUARD)
|
||||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
|
||||||
|
|
||||||
val queryParam = uri.rawQuery.split("&")
|
val queryParam = uri.rawQuery.split("&")
|
||||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||||
@@ -24,7 +24,7 @@ object WireguardFmt {
|
|||||||
(queryParam["address"]
|
(queryParam["address"]
|
||||||
?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
|
?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
|
||||||
.split(",")
|
.split(",")
|
||||||
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: ""
|
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"].orEmpty()
|
||||||
wireguard.peers?.get(0)?.endpoint =
|
wireguard.peers?.get(0)?.endpoint =
|
||||||
Utils.getIpv6Address(uri.idnHost) + ":${uri.port}"
|
Utils.getIpv6Address(uri.idnHost) + ":${uri.port}"
|
||||||
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||||
@@ -38,6 +38,40 @@ object WireguardFmt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
fun toUri(config: ServerConfig): String {
|
||||||
val outbound = config.getProxyOutbound() ?: return ""
|
val outbound = config.getProxyOutbound() ?: return ""
|
||||||
|
|
||||||
@@ -47,12 +81,12 @@ object WireguardFmt {
|
|||||||
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
|
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
|
||||||
if (outbound.settings?.reserved != null) {
|
if (outbound.settings?.reserved != null) {
|
||||||
dicQuery["reserved"] = Utils.urlEncode(
|
dicQuery["reserved"] = Utils.urlEncode(
|
||||||
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
|
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString(","))
|
||||||
.toString()
|
.toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
dicQuery["address"] = Utils.urlEncode(
|
dicQuery["address"] = Utils.urlEncode(
|
||||||
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
|
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString(","))
|
||||||
.toString()
|
.toString()
|
||||||
)
|
)
|
||||||
if (outbound.settings?.mtu != null) {
|
if (outbound.settings?.mtu != null) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import android.util.Log
|
|||||||
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
|
||||||
@@ -20,18 +19,16 @@ 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.ServerConfig
|
||||||
import com.v2ray.ang.dto.ServersCache
|
import com.v2ray.ang.dto.ServersCache
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.util.AngConfigManager
|
import com.v2ray.ang.util.AngConfigManager
|
||||||
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
|
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
import com.v2ray.ang.util.MmkvManager
|
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
|
||||||
@@ -43,10 +40,10 @@ 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, "") ?: ""
|
var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
||||||
|
|
||||||
//var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
|
//var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
|
||||||
private var keywordFilter = ""
|
var keywordFilter = ""
|
||||||
val serversCache = mutableListOf<ServersCache>()
|
val serversCache = mutableListOf<ServersCache>()
|
||||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||||
val updateListAction by lazy { MutableLiveData<Int>() }
|
val updateListAction by lazy { MutableLiveData<Int>() }
|
||||||
@@ -101,10 +98,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
try {
|
try {
|
||||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
val config = ServerConfig.create(EConfigType.CUSTOM)
|
||||||
config.subscriptionId = subscriptionId
|
config.subscriptionId = subscriptionId
|
||||||
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
|
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
|
||||||
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
|
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 = ProfileItem(
|
||||||
configType = config.configType,
|
configType = config.configType,
|
||||||
@@ -125,7 +122,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun swapServer(fromPosition: Int, toPosition: Int) {
|
fun swapServer(fromPosition: Int, toPosition: Int) {
|
||||||
Collections.swap(serverList, fromPosition, toPosition)
|
Collections.swap(serverList, fromPosition, toPosition)
|
||||||
Collections.swap(serversCache, fromPosition, toPosition)
|
Collections.swap(serversCache, fromPosition, toPosition)
|
||||||
MmkvManager.mainStorage?.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
|
MmkvManager.encodeServerList(serverList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -159,18 +156,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
if (subscriptionId.isNullOrEmpty()) {
|
if (subscriptionId.isNullOrEmpty()) {
|
||||||
return AngConfigManager.updateConfigViaSubAll()
|
return AngConfigManager.updateConfigViaSubAll()
|
||||||
} else {
|
} else {
|
||||||
val json = subStorage?.decodeString(subscriptionId)
|
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0
|
||||||
if (!json.isNullOrBlank()) {
|
return 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()) {
|
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
||||||
serverList
|
serverList
|
||||||
} else {
|
} else {
|
||||||
serversCache.map { it.guid }.toList()
|
serversCache.map { it.guid }.toList()
|
||||||
@@ -218,14 +211,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
getApplication<AngApplication>().toast(R.string.connection_test_testing)
|
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +281,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllServer() {
|
fun removeAllServer() {
|
||||||
if (subscriptionId.isNullOrEmpty()) {
|
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
||||||
MmkvManager.removeAllServer()
|
MmkvManager.removeAllServer()
|
||||||
} else {
|
} else {
|
||||||
val serversCopy = serversCache.toList()
|
val serversCopy = serversCache.toList()
|
||||||
@@ -306,7 +292,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeInvalidServer() {
|
fun removeInvalidServer() {
|
||||||
if (subscriptionId.isNullOrEmpty()) {
|
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) {
|
||||||
MmkvManager.removeInvalidServer("")
|
MmkvManager.removeInvalidServer("")
|
||||||
} else {
|
} else {
|
||||||
val serversCopy = serversCache.toList()
|
val serversCopy = serversCache.toList()
|
||||||
@@ -317,7 +303,22 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -348,6 +349,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun filterConfig(keyword: String) {
|
fun filterConfig(keyword: String) {
|
||||||
|
if (keyword == keywordFilter) {
|
||||||
|
return
|
||||||
|
}
|
||||||
keywordFilter = keyword
|
keywordFilter = keyword
|
||||||
MmkvManager.settingsStorage.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
|
MmkvManager.settingsStorage.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
|
||||||
reloadServerList()
|
reloadServerList()
|
||||||
@@ -383,7 +387,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
||||||
val resultPair = intent.getSerializableExtra("content") as Pair<String, Long>
|
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
|
||||||
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.util.MmkvManager.settingsStorage
|
||||||
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,10 +39,6 @@ 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,
|
||||||
@@ -61,6 +49,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
|
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
|
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="1024"
|
android:viewportWidth="1024"
|
||||||
android:viewportHeight="1024">
|
android:viewportHeight="1024">
|
||||||
<path
|
<path
|
||||||
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z"
|
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z"
|
||||||
android:fillColor="#FFFFFFFF"/>
|
android:fillColor="#FFFFFFFF" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
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>
|
||||||
@@ -5,11 +5,11 @@
|
|||||||
android:viewportHeight="1024">
|
android:viewportHeight="1024">
|
||||||
<path
|
<path
|
||||||
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z"
|
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z"
|
||||||
android:fillColor="#FFFFFFFF"/>
|
android:fillColor="#FFFFFFFF" />
|
||||||
<path
|
<path
|
||||||
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z"
|
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z"
|
||||||
android:fillColor="#FFFFFFFF"/>
|
android:fillColor="#FFFFFFFF" />
|
||||||
<path
|
<path
|
||||||
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z"
|
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z"
|
||||||
android:fillColor="#FFFFFFFF"/>
|
android:fillColor="#FFFFFFFF" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
<vector android:height="24dp"
|
||||||
android:viewportHeight="24" android:viewportWidth="24"
|
android:tint="#FFFFFF"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:viewportHeight="24"
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
|
android:viewportWidth="24"
|
||||||
|
android:width="24dp"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
android:viewportHeight="1024">
|
android:viewportHeight="1024">
|
||||||
<path
|
<path
|
||||||
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z"
|
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z"
|
||||||
android:fillColor="#FFFFFFFF"/>
|
android:fillColor="#FFFFFFFF" />
|
||||||
<path
|
<path
|
||||||
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z"
|
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z"
|
||||||
android:fillColor="#FFFFFFFF"/>
|
android:fillColor="#FFFFFFFF" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M740,161a112.1,112.1 0,0 0,-33.5 218.9v95.9L320,602.4L320,318.1a112.1,112.1 0,1 0,-148 -106.1c0,49.3 31.8,91 76,106.1v387.9a112.1,112.1 0,1 0,148 106.1c0,-49.2 -31.8,-91 -76,-106.1v-27.8l423.5,-138.7a50.6,50.6 0,0 0,34.9 -48.2L778.4,378.2c42.9,-15.8 73.6,-57 73.6,-105.2 0,-61.8 -50.2,-112 -112,-112zM236,212a48,48 0,1 1,96 0,48 48,0 0,1 -96,0zM332,812a48,48 0,1 1,-96 0,48 48,0 0,1 96,0zM740,321a48,48 0,1 1,0 -96,48 48,0 0,1 0,96z" />
|
||||||
|
</vector>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFFFF"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user