Compare commits

...

108 Commits

Author SHA1 Message Date
2dust
1b2cc11a97 up 1.9.21 2024-11-26 18:58:29 +08:00
hosêyň abāspanā
a3591e4bbb Update Luri Bakhtiari translation (#4032) 2024-11-26 18:55:46 +08:00
2dust
aa0f5639b1 Bug fix
https://github.com/2dust/v2rayNG/issues/4033
2024-11-26 18:54:43 +08:00
2dust
f3abd0d9fc up 1.9.20 2024-11-24 19:50:03 +08:00
Tamim Hossain
4a62aff7d2 Add OSS Licenses Plugin to Display Open Source Licenses (#4022)
### Commit Message
- Integrated Google OSS Licenses Plugin to display the licenses of third-party libraries used in the app.
- Added the plugin to app-level Gradle file and included the required dependencies.
- Created a pre-built activity (`OssLicensesMenuActivity`) to show the licenses, accessible via a button/menu.
- Verified the implementation, ensuring the licenses are displayed correctly.
- Tested on release builds to confirm functionality and compliance.

This implementation ensures transparency and complies with open-source license requirements.
2024-11-23 20:04:37 +08:00
2dust
c78e624eaf Add VPN setHttpProxy
https://github.com/2dust/v2rayNG/issues/4017
2024-11-23 10:25:21 +08:00
2dust
934cf5d21c Bug fix
https://github.com/2dust/v2rayNG/issues/4020
2024-11-23 09:52:02 +08:00
2dust
f252d1395a up 1.9.19 2024-11-21 20:30:35 +08:00
2dust
2dc0472c69 When creating a new routing rule, add it to the top 2024-11-21 20:24:01 +08:00
hosêyň abāspanā
3e09adc4d1 Improved Luri Bakhtiari Translation (#4003) 2024-11-21 09:26:08 +08:00
phoenix6936
11750b9382 Improved Persian translation (#4001)
* Improved Persian translation

Improved Persian translation

* Improved Persian translation

Improved Persian translation
2024-11-21 09:25:48 +08:00
solokot
30a4c2199a Improved Russian translation (#3997) 2024-11-20 16:00:14 +08:00
2dust
406a9f996e up 1.9.18 2024-11-20 09:40:08 +08:00
Chocolate4U
5373579bd5 Import rulesets from qrcode (#3991)
* Renamed functions to be more semantically accurate

* Import rulesets from qrcode

Add capability to import rulesets from qrcode
fixes and improvements
2024-11-20 09:23:08 +08:00
Tamim Hossain
9b4cc201e7 Add preSharedKey support and fix parsing function #3512 (#3989)
Add support for preSharedKey in WireGuard configurations and fix the parsing function to correctly handle all necessary fields.

Previously, the application did not support the optional preSharedKey parameter in WireGuard config files, forcing users to rely on JSON custom configurations. This update introduces a dedicated field for preSharedKey in the UI, aligning with Xray Core's support and simplifying the setup process for users.

Changes include:
- Added `preSharedKey` field in the WireGuard UI configuration.
- Updated `parseWireguardConfFile` function to correctly parse `PrivateKey`, `PublicKey`, and `preSharedKey`.
- Ensured `preSharedKey` is optional and handled gracefully when absent.
- Updated `toOutbound` method to include `preSharedKey` in the outbound configuration.
- Set `remarks` to the current Unix time during parsing.

Tested with the following configuration:
```[Interface]
Address = 192.168.6.66/32
DNS = 1.1.1.1,8.8.8.8
PrivateKey = eD/6cpJQaEeDH05AMeFyN3KSLLX+7YFR+MYRdgPDQ3Y=
[Peer]
publickey=/HS7r3waPuU7tTBLd2FlBhC+VROpJ5bwh5XXxuOoKFs=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = sg3.vpnjantit.com:1024
```
Resolves #3512
2024-11-20 09:15:11 +08:00
2dust
6f2c96c2b6 Bug fix
https://github.com/2dust/v2rayNG/issues/3988
2024-11-19 20:38:44 +08:00
solokot
1f6104de8b Update Russian translation (#3987) 2024-11-19 19:07:13 +08:00
2dust
e078a2ab27 up 1.9.17 2024-11-19 17:30:33 +08:00
2dust
5695c17908 Code optimization 2024-11-19 14:28:58 +08:00
decorativeman
e2c1081d5a Update Persian Translation (#3980)
* Update persian translate 

Update persian translate

* Update strings.xml
2024-11-19 13:49:50 +08:00
hosêyň abāspanā
bcbcbc91c7 Update Luri Bakhtiari translation (#3985) 2024-11-19 13:49:34 +08:00
2dust
5eb3566e8d Add libhysteria2.so 2024-11-19 11:38:05 +08:00
decorativeman
d75eca8dd4 Update gradle-wrapper.properties (#3969)
Update Gradle 8.9 to 8.11
2024-11-19 10:58:11 +08:00
2dust
640c16d8dc Improve Fmt 2024-11-19 10:45:01 +08:00
2dust
1ba5c5a7a6 Add xhttp extra 2024-11-19 10:17:03 +08:00
2dust
f67af69dda Improve Utils 2024-11-19 10:09:03 +08:00
2dust
5d47777307 Add xhttp mode 2024-11-18 20:25:13 +08:00
Tamim Hossain
ab22bb9804 Updated the WorkManager configuration to replace BuildConfig.APPLICATION_ID with ANG_PACKAGE for setting the default process name. This ensures consistent configuration handling in background processes. (#3970)
Changes:
- Modified `setDefaultProcessName` to use `ANG_PACKAGE` instead of `BuildConfig.APPLICATION_ID`.

This resolves inconsistencies in process naming conventions and aligns with project requirements.
2024-11-18 19:26:11 +08:00
Tamim Hossain
2626462e49 Remove unnecessary Context parameter from setNightMode (#3971)
Refactored the `setNightMode` function to remove the unused `Context` parameter.

Changes:
- Eliminated the `context` parameter from the `setNightMode` function.
- Adjusted function signature to align with the current implementation, which does not utilize the `Context`.

This simplifies the function interface and ensures cleaner, more maintainable code.
2024-11-18 19:26:00 +08:00
2dust
41893d79c0 SplitHTTP is now XHTTP 2024-11-18 18:58:15 +08:00
2dust
834c1ba63d Remove quic 2024-11-18 18:39:06 +08:00
2dust
633ee63891 Improved duplicate configuration
https://github.com/2dust/v2rayNG/issues/3948
2024-11-16 20:18:37 +08:00
hosêyň abāspanā
eb5627c0d0 Update strings.xml (#3955) 2024-11-16 09:28:52 +08:00
hosêyň abāspanā
6db38f6e3d Update arrays.xml (#3954)
Rename بختیاری to لۊری بختیاری
2024-11-15 18:31:01 +08:00
Tamim Hossain
c69a758429 Add Bakhtiari language support and fix implementation issues (#3952)
This commit improves the Bakhtiari language support by addressing issues from PR [#3927](https://github.com/2dust/v2rayNG/pull/3927), where the initial implementation had incorrect file placements and lacked necessary changes for functionality.

- Integrated `strings.xml` into the correct `values-bqi-rIR` directory.
- Updated the `Language` enum to include `BAKHTIARI("bqi-rIR")`.
- Modified the `getLocale()` function to handle Bakhtiari with `Locale("bqi", "IR")`.
- Added Bakhtiari to the `language_select` string-array using its native script: `<item>بختیاری</item>`.
- Updated the `language_select_value` string-array to include `<item>bqi-rIR</item>`.

Verified that the language switching works correctly. For grammatical or translation accuracy, a native speaker's review is needed.

@hosseinabaspanah, since I assume Bakhtiari is your native language, could you please review the translations to ensure accuracy?
2024-11-15 17:15:10 +08:00
Tamim Hossain
cee3a0ffec Rename styles.xml and themes.xml for consistency with Android Studio templates (#3950)
Updated the naming of `styles.xml` and `themes.xml` to align with the new Android Studio template conventions. This follows up on commit `18c0143` where I introduced the new android studio project template.

- Verified and tested the changes thoroughly to ensure that the app behaves as expected, with no regressions.
- Ensured all affected references and dependencies were updated accordingly.

This keeps the project consistent with modern Android development practices and improves maintainability.
2024-11-15 17:07:30 +08:00
Tamim Hossain
18c0143186 Upgrade project to new Android Studio template and migrate Java code to Kotlin (#3937)
### Summary
- Updated the project structure using the latest Android Studio template.
- Migrated portions of the codebase from Java to Kotlin for improved readability and maintainability.

### Details
- Refactored and reorganized files according to the new Android Studio project template to ensure compatibility with the latest project standards.
- Migrated key Java classes to Kotlin, adopting Kotlin idioms and improving type safety.
- Verified that core functionalities remain intact after migration and update.
- Removed redundant Java files and updated imports where necessary.

### Notes
- Further Kotlin migration may be needed as additional Java files are reviewed.
- Test thoroughly to confirm that all functionalities work as expected after these changes.
2024-11-15 13:42:46 +08:00
Chocolate4U
bbf0b05b49 Add Assets From QRcode (#3933)
Add Capability to Import Geo Assets From QRcode
2024-11-13 18:49:35 +08:00
2dust
44723c56ad up 1.9.16 2024-11-12 14:34:40 +08:00
2dust
e53c36b53b Bug fix 2024-11-12 14:33:29 +08:00
TTG
80f26cd4b8 Update DNS in Routing and Configurations (#3921)
* Update DNS configs

* Update DNS in AppConfig
2024-11-12 09:41:54 +08:00
2dust
b023414cd0 Fix UI
https://github.com/2dust/v2rayNG/issues/3866
2024-11-10 19:57:06 +08:00
2dust
2f56104565 Fix UI
https://github.com/2dust/v2rayNG/issues/3892
2024-11-10 19:37:21 +08:00
2dust
9cd5fefdca up 1.9.15 2024-11-10 09:58:08 +08:00
2dust
25c42c475f newFixedThreadPool(Runtime.getRuntime().availableProcessors()) 2024-11-09 19:38:59 +08:00
DecorativeFamily
96e66da071 Update persian translate (#3913)
* Update persian translate

Update persian translate

* Update persian translate 

Update persian translate
2024-11-09 18:31:36 +08:00
2dust
6914b9ee1b Bug fix
https://github.com/2dust/v2rayNG/issues/3911
2024-11-09 17:32:13 +08:00
TTG
cfc6546c97 Update Configuration to Optimize (#3912)
* Remove geolocation-cn

* Segment and update DNS lists
2024-11-09 14:40:24 +08:00
2dust
0880313659 Bug fix
https://github.com/2dust/v2rayNG/issues/3885
2024-11-08 15:06:52 +08:00
2dust
875ca02126 Bug fix
https://github.com/2dust/v2rayNG/issues/3900
2024-11-08 10:43:13 +08:00
2dust
c2d5925053 up 1.9.14 2024-11-07 20:54:09 +08:00
2dust
547bbf8e95 Bug fix 2024-11-07 20:37:17 +08:00
2dust
2218251b03 Bug fix
https://github.com/2dust/v2rayNG/issues/3895
2024-11-07 20:32:08 +08:00
2dust
4da3a23162 up 1.9.13 2024-11-06 14:31:10 +08:00
2dust
28a90baf88 Bug fix
https://github.com/2dust/v2rayNG/issues/3883
2024-11-06 11:22:01 +08:00
2dust
7bbdda2f2f This is a temporary solution
https://github.com/2dust/v2rayNG/issues/3883
2024-11-05 21:32:50 +08:00
2dust
884b444a41 up 1.9.12 2024-11-05 19:01:03 +08:00
2dust
e1ff2df36e Bug fix 2024-11-05 18:59:05 +08:00
2dust
61c0111778 Revert "Refactor listenForPackageChanges to remove redundant registerReceiver calls (#3872)"
This reverts commit 5f167512f5.
2024-11-05 18:07:55 +08:00
Tamim Hossain
bfc9a64e07 Correct isEmpty syntax for collection check (#3881)
Fixed the syntax for checking if `filesToCompress` is empty by using `isEmpty()` instead of `isEmpty`. This ensures correct functionality when verifying if the collection has elements.
2024-11-05 18:01:32 +08:00
Tamim Hossain
2ba92045cc Remove unnecessary Boolean comparisons in conditional checks (#3880)
Simplified conditional checks by removing unnecessary `== true` comparisons. The `decodeSettingsBool` function returns a non-nullable Boolean with a default value, so direct usage improves readability and keeps the code concise.
2024-11-05 18:00:52 +08:00
Tamim Hossain
aeca9f51c8 Remove redundant Boolean comparison in runLoop call (#3879)
Simplified the call to `runLoop` by removing the redundant `== true` comparison. Since `decodeSettingsBool` returns a non-nullable Boolean, direct usage improves readability.
2024-11-05 18:00:23 +08:00
Tamim Hossain
b107c0ac1d Remove redundant TAG field in ProcessService (#3878)
Refactored `ProcessService` by removing the redundant `TAG` variable and using `ANG_PACKAGE` directly in logging calls, simplifying the code and reducing unnecessary field assignments.
2024-11-05 17:59:37 +08:00
Tamim Hossain
65a04b4784 Refactor getString call to use orEmpty for null safety (#3877)
Updated `getString` call to use `orEmpty()` instead of specifying a default empty string, making the code cleaner and handling nullability more effectively.
2024-11-05 17:58:23 +08:00
Tamim Hossain
eab9f50cfd Refactor BootReceiver for improved null handling and readability (#3876)
Refactored `BootReceiver` to simplify null checks and conditional structure. Combined context and intent checks into a single early return and refactored logic for `decodeStartOnBoot` and `getSelectServer` to improve readability.
2024-11-05 17:57:51 +08:00
Tamim Hossain
b8bb83b524 Used safecall ? (#3874)
Used safecall `?`
2024-11-05 17:56:32 +08:00
Tamim Hossain
93eb9fe3b9 Introduce NetworkType enum to improve network type handling (#3873)
* Introduce NetworkType enum to improve network type handling

Created a `NetworkType` enum to represent various network types, improving readability and reducing potential errors caused by hardcoded string comparisons. Updated the `getQueryDic` function to utilize this enum.

* Refactor to use NetworkType enum in VmessFmt

Replaced hardcoded network type strings with the `NetworkType` enum in `VmessFmt` functions. Updated `parse`, `toUri`, and `parseVmessStd` methods to use `NetworkType.fromString`, improving readability and reducing errors caused by typos in network type strings.
2024-11-05 17:56:04 +08:00
Tamim Hossain
5f167512f5 Refactor listenForPackageChanges to remove redundant registerReceiver calls (#3872)
Refactored the `listenForPackageChanges` function to remove redundant calls to `registerReceiver` by creating a single `IntentFilter` instance. This simplifies the code and improves readability.
2024-11-05 17:55:08 +08:00
Tamim Hossain
a727b81263 Update registerReceiver usage to comply with Android Tiramisu+ guidelines (#3871)
### Summary
- Updated `registerReceiver` usage to align with Android Tiramisu+ documentation.

### Details
- Replaced direct `registerReceiver` calls with `ContextCompat.registerReceiver` for improved compatibility.
- Used `RECEIVER_EXPORTED` and `RECEIVER_NOT_EXPORTED` flags based on API level to ensure correct receiver permissions.
- Added reference to the official Android documentation for `registerReceiver`.

### References
- [Documentation on registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int))

This commit ensures that the `registerReceiver` call is consistent with the latest Android standards, improving compatibility and security across Android versions.
2024-11-05 16:42:17 +08:00
solokot
f27c9192d1 Update Russian translation (#3870) 2024-11-05 16:39:19 +08:00
DecorativeFamily
90153fa17f Update persian translate (#3869)
* Update persian translate

* Update strings.xml
2024-11-05 16:38:54 +08:00
DecorativeFamily
cdfaa01852 Revert "Update persian translate (#3867)" (#3868)
This reverts commit 33d2c3b00d.
2024-11-05 15:05:42 +08:00
DecorativeFamily
33d2c3b00d Update persian translate (#3867) 2024-11-05 15:03:35 +08:00
DecorativeFamily
b7f992cdc0 Update gradle-wrapper.properties (#3844)
Gradle 8.10.2
2024-11-05 15:03:21 +08:00
DecorativeFamily
0a6a24e309 Gradle (#3839)
* Create dependabot.yml

* Update dependabot.yml

* Update libs.versions.toml

* Delete .github/dependabot.yml
2024-11-05 15:03:07 +08:00
2dust
153b4cffef Unable to obtain the notification permission 2024-11-05 14:29:44 +08:00
2dust
f09a413232 Bug fix
https://github.com/2dust/v2rayNG/issues/3858
2024-11-05 10:24:06 +08:00
886963226
cba58f6ae2 Update V2rayConfigManager.kt (#3860) 2024-11-05 09:31:01 +08:00
2dust
2bf4b91488 Bug fix
https://github.com/2dust/v2rayNG/issues/3852
2024-11-04 20:22:40 +08:00
2dust
b60b7f4307 up 1.9.11 2024-11-04 19:56:28 +08:00
2dust
e4ca04a096 Bug fix
https://github.com/2dust/v2rayNG/issues/3851
2024-11-04 19:55:01 +08:00
2dust
d0f7ecec44 Bug fix
https://github.com/2dust/v2rayNG/issues/3693
2024-11-04 17:51:26 +08:00
2dust
f488811f01 up 1.9.10 2024-11-04 16:49:52 +08:00
2dust
d212cda1e1 You can delete the downloaded geo file, which will be restored to the built-in geo file. 2024-11-04 16:47:49 +08:00
DecorativeFamily
ba760eac59 Update V2rayConfigManager.kt (#3847)
Update noise parameter

https://github.com/XTLS/Xray-docs-next/blob/main/docs/en/config/outbounds/freedom.md

"noises":[
{
"type":"base64",
"packet":"7nQBAAABAAAAAAAABnQtcmluZwZtc2VkZ2UDbmV0AAABAAE=",
"delay":"10-16"
},
{
"type":"rand",
"packet":"10-20",
"delay":"10-16"
},
{
"type":"str",
"packet":"hiGFW",
"delay":"10-16"
}
]

Add udp type":"base64",

@2dust
2024-11-04 09:42:45 +08:00
886963226
8549b5ea46 Optimization (#3842)
* Update PluginUtil.kt

fix kotlin.UninitializedProertyAcessException:lateinit property procService has not been initalized.

* Update ProcessService.kt
2024-11-04 09:42:17 +08:00
886963226
0b3c106c6f Update build.yml (#3841)
patch fix go
Do not use custom versions. Usually, major versions are bundled with the latest version.
Source:https://github.com/actions/toolkit/blob/main/docs/action-versioning.md
2024-11-04 09:32:16 +08:00
2dust
84e7ee4ef3 Bug fix 2024-11-03 20:06:43 +08:00
DecorativeFamily
e5d498ea6e Update V2rayConfigManager.kt (#3822)
* Update V2rayConfigManager.kt

Update some things

* Update V2rayConfigManager.kt

* Update V2rayConfigManager.kt
2024-11-03 19:29:48 +08:00
DecorativeFamily
6da835a2ca Update translation persian (#3835)
Update translation persian
2024-11-03 17:46:07 +08:00
solokot
1f9a71e6ac Update Russian translation (#3834) 2024-11-03 15:22:48 +08:00
2dust
9c92fdc257 Fix
https://github.com/2dust/v2rayNG/issues/3824
2024-11-02 14:36:45 +08:00
2dust
da219228fa Fix
https://github.com/2dust/v2rayNG/issues/3821
2024-11-01 20:57:11 +08:00
DecorativeFamily
c0a6455d08 Update Persian translation (#3818)
* Update Persian translation

Update Persian translation

* Update Persian translation

Update Persian translation

* Update Persian translation

Update Persian translation

* Update Persian translation

Update Persian translation

* Update Persian translation

Update Persian translation
2024-11-01 20:50:23 +08:00
2dust
709e2a9ed4 Improved settings storage 2024-11-01 20:43:17 +08:00
2dust
c3ac9f01d2 Refactor code 2024-11-01 19:01:24 +08:00
2dust
65eba3795f Bug fix
https://github.com/2dust/v2rayNG/issues/3807
2024-11-01 17:45:25 +08:00
2dust
341cdb5dbe Add migrate2ProfileCustom 2024-10-31 19:30:18 +08:00
2dust
4f43c2ce45 Reformat code 2024-10-31 19:30:04 +08:00
2dust
2ec691fc6b Add port hopping for hy2 2024-10-31 17:03:13 +08:00
2dust
81ed321654 Unit test 2024-10-31 14:16:39 +08:00
2dust
6ad37c70f1 Refactor server configuration storage 2024-10-31 14:10:09 +08:00
2dust
c7ffd6d82d Refactor V2rayConfig 2024-10-31 10:56:37 +08:00
any116
ae4b0fd8d3 Optimize up sub error log (#3805)
* Optimize up sub error log

Optimize the update subscription error log when not use proxy first.

* Update AngConfigManager.kt

* Update AngConfigManager.kt

* Update AngConfigManager.kt
2024-10-29 13:37:34 +08:00
DecorativeFamily
beceaba44d Update build.yml (#3804)
Fix restore cache failed
2024-10-29 13:36:00 +08:00
solokot
f5987d9767 Update Russian translation (#3802) 2024-10-29 13:35:35 +08:00
2dust
616712b338 Rename ProfileItem to ProfileLiteItem 2024-10-28 15:01:15 +08:00
2dust
076a968476 targetSdk = 35 2024-10-28 14:33:30 +08:00
158 changed files with 3777 additions and 2329 deletions

View File

@@ -21,12 +21,19 @@ jobs:
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
java-version: '21'
- name: Setup Golang
uses: actions/setup-go@v5
with:
go-version: '1.22.4'
go-version: '1.23.2'
cache: false
- name: Patch Go use 600296
#https://go-review.googlesource.com/c/go/+/600296
run: |
cd "$(go env GOROOT)"
curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1
- name: Install gomobile
run: |

View File

@@ -1,19 +1,21 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("com.google.android.gms.oss-licenses-plugin")
}
android {
namespace = "com.v2ray.ang"
compileSdk = 34
compileSdk = 35
defaultConfig {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 34
versionCode = 603
versionName = "1.9.9"
targetSdk = 35
versionCode = 617
versionName = "1.9.21"
multiDexEnabled = true
splits {
abi {
isEnable = true
@@ -27,20 +29,16 @@ android {
}
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildTypes {
release {
isMinifyEnabled = false
}
debug {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
@@ -50,8 +48,13 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
jvmTarget = JavaVersion.VERSION_11.toString()
}
applicationVariants.all {
@@ -69,7 +72,8 @@ android {
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if (versionCodes.containsKey(abi)) {
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode)
output.versionCodeOverride =
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} else {
return@forEach
}
@@ -86,47 +90,62 @@ android {
useLegacyPackaging = true
}
}
}
dependencies {
// Core Libraries
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
testImplementation(libs.junit)
implementation(libs.flexbox)
// Androidx
implementation(libs.constraintlayout)
implementation(libs.legacy.support.v4)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.cardview)
// AndroidX Core Libraries
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.preference.ktx)
implementation(libs.recyclerview)
implementation(libs.fragment.ktx)
implementation(libs.multidex)
implementation(libs.viewpager2)
// Androidx ktx
implementation(libs.activity.ktx)
// UI Libraries
implementation(libs.material)
implementation(libs.toastcompat)
implementation(libs.editorkit)
implementation(libs.flexbox)
// Data and Storage Libraries
implementation(libs.mmkv.static)
implementation(libs.gson)
// Reactive and Utility Libraries
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxpermissions)
// Language and Processing Libraries
implementation(libs.language.base)
implementation(libs.language.json)
// Intent and Utility Libraries
implementation(libs.quickie.bundled)
implementation(libs.core)
// AndroidX Lifecycle and Architecture Components
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.runtime.ktx)
//kotlin
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.mmkv.static)
implementation(libs.gson)
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxpermissions)
implementation(libs.toastcompat)
implementation(libs.editorkit)
implementation(libs.language.base)
implementation(libs.language.json)
implementation(libs.quickie.bundled)
implementation(libs.core)
// Background Task Libraries
implementation(libs.work.runtime.ktx)
implementation(libs.work.multiprocess)
// Multidex Support
implementation(libs.multidex)
// Testing Libraries
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
testImplementation(libs.org.mockito.mockito.inline)
testImplementation(libs.mockito.kotlin)
// Oss Licenses
implementation(libs.play.services.oss.licenses)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -224,8 +224,7 @@
<activity
android:name=".ui.TaskerActivity"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
android:icon="@mipmap/ic_launcher">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
@@ -234,7 +233,8 @@
<receiver
android:name=".receiver.TaskerReceiver"
android:exported="true"
android:process=":RunSoLibV2RayDaemon">
android:process=":RunSoLibV2RayDaemon"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter>

View File

@@ -41,14 +41,90 @@
"geosite:private"
]
},
{
"remarks": "代理海外公共DNSIP",
"outboundTag": "proxy",
"ip": [
"1.1.1.1",
"1.0.0.1",
"2606:4700:4700::1111",
"2606:4700:4700::1001",
"1.1.1.2",
"1.0.0.2",
"2606:4700:4700::1112",
"2606:4700:4700::1002",
"1.1.1.3",
"1.0.0.3",
"2606:4700:4700::1113",
"2606:4700:4700::1003",
"8.8.8.8",
"8.8.4.4",
"2001:4860:4860::8888",
"2001:4860:4860::8844",
"94.140.14.14",
"94.140.15.15",
"2a10:50c0::ad1:ff",
"2a10:50c0::ad2:ff",
"94.140.14.15",
"94.140.15.16",
"2a10:50c0::bad1:ff",
"2a10:50c0::bad2:ff",
"94.140.14.140",
"94.140.14.141",
"2a10:50c0::1:ff",
"2a10:50c0::2:ff",
"208.67.222.222",
"208.67.220.220",
"2620:119:35::35",
"2620:119:53::53",
"208.67.222.123",
"208.67.220.123",
"2620:119:35::123",
"2620:119:53::123",
"9.9.9.9",
"149.112.112.112",
"2620:fe::9",
"2620:fe::fe",
"9.9.9.11",
"149.112.112.11",
"2620:fe::11",
"2620:fe::fe:11",
"9.9.9.10",
"149.112.112.10",
"2620:fe::10",
"2620:fe::fe:10",
"77.88.8.8",
"77.88.8.1",
"2a02:6b8::feed:0ff",
"2a02:6b8:0:1::feed:0ff",
"77.88.8.88",
"77.88.8.2",
"2a02:6b8::feed:bad",
"2a02:6b8:0:1::feed:bad",
"77.88.8.7",
"77.88.8.3",
"2a02:6b8::feed:a11",
"2a02:6b8:0:1::feed:a11"
]
},
{
"remarks": "代理海外公共DNS域名",
"outboundTag": "proxy",
"domain": [
"domain:cloudflare-dns.com",
"domain:one.one.one.one",
"domain:dns.google",
"domain:adguard-dns.com",
"domain:opendns.com",
"domain:umbrella.com",
"domain:quad9.net",
"domain:yandex.net"
]
},
{
"remarks": "代理IP",
"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",

View File

@@ -34,29 +34,62 @@
"geosite:private"
]
},
{
"remarks": "绕过中国公共DNSIP",
"outboundTag": "direct",
"ip": [
"223.5.5.5",
"223.6.6.6",
"2400:3200::1",
"2400:3200:baba::1",
"119.29.29.29",
"1.12.12.12",
"120.53.53.53",
"2402:4e00::",
"2402:4e00:1::",
"180.76.76.76",
"2400:da00::6666",
"114.114.114.114",
"114.114.115.115",
"114.114.114.119",
"114.114.115.119",
"114.114.114.110",
"114.114.115.110",
"180.184.1.1",
"180.184.2.2",
"101.226.4.6",
"218.30.118.6",
"123.125.81.6",
"140.207.198.6",
"1.2.4.8",
"210.2.4.8",
"52.80.66.66",
"117.50.22.22",
"2400:7fc0:849e:200::4",
"2404:c2c0:85d8:901::4",
"117.50.10.10",
"52.80.52.52",
"2400:7fc0:849e:200::8",
"2404:c2c0:85d8:901::8",
"117.50.60.30",
"52.80.60.30"
]
},
{
"remarks": "绕过中国公共DNS域名",
"outboundTag": "direct",
"domain": [
"domain:alidns.com",
"domain:doh.pub",
"domain:dot.pub",
"domain:360.cn",
"domain:onedns.net"
]
},
{
"remarks": "绕过中国IP",
"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"
]
},
@@ -64,13 +97,7 @@
"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"
"geosite:cn"
]
},
{

View File

@@ -7,7 +7,8 @@ import androidx.multidex.MultiDexApplication
import androidx.work.Configuration
import androidx.work.WorkManager
import com.tencent.mmkv.MMKV
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils
class AngApplication : MultiDexApplication() {
@@ -22,7 +23,7 @@ class AngApplication : MultiDexApplication() {
}
private val workManagerConfiguration: Configuration = Configuration.Builder()
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
.setDefaultProcessName("${ANG_PACKAGE}:bg")
.build()
override fun onCreate() {
@@ -37,7 +38,7 @@ class AngApplication : MultiDexApplication() {
MMKV.initialize(this)
Utils.setNightMode(application)
Utils.setNightMode()
// Initialize WorkManager with the custom configuration
WorkManager.initialize(this, workManagerConfiguration)

View File

@@ -155,21 +155,33 @@ object AppConfig {
/** Give a good name to this, IDK*/
const val VPN = "VPN"
// Google API rule constants
const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
// Android Private DNS constants
const val DNS_PUB_DOMAIN = "dns.pub"
const val DNS_DNSPOD_DOMAIN = "dot.pub"
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
const val DNS_ONE_ONE_DOMAIN = "one.one.one.one"
const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
const val DNS_GOOGLE_DOMAIN = "dns.google"
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
val DNS_PUB_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_ONE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8
const val DEFAULT_NETWORK = "tcp"
const val TLS = "tls"
const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http"
}

View File

@@ -1,6 +1,6 @@
package com.v2ray.ang.dto
data class ConfigResult (
data class ConfigResult(
var status: Boolean,
var guid: String? = null,
var content: String = "",

View File

@@ -8,6 +8,7 @@ data class Hysteria2Bean(
val socks5: Socks5Bean? = null,
val http: Socks5Bean? = null,
val tls: TlsBean? = null,
val transport: TransportBean? = null,
) {
data class ObfsBean(
val type: String?,
@@ -25,5 +26,15 @@ data class Hysteria2Bean(
data class TlsBean(
val sni: String?,
val insecure: Boolean?,
val pinSHA256: String?,
)
data class TransportBean(
val type: String?,
val udp: TransportUdpBean?
) {
data class TransportUdpBean(
val hopInterval: String?,
)
}
}

View File

@@ -8,7 +8,8 @@ enum class Language(val code: String) {
VIETNAMESE("vi"),
RUSSIAN("ru"),
PERSIAN("fa"),
BANGLA("bn");
BANGLA("bn"),
BAKHTIARI("bqi-rIR");
companion object {
fun fromCode(code: String): Language {

View File

@@ -0,0 +1,18 @@
package com.v2ray.ang.dto
enum class NetworkType(val type: String) {
TCP("tcp"),
KCP("kcp"),
WS("ws"),
HTTP_UPGRADE("httpupgrade"),
SPLIT_HTTP("splithttp"),
XHTTP("xhttp"),
HTTP("http"),
H2("h2"),
//QUIC("quic"),
GRPC("grpc");
companion object {
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
}
}

View File

@@ -0,0 +1,113 @@
package com.v2ray.ang.dto
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.util.Utils
data class ProfileItem(
val configVersion: Int = 4,
val configType: EConfigType,
var subscriptionId: String = "",
var addedTime: Long = System.currentTimeMillis(),
var remarks: String = "",
var server: String? = null,
var serverPort: String? = null,
var password: String? = null,
var method: String? = null,
var flow: String? = null,
var username: String? = null,
var network: String? = null,
var headerType: String? = null,
var host: String? = null,
var path: String? = null,
var seed: String? = null,
var quicSecurity: String? = null,
var quicKey: String? = null,
var mode: String? = null,
var serviceName: String? = null,
var authority: String? = null,
var xhttpMode: String? = null,
var xhttpExtra: String? = null,
var security: String? = null,
var sni: String? = null,
var alpn: String? = null,
var fingerPrint: String? = null,
var insecure: Boolean? = null,
var publicKey: String? = null,
var shortId: String? = null,
var spiderX: String? = null,
var secretKey: String? = null,
var preSharedKey: String? = null,
var localAddress: String? = null,
var reserved: String? = null,
var mtu: Int? = null,
var obfsPassword: String? = null,
var portHopping: String? = null,
var portHoppingInterval: String? = null,
var pinSHA256: String? = null,
) {
companion object {
fun create(configType: EConfigType): ProfileItem {
return ProfileItem(configType = configType)
}
}
fun getAllOutboundTags(): MutableList<String> {
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
}
fun getServerAddressAndPort(): String {
return Utils.getIpv6Address(server) + ":" + serverPort
}
override fun equals(other: Any?): Boolean {
if (other == null) return false
val obj = other as ProfileItem
return (this.server == obj.server
&& this.serverPort == obj.serverPort
&& this.password == obj.password
&& this.method == obj.method
&& this.flow == obj.flow
&& this.username == obj.username
&& this.network == obj.network
&& this.headerType == obj.headerType
&& this.host == obj.host
&& this.path == obj.path
&& this.seed == obj.seed
&& this.quicSecurity == obj.quicSecurity
&& this.quicKey == obj.quicKey
&& this.mode == obj.mode
&& this.serviceName == obj.serviceName
&& this.authority == obj.authority
&& this.xhttpMode == obj.xhttpMode
&& this.security == obj.security
&& this.sni == obj.sni
&& this.alpn == obj.alpn
&& this.fingerPrint == obj.fingerPrint
&& this.publicKey == obj.publicKey
&& this.shortId == obj.shortId
&& this.secretKey == obj.secretKey
&& this.localAddress == obj.localAddress
&& this.reserved == obj.reserved
&& this.mtu == obj.mtu
&& this.obfsPassword == obj.obfsPassword
&& this.portHopping == obj.portHopping
&& this.portHoppingInterval == obj.portHoppingInterval
&& this.pinSHA256 == obj.pinSHA256
)
}
}

View File

@@ -1,6 +1,6 @@
package com.v2ray.ang.dto
data class ProfileItem(
data class ProfileLiteItem(
val configType: EConfigType,
var subscriptionId: String = "",
var remarks: String = "",

View File

@@ -7,6 +7,9 @@ import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.*
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.*
import com.v2ray.ang.util.Utils
import java.lang.reflect.Type
@@ -27,16 +30,6 @@ data class V2rayConfig(
var observatory: Any? = null,
var burstObservatory: Any? = null
) {
companion object {
const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8
const val DEFAULT_NETWORK = "tcp"
const val TLS = "tls"
const val REALITY = "reality"
const val HTTP = "http"
}
data class LogBean(
val access: String,
@@ -82,6 +75,49 @@ data class V2rayConfig(
val sendThrough: String? = null,
var mux: MuxBean? = MuxBean(false)
) {
companion object {
fun create(configType: EConfigType): OutboundBean? {
return when (configType) {
EConfigType.VMESS,
EConfigType.VLESS ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
vnext = listOf(
VnextBean(
users = listOf(UsersBean())
)
)
),
streamSettings = StreamSettingsBean()
)
EConfigType.SHADOWSOCKS,
EConfigType.SOCKS,
EConfigType.HTTP,
EConfigType.TROJAN,
EConfigType.HYSTERIA2 ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
servers = listOf(ServersBean())
),
streamSettings = StreamSettingsBean()
)
EConfigType.WIREGUARD ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
secretKey = "",
peers = listOf(WireGuardBean())
)
)
EConfigType.CUSTOM -> null
}
}
}
data class OutSettingsBean(
var vnext: List<VnextBean>? = null,
@@ -110,17 +146,17 @@ data class V2rayConfig(
data class VnextBean(
var address: String = "",
var port: Int = DEFAULT_PORT,
var port: Int = AppConfig.DEFAULT_PORT,
var users: List<UsersBean>
) {
data class UsersBean(
var id: String = "",
var alterId: Int? = null,
var security: String = DEFAULT_SECURITY,
var level: Int = DEFAULT_LEVEL,
var encryption: String = "",
var flow: String = ""
var security: String? = null,
var level: Int = AppConfig.DEFAULT_LEVEL,
var encryption: String? = null,
var flow: String? = null
)
}
@@ -141,8 +177,8 @@ data class V2rayConfig(
var method: String? = null,
var ota: Boolean = false,
var password: String? = null,
var port: Int = DEFAULT_PORT,
var level: Int = DEFAULT_LEVEL,
var port: Int = AppConfig.DEFAULT_PORT,
var level: Int = AppConfig.DEFAULT_LEVEL,
val email: String? = null,
var flow: String? = null,
val ivCheck: Boolean? = null,
@@ -151,7 +187,7 @@ data class V2rayConfig(
data class SocksUsersBean(
var user: String = "",
var pass: String = "",
var level: Int = DEFAULT_LEVEL
var level: Int = AppConfig.DEFAULT_LEVEL
)
}
@@ -159,18 +195,19 @@ data class V2rayConfig(
data class WireGuardBean(
var publicKey: String = "",
var preSharedKey: String = "",
var endpoint: String = ""
)
}
data class StreamSettingsBean(
var network: String = DEFAULT_NETWORK,
var security: String = "",
var network: String = AppConfig.DEFAULT_NETWORK,
var security: String? = null,
var tcpSettings: TcpSettingsBean? = null,
var kcpSettings: KcpSettingsBean? = null,
var wsSettings: WsSettingsBean? = null,
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
var splithttpSettings: SplithttpSettingsBean? = null,
var xhttpSettings: XhttpSettingsBean? = null,
var httpSettings: HttpSettingsBean? = null,
var tlsSettings: TlsSettingsBean? = null,
var quicSettings: QuicSettingBean? = null,
@@ -224,7 +261,7 @@ data class V2rayConfig(
}
data class WsSettingsBean(
var path: String = "",
var path: String? = null,
var headers: HeadersBean = HeadersBean(),
val maxEarlyData: Int? = null,
val useBrowserForwarding: Boolean? = null,
@@ -234,21 +271,21 @@ data class V2rayConfig(
}
data class HttpupgradeSettingsBean(
var path: String = "",
var host: String = "",
var path: String? = null,
var host: String? = null,
val acceptProxyProtocol: Boolean? = null
)
data class SplithttpSettingsBean(
var path: String = "",
var host: String = "",
val maxUploadSize: Int? = null,
val maxConcurrentUploads: Int? = null
data class XhttpSettingsBean(
var path: String? = null,
var host: String? = null,
var mode: String? = null,
var extra: Any? = null,
)
data class HttpSettingsBean(
var host: List<String> = ArrayList(),
var path: String = ""
var path: String? = null
)
data class SockoptBean(
@@ -262,7 +299,7 @@ data class V2rayConfig(
data class TlsSettingsBean(
var allowInsecure: Boolean = false,
var serverName: String = "",
var serverName: String? = null,
val alpn: List<String>? = null,
val minVersion: String? = null,
val maxVersion: String? = null,
@@ -308,17 +345,24 @@ data class V2rayConfig(
}
fun populateTransportSettings(
transport: String, headerType: String?, host: String?, path: String?, seed: String?,
quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
transport: String,
headerType: String?,
host: String?,
path: String?,
seed: String?,
quicSecurity: String?,
key: String?,
mode: String?,
serviceName: String?,
authority: String?
): String {
var sni = ""
network = transport
): String? {
var sni: String? = null
network = if (transport.isEmpty()) NetworkType.TCP.type else transport
when (network) {
"tcp" -> {
NetworkType.TCP.type -> {
val tcpSetting = TcpSettingsBean()
if (headerType == HTTP) {
tcpSetting.header.type = HTTP
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
@@ -333,7 +377,7 @@ data class V2rayConfig(
tcpSettings = tcpSetting
}
"kcp" -> {
NetworkType.KCP.type -> {
val kcpsetting = KcpSettingsBean()
kcpsetting.header.type = headerType ?: "none"
if (seed.isNullOrEmpty()) {
@@ -344,7 +388,7 @@ data class V2rayConfig(
kcpSettings = kcpsetting
}
"ws" -> {
NetworkType.WS.type -> {
val wssetting = WsSettingsBean()
wssetting.headers.Host = host.orEmpty()
sni = wssetting.headers.Host
@@ -352,7 +396,7 @@ data class V2rayConfig(
wsSettings = wssetting
}
"httpupgrade" -> {
NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = HttpupgradeSettingsBean()
httpupgradeSetting.host = host.orEmpty()
sni = httpupgradeSetting.host
@@ -360,16 +404,16 @@ data class V2rayConfig(
httpupgradeSettings = httpupgradeSetting
}
"splithttp" -> {
val splithttpSetting = SplithttpSettingsBean()
splithttpSetting.host = host.orEmpty()
sni = splithttpSetting.host
splithttpSetting.path = path ?: "/"
splithttpSettings = splithttpSetting
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
val xhttpSetting = XhttpSettingsBean()
xhttpSetting.host = host.orEmpty()
sni = xhttpSetting.host
xhttpSetting.path = path ?: "/"
xhttpSettings = xhttpSetting
}
"h2", "http" -> {
network = "h2"
NetworkType.H2.type, NetworkType.HTTP.type -> {
network = NetworkType.H2.type
val h2Setting = HttpSettingsBean()
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
sni = h2Setting.host.getOrNull(0) ?: sni
@@ -377,15 +421,15 @@ data class V2rayConfig(
httpSettings = h2Setting
}
"quic" -> {
val quicsetting = QuicSettingBean()
quicsetting.security = quicSecurity ?: "none"
quicsetting.key = key.orEmpty()
quicsetting.header.type = headerType ?: "none"
quicSettings = quicsetting
}
// "quic" -> {
// val quicsetting = QuicSettingBean()
// quicsetting.security = quicSecurity ?: "none"
// quicsetting.key = key.orEmpty()
// quicsetting.header.type = headerType ?: "none"
// quicSettings = quicsetting
// }
"grpc" -> {
NetworkType.GRPC.type -> {
val grpcSetting = GrpcSettingsBean()
grpcSetting.multiMode = mode == "multi"
grpcSetting.serviceName = serviceName.orEmpty()
@@ -400,10 +444,17 @@ data class V2rayConfig(
}
fun populateTlsSettings(
streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?,
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 = if (streamSecurity.isEmpty()) null else streamSecurity
if (security == null) return
val tlsSetting = TlsSettingsBean(
allowInsecure = allowInsecure,
serverName = sni,
@@ -413,10 +464,10 @@ data class V2rayConfig(
shortId = shortId,
spiderX = spiderX
)
if (security == TLS) {
if (security == AppConfig.TLS) {
tlsSettings = tlsSetting
realitySettings = null
} else if (security == REALITY) {
} else if (security == AppConfig.REALITY) {
tlsSettings = null
realitySettings = tlsSetting
}
@@ -434,16 +485,16 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.get(0)?.address
return settings?.vnext?.first()?.address
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.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?.first()?.address
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":")
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
}
return null
}
@@ -452,16 +503,16 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.get(0)?.port
return settings?.vnext?.first()?.port
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.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?.first()?.port
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt()
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
}
return null
}
@@ -476,16 +527,16 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.get(0)?.users?.get(0)?.id
return settings?.vnext?.first()?.users?.first()?.id
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
) {
return settings?.servers?.get(0)?.password
return settings?.servers?.first()?.password
} else if (protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true)
) {
return settings?.servers?.get(0)?.users?.get(0)?.pass
return settings?.servers?.first()?.users?.first()?.pass
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.secretKey
}
@@ -494,14 +545,14 @@ data class V2rayConfig(
fun getSecurityEncryption(): String? {
return when {
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method
else -> null
}
}
fun getTransportSettingDetails(): List<String>? {
fun getTransportSettingDetails(): List<String?>? {
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
@@ -509,7 +560,7 @@ data class V2rayConfig(
) {
val transport = streamSettings?.network ?: return null
return when (transport) {
"tcp" -> {
NetworkType.TCP.type -> {
val tcpSetting = streamSettings?.tcpSettings ?: return null
listOf(
tcpSetting.header.type,
@@ -518,7 +569,7 @@ data class V2rayConfig(
)
}
"kcp" -> {
NetworkType.KCP.type -> {
val kcpSetting = streamSettings?.kcpSettings ?: return null
listOf(
kcpSetting.header.type,
@@ -527,7 +578,7 @@ data class V2rayConfig(
)
}
"ws" -> {
NetworkType.WS.type -> {
val wsSetting = streamSettings?.wsSettings ?: return null
listOf(
"",
@@ -536,7 +587,7 @@ data class V2rayConfig(
)
}
"httpupgrade" -> {
NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
listOf(
"",
@@ -545,16 +596,16 @@ data class V2rayConfig(
)
}
"splithttp" -> {
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
val xhttpSettings = streamSettings?.xhttpSettings ?: return null
listOf(
"",
splithttpSetting.host,
splithttpSetting.path
xhttpSettings.host,
xhttpSettings.path
)
}
"h2" -> {
NetworkType.H2.type -> {
val h2Setting = streamSettings?.httpSettings ?: return null
listOf(
"",
@@ -563,16 +614,16 @@ data class V2rayConfig(
)
}
"quic" -> {
val quicSetting = streamSettings?.quicSettings ?: return null
listOf(
quicSetting.header.type,
quicSetting.security,
quicSetting.key
)
}
// "quic" -> {
// val quicSetting = streamSettings?.quicSettings ?: return null
// listOf(
// quicSetting.header.type,
// quicSetting.security,
// quicSetting.key
// )
// }
"grpc" -> {
NetworkType.GRPC.type -> {
val grpcSetting = streamSettings?.grpcSettings ?: return null
listOf(
if (grpcSetting.multiMode == true) "multi" else "gun",
@@ -601,7 +652,8 @@ data class V2rayConfig(
var port: Int? = null,
var domains: List<String>? = null,
var expectIPs: List<String>? = null,
val clientIp: String? = null
val clientIp: String? = null,
val skipFallback: Boolean? = null,
)
}

View File

@@ -94,3 +94,5 @@ inline fun <reified T : Serializable> Intent.serializable(key: String): T? = whe
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
}
inline fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())

View File

@@ -0,0 +1,21 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.JsonUtil
object CustomFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.CUSTOM)
val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
val outbound = fullConfig.getProxyOutbound()
config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString()
config.server = outbound?.getServerAddress()
config.serverPort = outbound?.getServerPort().toString()
return config
}
}

View File

@@ -0,0 +1,123 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.Utils
import java.net.URI
open class FmtBase {
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
val query = if (dicQuery != null)
("?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
else ""
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(userInfo ?: ""),
Utils.getIpv6Address(config.server),
config.serverPort
)
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
}
fun getQueryParam(uri: URI): Map<String, String> {
return uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
}
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
config.network = queryParam["type"] ?: NetworkType.TCP.type
//TODO
if (config.network == NetworkType.SPLIT_HTTP.type) config.network = NetworkType.XHTTP.type
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.xhttpMode = queryParam["mode"]
config.xhttpExtra = queryParam["extra"]
config.security = queryParam["security"]
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["allowInsecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
config.publicKey = queryParam["pbk"]
config.shortId = queryParam["sid"]
config.spiderX = queryParam["spx"]
config.flow = queryParam["flow"]
}
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
val dicQuery = HashMap<String, String>()
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() }
config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
val networkType = NetworkType.fromString(config.network)
dicQuery["type"] = networkType.type
when (networkType) {
NetworkType.TCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
}
NetworkType.KCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
}
NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
NetworkType.SPLIT_HTTP, NetworkType.XHTTP -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() }
}
NetworkType.HTTP, NetworkType.H2 -> {
dicQuery["type"] = "http"
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
// NetworkType.QUIC -> {
// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
// config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
// config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
// }
NetworkType.GRPC -> {
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
}
}
return dicQuery
}
}

View File

@@ -0,0 +1,28 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.isNotNullEmpty
import kotlin.text.orEmpty
object HttpFmt : FmtBase() {
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HTTP)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = profileItem.username.orEmpty()
socksUsersBean.pass = profileItem.password.orEmpty()
server.users = listOf(socksUsersBean)
}
}
return outboundBean
}
}

View File

@@ -0,0 +1,120 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.Hysteria2Bean
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object Hysteria2Fmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.security = AppConfig.TLS
if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri)
config.security = queryParam["security"] ?: AppConfig.TLS
config.insecure = if (queryParam["insecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["insecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.alpn = queryParam["alpn"]
config.obfsPassword = queryParam["obfs-password"]
config.portHopping = queryParam["mport"]
config.pinSHA256 = queryParam["pinSHA256"]
}
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>()
config.security.let { if (it != null) dicQuery["security"] = it }
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
config.insecure.let { dicQuery["insecure"] = if (it == true) "1" else "0" }
if (config.obfsPassword.isNotNullEmpty()) {
dicQuery["obfs"] = "salamander"
dicQuery["obfs-password"] = config.obfsPassword.orEmpty()
}
if (config.portHopping.isNotNullEmpty()) {
dicQuery["mport"] = config.portHopping.orEmpty()
}
if (config.pinSHA256.isNotNullEmpty()) {
dicQuery["pinSHA256"] = config.pinSHA256.orEmpty()
}
return toUri(config, config.password, dicQuery)
}
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
Hysteria2Bean.ObfsBean(
type = "salamander",
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
password = config.obfsPassword
)
)
val transport = if (config.portHopping.isNullOrEmpty()) null else
Hysteria2Bean.TransportBean(
type = "udp",
udp = Hysteria2Bean.TransportBean.TransportUdpBean(
hopInterval = (config.portHoppingInterval ?: "30") + "s"
)
)
val server =
if (config.portHopping.isNullOrEmpty())
config.getServerAddressAndPort()
else
Utils.getIpv6Address(config.server) + ":" + config.portHopping
val bean = Hysteria2Bean(
server = server,
auth = config.password,
obfs = obfs,
transport = transport,
socks5 = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
http = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
tls = Hysteria2Bean.TlsBean(
sni = config.sni ?: config.server,
insecure = config.insecure,
pinSHA256 = if (config.pinSHA256.isNullOrEmpty()) null else config.pinSHA256
)
)
return bean
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
return outboundBean
}
}

View File

@@ -0,0 +1,133 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
return parseSip002(str) ?: parseLegacy(str)
}
fun parseSip002(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
if (uri.userInfo.isNullOrEmpty()) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
val result = if (uri.userInfo.contains(":")) {
uri.userInfo.split(":", limit = 2)
} else {
Utils.decode(uri.userInfo).split(":", limit = 2)
}
if (result.count() == 2) {
config.method = result.first()
config.password = result.last()
}
if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri)
if (queryParam["plugin"] == "obfs-local" && queryParam["obfs"] == "http") {
config.network = NetworkType.TCP.type
config.headerType = "http"
config.host = queryParam["obfs-host"]
config.path = queryParam["path"]
}
}
return config
}
fun parseLegacy(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result) ?: return null
config.server = match.groupValues[3].removeSurrounding("[", "]")
config.serverPort = match.groupValues[4]
config.password = match.groupValues[2]
config.method = match.groupValues[1].lowercase()
return config
}
fun toUri(config: ProfileItem): String {
val pw = "${config.method}:${config.password}"
return toUri(config, Utils.encode(pw), null)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.method = profileItem.method
}
outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}

View File

@@ -0,0 +1,62 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object SocksFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SOCKS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
if (uri.userInfo?.isEmpty() == false) {
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
if (result.count() == 2) {
config.username = result.first()
config.password = result.last()
}
}
return config
}
fun toUri(config: ProfileItem): String {
val pw =
if (config.username.isNotNullEmpty())
"${config.username}:${config.password}"
else
":"
return toUri(config, Utils.encode(pw), null)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = profileItem.username.orEmpty()
socksUsersBean.pass = profileItem.password.orEmpty()
server.users = listOf(socksUsersBean)
}
}
return outboundBean
}
}

View File

@@ -0,0 +1,81 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object TrojanFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
if (uri.rawQuery.isNullOrEmpty()) {
config.network = NetworkType.TCP.type
config.security = AppConfig.TLS
config.insecure = allowInsecure
} else {
val queryParam = getQueryParam(uri)
getItemFormQuery(config, queryParam, allowInsecure)
config.security = queryParam["security"] ?: AppConfig.TLS
}
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
return toUri(config, config.password, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.flow = profileItem.flow
}
outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}

View File

@@ -0,0 +1,83 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = queryParam["encryption"] ?: "none"
getItemFormQuery(config, queryParam, allowInsecure)
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
dicQuery["encryption"] = config.method ?: "none"
return toUri(config, config.password, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VLESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].encryption = profileItem.method
vnext.users[0].flow = profileItem.flow
}
outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}

View File

@@ -0,0 +1,183 @@
package com.v2ray.ang.fmt
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object VmessFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str)
}
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
return null
}
config.remarks = vmessQRCode.ps
config.server = vmessQRCode.add
config.serverPort = vmessQRCode.port
config.password = vmessQRCode.id
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
config.network = vmessQRCode.net ?: NetworkType.TCP.type
config.headerType = vmessQRCode.type
config.host = vmessQRCode.host
config.path = vmessQRCode.path
when (NetworkType.fromString(config.network)) {
NetworkType.KCP -> {
config.seed = vmessQRCode.path
}
// NetworkType.QUIC -> {
// config.quicSecurity = vmessQRCode.host
// config.quicKey = vmessQRCode.path
// }
NetworkType.GRPC -> {
config.mode = vmessQRCode.type
config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host
}
else -> {}
}
config.security = vmessQRCode.tls
config.insecure = allowInsecure
config.sni = vmessQRCode.sni
config.fingerPrint = vmessQRCode.fp
config.alpn = vmessQRCode.alpn
return config
}
fun toUri(config: ProfileItem): String {
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = config.server.orEmpty()
vmessQRCode.port = config.serverPort.orEmpty()
vmessQRCode.id = config.password.orEmpty()
vmessQRCode.scy = config.method.orEmpty()
vmessQRCode.aid = "0"
vmessQRCode.net = config.network.orEmpty()
vmessQRCode.type = config.headerType.orEmpty()
when (NetworkType.fromString(config.network)) {
NetworkType.KCP -> {
vmessQRCode.path = config.seed.orEmpty()
}
// NetworkType.QUIC -> {
// vmessQRCode.host = config.quicSecurity.orEmpty()
// vmessQRCode.path = config.quicKey.orEmpty()
// }
NetworkType.GRPC -> {
vmessQRCode.type = config.mode.orEmpty()
vmessQRCode.path = config.serviceName.orEmpty()
vmessQRCode.host = config.authority.orEmpty()
}
else -> {}
}
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
vmessQRCode.tls = config.security.orEmpty()
vmessQRCode.sni = config.sni.orEmpty()
vmessQRCode.fp = config.fingerPrint.orEmpty()
vmessQRCode.alpn = config.alpn.orEmpty()
val json = JsonUtil.toJson(vmessQRCode)
return Utils.encode(json)
}
fun parseVmessStd(str: String): ProfileItem? {
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = AppConfig.DEFAULT_SECURITY
getItemFormQuery(config, queryParam, allowInsecure)
return config
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VMESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].security = profileItem.method
}
outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
null,
null,
null
)
return outboundBean
}
}

View File

@@ -0,0 +1,124 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object WireguardFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo.orEmpty()
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
config.publicKey = queryParam["publickey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"].orEmpty()
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = (queryParam["reserved"] ?: "0,0,0")
return config
}
fun parseWireguardConfFile(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val interfaceParams: MutableMap<String, String> = mutableMapOf()
val peerParams: MutableMap<String, String> = mutableMapOf()
var currentSection: String? = null
str.lines().forEach { line ->
val trimmedLine = line.trim()
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
return@forEach
}
when {
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
else -> {
if (currentSection != null) {
val parts = trimmedLine.split("=", limit = 2).map { it.trim() }
if (parts.size == 2) {
val key = parts[0].lowercase()
val value = parts[1]
when (currentSection) {
"Interface" -> interfaceParams[key] = value
"Peer" -> peerParams[key] = value
}
}
}
}
}
}
config.secretKey = interfaceParams["privatekey"].orEmpty()
config.remarks = System.currentTimeMillis().toString()
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.publicKey = peerParams["publickey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"].orEmpty()
val endpoint = peerParams["endpoint"].orEmpty()
val endpointParts = endpoint.split(":", limit = 2)
if (endpointParts.size == 2) {
config.server = endpointParts[0]
config.serverPort = endpointParts[1]
} else {
config.server = endpoint
config.serverPort = ""
}
config.reserved = peerParams["reserved"] ?: "0,0,0"
return config
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
}
wireguard.mtu = profileItem.mtu
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
}
return outboundBean
}
fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] = config.publicKey.orEmpty()
if (config.reserved != null) {
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty()
}
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty()
if (config.mtu != null) {
dicQuery["mtu"] = config.mtu.toString()
}
if (config.preSharedKey != null) {
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty()
}
return toUri(config, config.secretKey, dicQuery)
}
}

View File

@@ -1,29 +1,25 @@
package com.v2ray.ang.util
package com.v2ray.ang.handler
import android.content.Context
import android.graphics.Bitmap
import android.text.TextUtils
import android.util.Log
import com.google.gson.GsonBuilder
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R
import com.v2ray.ang.dto.*
import com.v2ray.ang.util.fmt.Hysteria2Fmt
import com.v2ray.ang.util.fmt.ShadowsocksFmt
import com.v2ray.ang.util.fmt.SocksFmt
import com.v2ray.ang.util.fmt.TrojanFmt
import com.v2ray.ang.util.fmt.VlessFmt
import com.v2ray.ang.util.fmt.VmessFmt
import com.v2ray.ang.util.fmt.WireguardFmt
import java.lang.reflect.Type
import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.fmt.ShadowsocksFmt
import com.v2ray.ang.fmt.SocksFmt
import com.v2ray.ang.fmt.TrojanFmt
import com.v2ray.ang.fmt.VlessFmt
import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils
import java.net.URI
import java.util.*
object AngConfigManager {
/**
@@ -33,7 +29,7 @@ object AngConfigManager {
str: String?,
subid: String,
subItem: SubscriptionItem?,
removedSelectedServer: ServerConfig?
removedSelectedServer: ProfileItem?
): Int {
try {
if (str == null || TextUtils.isEmpty(str)) {
@@ -71,12 +67,7 @@ object AngConfigManager {
config.subscriptionId = subid
val guid = MmkvManager.encodeServerConfig("", config)
if (removedSelectedServer != null &&
config.getProxyOutbound()
?.getServerAddress() == removedSelectedServer.getProxyOutbound()
?.getServerAddress() &&
config.getProxyOutbound()
?.getServerPort() == removedSelectedServer.getProxyOutbound()
?.getServerPort()
config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort
) {
MmkvManager.setSelectServer(guid)
}
@@ -177,7 +168,7 @@ object AngConfigManager {
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
try {
if (guid == null) return -1
val result = V2rayConfigUtil.getV2rayConfig(context, guid)
val result = V2rayConfigManager.getV2rayConfig(context, guid)
if (result.status) {
Utils.setClipboard(context, result.content)
} else {
@@ -218,6 +209,7 @@ object AngConfigManager {
var count = 0
servers.lines()
.distinct()
.forEach { str ->
if (Utils.isValidSubUrl(str)) {
count += importUrlAsSubscription(str)
@@ -255,6 +247,7 @@ object AngConfigManager {
val subItem = MmkvManager.decodeSubscription(subid)
var count = 0
servers.lines()
.distinct()
.reversed()
.forEach {
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
@@ -284,15 +277,10 @@ object AngConfigManager {
if (serverList.isNotEmpty()) {
var count = 0
for (srv in serverList.reversed()) {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.fullConfig =
JsonUtil.fromJson(JsonUtil.toJson(srv), V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
.toString())
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv))
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv)?:"")
count += 1
}
return count
@@ -303,10 +291,8 @@ object AngConfigManager {
try {
// For compatibility
val config = ServerConfig.create(EConfigType.CUSTOM)
val config = CustomFmt.parse(server) ?: return 0
config.subscriptionId = subid
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
@@ -316,9 +302,7 @@ object AngConfigManager {
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 config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
@@ -365,7 +349,8 @@ object AngConfigManager {
val httpPort = SettingsManager.getHttpPort()
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……")
//e.printStackTrace()
""
}
if (configText.isEmpty()) {

View File

@@ -0,0 +1,191 @@
package com.v2ray.ang.handler
import android.util.Log
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object MigrateManager {
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
fun migrateServerConfig2Profile(): Boolean {
if (serverStorage.count().toInt() == 0) {
return false
}
val serverList = serverStorage.allKeys() ?: return false
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
for (guid in serverList) {
var configOld = decodeServerConfigOld(guid) ?: continue
var config = decodeServerConfig(guid)
if (config != null) {
serverStorage.remove(guid)
continue
}
config = migrateServerConfig2ProfileSub(configOld) ?: continue
config.subscriptionId = configOld.subscriptionId
MmkvManager.encodeServerConfig(guid, config)
//check and remove old
decodeServerConfig(guid) ?: continue
serverStorage.remove(guid)
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
}
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
return true
}
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
return when (configOld.getProxyOutbound()?.protocol) {
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld)
EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld)
EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld)
EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld)
EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld)
else -> null
}
}
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(configOld.configType)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.method = outbound.getSecurityEncryption()
config.password = outbound.getPassword()
config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type
outbound.getTransportSettingDetails()?.let { transportDetails ->
config.headerType = transportDetails[0].orEmpty()
config.host = transportDetails[1].orEmpty()
config.path = transportDetails[2].orEmpty()
}
config.seed = outbound?.streamSettings?.kcpSettings?.seed
config.quicSecurity = outbound?.streamSettings?.quicSettings?.security
config.quicKey = outbound?.streamSettings?.quicSettings?.key
config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun"
config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName
config.authority = outbound?.streamSettings?.grpcSettings?.authority
config.security = outbound.streamSettings?.security
val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings
config.insecure = tlsSettings?.allowInsecure
config.sni = tlsSettings?.serverName
config.fingerPrint = tlsSettings?.fingerprint
config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString()
config.publicKey = tlsSettings?.publicKey
config.shortId = tlsSettings?.shortId
config.spiderX = tlsSettings?.spiderX
return config
}
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.SOCKS)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
config.password = outbound.getPassword()
return config
}
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.HTTP)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
config.password = outbound.getPassword()
return config
}
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
outbound.settings?.let { wireguard ->
config.secretKey = wireguard.secretKey
config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString()
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
config.mtu = wireguard.mtu
config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString()
}
return config
}
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.HYSTERIA2)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.password = outbound.getPassword()
config.security = AppConfig.TLS
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
config.insecure = tlsSetting.allowInsecure
config.sni = tlsSetting.serverName
config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty()
}
config.obfsPassword = outbound.settings?.obfsPassword
return config
}
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.CUSTOM)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
return config
}
private fun decodeServerConfigOld(guid: String): ServerConfig? {
if (guid.isBlank()) {
return null
}
val json = serverStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJson(json, ServerConfig::class.java)
}
}

View File

@@ -1,4 +1,4 @@
package com.v2ray.ang.util
package com.v2ray.ang.handler
import com.tencent.mmkv.MMKV
@@ -8,16 +8,17 @@ import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerAffiliationInfo
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object MmkvManager {
//region private
//private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
private const val ID_MAIN = "MAIN"
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
private const val ID_SERVER_RAW = "SERVER_RAW"
private const val ID_SERVER_AFF = "SERVER_AFF"
private const val ID_SUB = "SUB"
@@ -27,14 +28,14 @@ object MmkvManager {
private const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
private const val KEY_SUB_IDS = "SUB_IDS"
//private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
//endregion
@@ -61,31 +62,32 @@ object MmkvManager {
}
}
fun decodeServerConfig(guid: String): ServerConfig? {
if (guid.isBlank()) {
return null
}
val json = serverStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJson(json, ServerConfig::class.java)
}
fun decodeProfileConfig(guid: String): ProfileItem? {
fun decodeServerConfig(guid: String): ProfileItem? {
if (guid.isBlank()) {
return null
}
val json = profileStorage.decodeString(guid)
val json = profileFullStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJson(json, ProfileItem::class.java)
}
fun encodeServerConfig(guid: String, config: ServerConfig): String {
// fun decodeProfileConfig(guid: String): ProfileLiteItem? {
// if (guid.isBlank()) {
// return null
// }
// val json = profileStorage.decodeString(guid)
// if (json.isNullOrBlank()) {
// return null
// }
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
// }
fun encodeServerConfig(guid: String, config: ProfileItem): String {
val key = guid.ifBlank { Utils.getUuid() }
serverStorage.encode(key, JsonUtil.toJson(config))
profileFullStorage.encode(key, JsonUtil.toJson(config))
val serverList = decodeServerList()
if (!serverList.contains(key)) {
serverList.add(0, key)
@@ -94,14 +96,14 @@ object MmkvManager {
mainStorage.encode(KEY_SELECTED_SERVER, key)
}
}
val profile = ProfileItem(
configType = config.configType,
subscriptionId = config.subscriptionId,
remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
profileStorage.encode(key, JsonUtil.toJson(profile))
// val profile = ProfileLiteItem(
// configType = config.configType,
// subscriptionId = config.subscriptionId,
// remarks = config.remarks,
// server = config.getProxyOutbound()?.getServerAddress(),
// serverPort = config.getProxyOutbound()?.getServerPort(),
// )
// profileStorage.encode(key, JsonUtil.toJson(profile))
return key
}
@@ -115,8 +117,8 @@ object MmkvManager {
val serverList = decodeServerList()
serverList.remove(guid)
encodeServerList(serverList)
serverStorage.remove(guid)
profileStorage.remove(guid)
profileFullStorage.remove(guid)
//profileStorage.remove(guid)
serverAffStorage.remove(guid)
}
@@ -124,7 +126,7 @@ object MmkvManager {
if (subid.isBlank()) {
return
}
serverStorage.allKeys()?.forEach { key ->
profileFullStorage.allKeys()?.forEach { key ->
decodeServerConfig(key)?.let { config ->
if (config.subscriptionId == subid) {
removeServer(key)
@@ -164,8 +166,8 @@ object MmkvManager {
fun removeAllServer() {
mainStorage.clearAll()
serverStorage.clearAll()
profileStorage.clearAll()
profileFullStorage.clearAll()
//profileStorage.clearAll()
serverAffStorage.clearAll()
}
@@ -192,7 +194,7 @@ object MmkvManager {
}
fun decodeServerRaw(guid: String): String? {
return serverRawStorage.decodeString(guid) ?: return null
return serverRawStorage.decodeString(guid)
}
//endregion
@@ -302,9 +304,51 @@ object MmkvManager {
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
if (rulesetList.isNullOrEmpty())
settingsStorage.encode(PREF_ROUTING_RULESET, "")
encodeSettings(PREF_ROUTING_RULESET, "")
else
settingsStorage.encode(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
encodeSettings(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
}
//endregion
fun encodeSettings(key: String, value: String?): Boolean {
return settingsStorage.encode(key, value)
}
fun encodeSettings(key: String, value: Int): Boolean {
return settingsStorage.encode(key, value)
}
fun encodeSettings(key: String, value: Boolean): Boolean {
return settingsStorage.encode(key, value)
}
fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
return settingsStorage.encode(key, value)
}
fun decodeSettingsString(key: String): String? {
return settingsStorage.decodeString(key)
}
fun decodeSettingsString(key: String, defaultValue: String?): String? {
return settingsStorage.decodeString(key, defaultValue)
}
fun decodeSettingsBool(key: String): Boolean {
return settingsStorage.decodeBool(key,false)
}
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
return settingsStorage.decodeBool(key, defaultValue)
}
fun decodeSettingsInt(key: String, defaultValue: Int): Int {
return settingsStorage.decodeInt(key, defaultValue)
}
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
return settingsStorage.decodeStringSet(key)
}
//endregion
@@ -312,11 +356,11 @@ object MmkvManager {
//region Others
fun encodeStartOnBoot(startOnBoot: Boolean) {
settingsStorage.encode(PREF_IS_BOOTED, startOnBoot)
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
}
fun decodeStartOnBoot(): Boolean {
return settingsStorage.decodeBool(PREF_IS_BOOTED, false)
return decodeSettingsBool(PREF_IS_BOOTED, false)
}
//endregion

View File

@@ -1,21 +1,26 @@
package com.v2ray.ang.util
package com.v2ray.ang.handler
import android.content.Context
import android.content.res.AssetManager
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RoutingType
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.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.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerList
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.Utils.parseInt
import java.io.File
import java.io.FileOutputStream
import java.util.Collections
import kotlin.Int
object SettingsManager {
@@ -38,12 +43,12 @@ object SettingsManager {
}
fun resetRoutingRulesets(context: Context, index: Int) {
fun resetRoutingRulesetsFromPresets(context: Context, index: Int) {
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
resetRoutingRulesetsCommon(rulesetList)
}
fun resetRoutingRulesetsFromClipboard(content: String?): Boolean {
fun resetRoutingRulesets(content: String?): Boolean {
if (content.isNullOrEmpty()) {
return false
}
@@ -90,7 +95,7 @@ object SettingsManager {
if (rulesetList.isNullOrEmpty()) return
if (index < 0 || index >= rulesetList.count()) {
rulesetList.add(ruleset)
rulesetList.add(0, ruleset)
} else {
rulesetList[index] = ruleset
}
@@ -131,26 +136,51 @@ object SettingsManager {
MmkvManager.encodeSubsList(subsList)
}
fun getServerViaRemarks(remarks: String?): ServerConfig? {
fun getServerViaRemarks(remarks: String?): ProfileItem? {
if (remarks == null) {
return null
}
val serverList = decodeServerList()
for (guid in serverList) {
val profile = decodeProfileConfig(guid)
val profile = decodeServerConfig(guid)
if (profile != null && profile.remarks == remarks) {
return decodeServerConfig(guid)
return profile
}
}
return null
}
fun getSocksPort(): Int {
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
}
fun getHttpPort(): Int {
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
}
fun initAssets(context: Context, assets: AssetManager) {
val extFolder = Utils.userAssetPath(context)
try {
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
}
}

View File

@@ -1,25 +1,30 @@
package com.v2ray.ang.util
package com.v2ray.ang.handler
import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_DNSPOD_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_DNSPOD_DOMAIN
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_PUB_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_PUB_DOMAIN
import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN
import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_YANDEX_DOMAIN
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.GOOGLEAPIS_CN_DOMAIN
import com.v2ray.ang.AppConfig.GOOGLEAPIS_COM_DOMAIN
import com.v2ray.ang.AppConfig.HEADER_TYPE_HTTP
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
@@ -29,28 +34,30 @@ import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.fmt.HttpFmt
import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.fmt.ShadowsocksFmt
import com.v2ray.ang.fmt.SocksFmt
import com.v2ray.ang.fmt.TrojanFmt
import com.v2ray.ang.fmt.VlessFmt
import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object V2rayConfigUtil {
object V2rayConfigManager {
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid)
val customConfig = if (raw.isNullOrBlank()) {
config.fullConfig?.toPrettyPrinting() ?: return ConfigResult(false)
} else {
raw
}
val domainPort = config.getProxyOutbound()?.getServerAddressAndPort()
return ConfigResult(true, guid, customConfig, domainPort)
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
val domainPort = config.getServerAddressAndPort()
return ConfigResult(true, guid, raw, domainPort)
}
val result = getV2rayNonCustomConfig(context, config)
@@ -63,11 +70,10 @@ object V2rayConfigUtil {
}
}
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): ConfigResult {
private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult {
val result = ConfigResult(false)
val outbound = config.getProxyOutbound() ?: return result
val address = outbound.getServerAddress() ?: return result
val address = config.server ?: return result
if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
@@ -82,14 +88,13 @@ object V2rayConfigUtil {
}
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel =
settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning"
MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.remarks = config.remarks
inbounds(v2rayConfig)
val isPlugin = outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)
val retOut = outbounds(v2rayConfig, outbound, isPlugin)
val isPlugin = config.configType == EConfigType.HYSTERIA2
val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
routing(v2rayConfig)
@@ -98,10 +103,10 @@ object V2rayConfigUtil {
dns(v2rayConfig)
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
customLocalDns(v2rayConfig)
}
if (settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) != true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
v2rayConfig.stats = null
v2rayConfig.policy = null
}
@@ -118,20 +123,18 @@ object V2rayConfigUtil {
val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds.forEach { curInbound ->
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
//bind all inbounds to localhost if the user requests
curInbound.listen = LOOPBACK
}
}
v2rayConfig.inbounds[0].port = socksPort
val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED)
?: false
val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
val sniffAllTlsAndHttp =
settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
?: true
MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
v2rayConfig.inbounds[0].sniffing?.routeOnly =
settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
if (!sniffAllTlsAndHttp) {
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
}
@@ -154,11 +157,7 @@ object V2rayConfigUtil {
return true
}
private fun outbounds(
v2rayConfig: V2rayConfig,
outbound: V2rayConfig.OutboundBean,
isPlugin: Boolean
): Pair<Boolean, String> {
private fun outbounds(v2rayConfig: V2rayConfig, config: ProfileItem, isPlugin: Boolean): Pair<Boolean, String>? {
if (isPlugin) {
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
val outboundNew = V2rayConfig.OutboundBean(
@@ -181,8 +180,9 @@ object V2rayConfigUtil {
return Pair(true, outboundNew.getServerAddressAndPort())
}
val outbound = getProxyOutbound(config) ?: return null
val ret = updateOutboundWithGlobalSettings(outbound)
if (!ret) return Pair(false, "")
if (!ret) return null
if (v2rayConfig.outbounds.isNotEmpty()) {
v2rayConfig.outbounds[0] = outbound
@@ -191,12 +191,12 @@ object V2rayConfigUtil {
}
updateOutboundFragment(v2rayConfig)
return Pair(true, outbound.getServerAddressAndPort())
return Pair(true, config.getServerAddressAndPort())
}
private fun fakedns(v2rayConfig: V2rayConfig) {
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
&& settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
) {
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
}
@@ -206,7 +206,7 @@ object V2rayConfigUtil {
try {
v2rayConfig.routing.domainStrategy =
settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
?: "IPIfNonMatch"
val rulesetItems = MmkvManager.decodeRoutingRulesets()
@@ -256,7 +256,7 @@ object V2rayConfigUtil {
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
try {
if (settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
val geositeCn = arrayListOf(GEOSITE_CN)
val proxyDomain = userRule2Domain(TAG_PROXY)
val directDomain = userRule2Domain(TAG_DIRECT)
@@ -280,7 +280,7 @@ object V2rayConfigUtil {
)
val localDnsPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT),
MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT),
AppConfig.PORT_LOCAL_DNS.toInt()
)
v2rayConfig.inbounds.add(
@@ -337,10 +337,8 @@ object V2rayConfigUtil {
if (proxyDomain.size > 0) {
servers.add(
V2rayConfig.DnsBean.ServersBean(
remoteDns.first(),
53,
proxyDomain,
null
address = remoteDns.first(),
domains = proxyDomain,
)
)
}
@@ -353,10 +351,10 @@ object V2rayConfigUtil {
if (directDomain.size > 0) {
servers.add(
V2rayConfig.DnsBean.ServersBean(
domesticDns.first(),
53,
directDomain,
if (isCnRoutingMode) geoipCn else null
address = domesticDns.first(),
domains = directDomain,
expectIPs = if (isCnRoutingMode) geoipCn else null,
skipFallback = true
)
)
}
@@ -382,10 +380,12 @@ object V2rayConfigUtil {
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
// hardcode popular Android Private DNS rule to fix localhost DNS problem
hosts[DNS_PUB_DOMAIN] = DNS_PUB_ADDRESSES
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
hosts[DNS_ONE_ONE_DOMAIN] = DNS_ONE_ONE_ADDRESSES
hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_ADDRESSES
hosts[DNS_DNSPOD_DOMAIN] = DNS_DNSPOD_ADDRESSES
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
// DNS dns对象
@@ -414,7 +414,7 @@ object V2rayConfigUtil {
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
try {
var muxEnabled = settingsStorage?.decodeBool(AppConfig.PREF_MUX_ENABLED, false)
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
val protocol = outbound.protocol
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true)
@@ -425,18 +425,18 @@ object V2rayConfigUtil {
) {
muxEnabled = false
} else if (protocol.equals(EConfigType.VLESS.name, true)
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true
&& outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true
) {
muxEnabled = false
}
if (muxEnabled == true) {
outbound.mux?.enabled = true
outbound.mux?.concurrency =
settingsStorage?.decodeInt(AppConfig.PREF_MUX_CONCURRENCY) ?: 8
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_CONCURRENCY, 8)
outbound.mux?.xudpConcurrency =
settingsStorage?.decodeInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY) ?: 8
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY, 16)
outbound.mux?.xudpProxyUDP443 =
settingsStorage?.decodeString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
} else {
outbound.mux?.enabled = false
outbound.mux?.concurrency = -1
@@ -448,20 +448,20 @@ object V2rayConfigUtil {
} else {
outbound.settings?.address as List<*>
}
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) != true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) != true) {
localTunAddr = listOf(localTunAddr.first())
}
outbound.settings?.address = localTunAddr
}
if (outbound.streamSettings?.network == DEFAULT_NETWORK
&& outbound.streamSettings?.tcpSettings?.header?.type == HTTP
&& outbound.streamSettings?.tcpSettings?.header?.type == HEADER_TYPE_HTTP
) {
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
val requestString: String by lazy {
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (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 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
}
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString,
@@ -486,11 +486,11 @@ object V2rayConfigUtil {
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
try {
if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
return true
}
if (v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.TLS
&& v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.REALITY
if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS
&& v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY
) {
return true
}
@@ -503,12 +503,12 @@ object V2rayConfigUtil {
)
var packets =
settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY
&& packets == "tlshello"
) {
packets = "1-3"
} else if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.TLS
} else if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.TLS
&& packets != "tlshello"
) {
packets = "tlshello"
@@ -517,16 +517,16 @@ object V2rayConfigUtil {
fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
packets = packets,
length = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_LENGTH)
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
?: "50-100",
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL)
interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
?: "10-20"
),
noises = listOf(
V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
type = "rand",
packet = "100-200",
delay = "10-20",
packet = "10-20",
delay = "10-16",
)
),
)
@@ -562,7 +562,7 @@ object V2rayConfigUtil {
return returnPair
}
//fragment proxy
if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
return returnPair
}
@@ -578,7 +578,7 @@ object V2rayConfigUtil {
//Previous proxy
val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
if (prevNode != null) {
val prevOutbound = prevNode.getProxyOutbound()
val prevOutbound = getProxyOutbound(prevNode)
if (prevOutbound != null) {
updateOutboundWithGlobalSettings(prevOutbound)
prevOutbound.tag = TAG_PROXY + "2"
@@ -587,14 +587,14 @@ object V2rayConfigUtil {
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
dialerProxy = prevOutbound.tag
)
domainPort = prevOutbound.getServerAddressAndPort()
domainPort = prevNode.getServerAddressAndPort()
}
}
//Next proxy
val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
if (nextNode != null) {
val nextOutbound = nextNode.getProxyOutbound()
val nextOutbound = getProxyOutbound(nextNode)
if (nextOutbound != null) {
updateOutboundWithGlobalSettings(nextOutbound)
nextOutbound.tag = TAG_PROXY
@@ -616,4 +616,20 @@ object V2rayConfigUtil {
}
return returnPair
}
fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
return when (profileItem.configType) {
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
EConfigType.CUSTOM -> null
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toOutbound(profileItem)
EConfigType.SOCKS -> SocksFmt.toOutbound(profileItem)
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem)
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
}
}
}

View File

@@ -13,46 +13,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
package com.v2ray.ang.helper
/**
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
* Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
*
* @author Paul Burke (ipaulpro)
*/
public interface ItemTouchHelperAdapter {
interface ItemTouchHelperAdapter {
/**
* Called when an item has been dragged far enough to trigger a move. This is called every time
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
* an item is shifted, and **not** at the end of a "drop" event.<br></br>
* <br></br>
* Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
* adjusting the underlying data to reflect this move.
*
* @param fromPosition The start 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.
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
* @see RecyclerView.getAdapterPositionFor
* @see RecyclerView.ViewHolder.getAdapterPosition
*/
boolean onItemMove(int fromPosition, int toPosition);
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
void onItemMoveCompleted();
fun onItemMoveCompleted()
/**
* Called when an item has been dismissed by a swipe.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
* Called when an item has been dismissed by a swipe.<br></br>
* <br></br>
* Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
* adjusting the underlying data to reflect this removal.
*
* @param position The position of the item dismissed.
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
* @see RecyclerView.getAdapterPositionFor
* @see RecyclerView.ViewHolder.getAdapterPosition
*/
void onItemDismiss(int position);
fun onItemDismiss(position: Int)
}

View File

@@ -13,29 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper
package com.v2ray.ang.helper;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.ItemTouchHelper
/**
* Interface to notify an item ViewHolder of relevant callbacks from {@link
* ItemTouchHelper.Callback}.
* Interface to notify an item ViewHolder of relevant callbacks from [ ].
*
* @author Paul Burke (ipaulpro)
*/
public interface ItemTouchHelperViewHolder {
interface ItemTouchHelperViewHolder {
/**
* Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped.
* Called when the [ItemTouchHelper] first registers an item as being moved or swiped.
* Implementations should update the item view to indicate it's active state.
*/
void onItemSelected();
fun onItemSelected()
/**
* Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item
* Called when the [ItemTouchHelper] has completed the move or swipe, and the active item
* state should be cleared.
*/
void onItemClear();
fun onItemClear()
}

View File

@@ -1,159 +0,0 @@
/*
* Copyright (C) 2015 Paul Burke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.view.animation.DecelerateInterpolator;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
/**
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
* </br/>
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement
* {@link ItemTouchHelperViewHolder}.
*
* @author Paul Burke (ipaulpro)
*/
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private static final float ALPHA_FULL = 1.0f;
private static final float SWIPE_THRESHOLD = 0.25f;
private static final long ANIMATION_DURATION = 200;
private final ItemTouchHelperAdapter mAdapter;
private ValueAnimator mReturnAnimator;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
mAdapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
} else {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
// 不执行删除操作,仅返回项目到原位
returnViewToOriginalPosition(viewHolder);
}
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float maxSwipeDistance = viewHolder.itemView.getWidth() * SWIPE_THRESHOLD;
float swipeAmount = Math.abs(dX);
float direction = Math.signum(dX);
// 限制最大滑动距离
float translationX = Math.min(swipeAmount, maxSwipeDistance) * direction;
float alpha = ALPHA_FULL - Math.min(swipeAmount, maxSwipeDistance) / maxSwipeDistance;
viewHolder.itemView.setTranslationX(translationX);
viewHolder.itemView.setAlpha(alpha);
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
returnViewToOriginalPosition(viewHolder);
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
private void returnViewToOriginalPosition(RecyclerView.ViewHolder viewHolder) {
if (mReturnAnimator != null && mReturnAnimator.isRunning()) {
mReturnAnimator.cancel();
}
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.getTranslationX(), 0f);
mReturnAnimator.addUpdateListener(animation -> {
float value = (float) animation.getAnimatedValue();
viewHolder.itemView.setTranslationX(value);
viewHolder.itemView.setAlpha(1f - Math.abs(value) / (viewHolder.itemView.getWidth() * SWIPE_THRESHOLD));
});
mReturnAnimator.setInterpolator(new DecelerateInterpolator());
mReturnAnimator.setDuration(ANIMATION_DURATION);
mReturnAnimator.start();
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected();
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setAlpha(ALPHA_FULL);
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear();
}
mAdapter.onItemMoveCompleted();
}
@Override
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return 1.1f; // 设置一个大于1的值确保不会触发默认的滑动删除操作
}
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
return defaultValue * 10; // 增加滑动逃逸速度,使得更难触发滑动
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright (C) 2015 Paul Burke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper
import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.graphics.Canvas
import android.view.animation.DecelerateInterpolator
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.sign
/**
* An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br></br>
*
* Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement
* [ItemTouchHelperViewHolder].
*
* @author Paul Burke (ipaulpro)
*/
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
private var mReturnAnimator: ValueAnimator? = null
override fun isLongPressDragEnabled(): Boolean = true
override fun isItemViewSwipeEnabled(): Boolean = true
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags: Int
val swipeFlags: Int
if (recyclerView.layoutManager is GridLayoutManager) {
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
} else {
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
}
return makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(
recyclerView: RecyclerView,
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return if (source.itemViewType != target.itemViewType) {
false
} else {
mAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
true
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// Do not delete; simply return item to original position
returnViewToOriginalPosition(viewHolder)
}
override fun onChildDraw(
c: Canvas, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val maxSwipeDistance = viewHolder.itemView.width * SWIPE_THRESHOLD
val swipeAmount = abs(dX)
val direction = sign(dX)
// Limit maximum swipe distance
val translationX = min(swipeAmount, maxSwipeDistance) * direction
val alpha = ALPHA_FULL - min(swipeAmount, maxSwipeDistance) / maxSwipeDistance
viewHolder.itemView.translationX = translationX
viewHolder.itemView.alpha = alpha
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
returnViewToOriginalPosition(viewHolder)
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
private fun returnViewToOriginalPosition(viewHolder: RecyclerView.ViewHolder) {
mReturnAnimator?.takeIf { it.isRunning }?.cancel()
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.translationX, 0f).apply {
addUpdateListener { animation ->
val value = animation.animatedValue as Float
viewHolder.itemView.translationX = value
viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD))
}
interpolator = DecelerateInterpolator()
duration = ANIMATION_DURATION
start()
}
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is ItemTouchHelperViewHolder) {
viewHolder.onItemSelected()
}
super.onSelectedChanged(viewHolder, actionState)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = ALPHA_FULL
if (viewHolder is ItemTouchHelperViewHolder) {
viewHolder.onItemClear()
}
mAdapter.onItemMoveCompleted()
}
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
return 1.1f // Set a value greater than 1 to prevent default swipe delete
}
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
return defaultValue * 10 // Increase swipe escape velocity to make swipe harder to trigger
}
companion object {
private const val ALPHA_FULL = 1.0f
private const val SWIPE_THRESHOLD = 0.25f
private const val ANIMATION_DURATION: Long = 200
}
}

View File

@@ -29,7 +29,8 @@ class PluginList : ArrayList<Plugin>() {
init {
addAll(
AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
)
.filter { it.providerInfo.exported }.map { NativePlugin(it) })
}

View File

@@ -43,6 +43,7 @@ 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) {
@@ -88,21 +89,25 @@ object PluginManager {
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)
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)
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)
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)
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(pluginId, "fr.husi")), flags
)
.filter { it.providerInfo.exported }
}
if (providers.isEmpty()) {
@@ -138,9 +143,11 @@ object PluginManager {
authority(provider.authority)
}.build()
try {
return initNativeFast(AngApplication.application.contentResolver,
return initNativeFast(
AngApplication.application.contentResolver,
pluginId,
uri)?.let { InitResult(it) }
uri
)?.let { InitResult(it) }
} catch (t: Throwable) {
// Logs.w("Initializing native plugin fast mode failed")
failure?.also { t.addSuppressed(it) }
@@ -148,9 +155,11 @@ object PluginManager {
}
try {
return initNativeSlow(AngApplication.application.contentResolver,
return initNativeSlow(
AngApplication.application.contentResolver,
pluginId,
uri)?.let { InitResult(it) }
uri
)?.let { InitResult(it) }
} catch (t: Throwable) {
failure?.also { t.addSuppressed(it) }
throw t
@@ -180,11 +189,13 @@ object PluginManager {
throw IndexOutOfBoundsException("Plugin entry binary not found")
val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin")
(cr.query(uri,
(cr.query(
uri,
arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
null,
null,
null)
null
)
?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound()
pluginDir.deleteRecursively()
@@ -197,11 +208,13 @@ object PluginManager {
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
}
Os.chmod(file.absolutePath, when (cursor.getType(1)) {
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())
}
@@ -213,6 +226,7 @@ object PluginManager {
is String -> value
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
.getString(value)
null -> null
else -> error("meta-data $key has invalid type ${value.javaClass}")
}

View File

@@ -0,0 +1,18 @@
package com.v2ray.ang.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//Check if context is not null and action is the one we want
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
//Check if flag is true and a server is selected
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
//Start v2ray
V2RayServiceManager.startV2Ray(context)
}
}

View File

@@ -5,8 +5,8 @@ import android.content.Context
import android.content.Intent
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
class TaskerReceiver : BroadcastReceiver() {
@@ -16,9 +16,9 @@ class TaskerReceiver : BroadcastReceiver() {
try {
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "")
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty()
if (switch == null || guid == null || TextUtils.isEmpty(guid)) {
if (switch == null || TextUtils.isEmpty(guid)) {
return
} else if (switch) {
if (guid == AppConfig.TASKER_DEFAULT_GUID) {

View File

@@ -8,11 +8,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ProcessService {
private val TAG = ANG_PACKAGE
private lateinit var process: Process
private var process: Process? = null
fun runProcess(context: Context, cmd: MutableList<String>) {
Log.d(TAG, cmd.toString())
Log.d(ANG_PACKAGE, cmd.toString())
try {
val proBuilder = ProcessBuilder(cmd)
@@ -23,23 +22,23 @@ class ProcessService {
CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(50L)
Log.d(TAG, "runProcess check")
process.waitFor()
Log.d(TAG, "runProcess exited")
Log.d(ANG_PACKAGE, "runProcess check")
process?.waitFor()
Log.d(ANG_PACKAGE, "runProcess exited")
}
Log.d(TAG, process.toString())
Log.d(ANG_PACKAGE, process.toString())
} catch (e: Exception) {
Log.d(TAG, e.toString())
Log.d(ANG_PACKAGE, e.toString())
}
}
fun stopProcess() {
try {
Log.d(TAG, "runProcess destroy")
Log.d(ANG_PACKAGE, "runProcess destroy")
process?.destroy()
} catch (e: Exception) {
Log.d(TAG, e.toString())
Log.d(ANG_PACKAGE, e.toString())
}
}
}

View File

@@ -9,6 +9,7 @@ import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.util.MessageUtil
@@ -32,24 +33,31 @@ class QSTileService : TileService() {
qsTile?.updateTile()
}
/**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
*/
override fun onStartListening() {
super.onStartListening()
setState(Tile.STATE_INACTIVE)
mMsgReceive = ReceiveMessageHandler(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), Context.RECEIVER_EXPORTED)
} else {
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
}
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
}
override fun onStopListening() {
super.onStopListening()
unregisterReceiver(mMsgReceive)
try {
applicationContext.unregisterReceiver(mMsgReceive)
mMsgReceive = null
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onClick() {

View File

@@ -14,10 +14,8 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
import com.v2ray.ang.R
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import com.v2ray.ang.handler.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.handler.MmkvManager
object SubscriptionUpdater {

View File

@@ -13,21 +13,21 @@ import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toSpeedString
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.ui.MainActivity
import com.v2ray.ang.util.MessageUtil
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.V2rayConfigUtil
import go.Seq
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
@@ -55,7 +55,7 @@ object V2RayServiceManager {
Seq.setContext(value?.get()?.getService()?.applicationContext)
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
}
var currentConfig: ServerConfig? = null
var currentConfig: ProfileItem? = null
private var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null
@@ -65,15 +65,17 @@ object V2RayServiceManager {
fun startV2Ray(context: Context) {
if (v2rayPoint.isRunning) return
val guid = MmkvManager.getSelectServer() ?: return
val result = V2rayConfigUtil.getV2rayConfig(context, guid)
if (!result.status) return
val config = MmkvManager.decodeServerConfig(guid) ?: return
if (!Utils.isValidUrl(config.server) && !Utils.isIpAddress(config.server)) return
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) return
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) == true) {
context.toast(R.string.toast_warning_pref_proxysharing_short)
} else {
context.toast(R.string.toast_services_start)
}
val intent = if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
Intent(context.applicationContext, V2RayVpnService::class.java)
} else {
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
@@ -125,6 +127,11 @@ object V2RayServiceManager {
}
}
/**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
*/
fun startV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return
val guid = MmkvManager.getSelectServer() ?: return
@@ -132,7 +139,7 @@ object V2RayServiceManager {
if (v2rayPoint.isRunning) {
return
}
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status)
return
@@ -141,11 +148,7 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_SCREEN_ON)
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
mFilter.addAction(Intent.ACTION_USER_PRESENT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
} else {
service.registerReceiver(mMsgReceive, mFilter)
}
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
}
@@ -155,7 +158,7 @@ object V2RayServiceManager {
currentConfig = config
try {
v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false)
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
}
@@ -378,7 +381,7 @@ object V2RayServiceManager {
private fun startSpeedNotification() {
if (mDisposable == null &&
v2rayPoint.isRunning &&
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
) {
var lastZeroSpeed = false
val outboundTags = currentConfig?.getAllOutboundTags()

View File

@@ -8,12 +8,12 @@ import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.PluginUtil
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import go.Seq
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -24,7 +24,7 @@ import libv2ray.Libv2ray
import java.util.concurrent.Executors
class V2RayTestService : Service() {
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) }
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
override fun onCreate() {
super.onCreate()
@@ -56,12 +56,12 @@ class V2RayTestService : Service() {
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)
val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
if (config.configType == EConfigType.HYSTERIA2) {
val delay = PluginUtil.realPingHy2(this, config)
return delay
} else {
val config = V2rayConfigUtil.getV2rayConfig(this, guid)
val config = V2rayConfigManager.getV2rayConfig(this, guid)
if (!config.status) {
return retFailure
}

View File

@@ -10,6 +10,7 @@ import android.net.LocalSocketAddress
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Build
import android.os.ParcelFileDescriptor
@@ -17,12 +18,13 @@ import android.os.StrictMode
import android.util.Log
import androidx.annotation.RequiresApi
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -64,7 +66,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
.build()
}
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager }
@delegate:RequiresApi(Build.VERSION_CODES.P)
private val defaultNetworkCallback by lazy {
@@ -130,7 +132,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.addRoute("0.0.0.0", 0)
}
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
if (bypassLan) {
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
@@ -139,7 +141,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
}
// if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
// } else {
Utils.getVpnDnsServers()
@@ -153,9 +155,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
val selfPackageName = BuildConfig.APPLICATION_ID
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) {
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
//process self package
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
apps?.forEach {
@@ -165,6 +167,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
else
builder.addAllowedApplication(it)
} catch (e: PackageManager.NameNotFoundException) {
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
}
}
} else {
@@ -188,6 +191,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false)
builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOOPBACK, SettingsManager.getHttpPort()))
}
// Create a new interface using the builder and save the parameters.
@@ -215,12 +219,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
"--loglevel", "notice"
)
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
cmd.add("--netif-ip6addr")
cmd.add(PRIVATE_VLAN6_ROUTER)
}
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())
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
cmd.add("--dnsgw")
cmd.add("$LOOPBACK:${localDnsPort}")
}
@@ -232,7 +236,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
process = proBuilder
.directory(applicationContext.filesDir)
.start()
Thread(Runnable {
Thread {
Log.d(packageName, "$TUN2SOCKS check")
process.waitFor()
Log.d(packageName, "$TUN2SOCKS exited")
@@ -240,7 +244,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
Log.d(packageName, "$TUN2SOCKS restart")
runTun2socks()
}
}).start()
}.start()
Log.d(packageName, process.toString())
sendFd()

View File

@@ -20,6 +20,8 @@ import com.v2ray.ang.util.ZipUtil
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
class AboutActivity : BaseActivity() {
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
@@ -87,6 +89,9 @@ class AboutActivity : BaseActivity() {
binding.layoutFeedback.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGIssues)
}
binding.layoutOssLicenses.setOnClickListener{
startActivity(Intent(this, OssLicensesMenuActivity::class.java))
}
binding.layoutTgChannel.setOnClickListener {
Utils.openUri(this, AppConfig.TgChannelUrl)

View File

@@ -34,11 +34,11 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MigrateManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@@ -90,7 +90,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.fab.setOnClickListener {
if (mainViewModel.isRunning.value == true) {
Utils.stopVService(this)
} else if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
val intent = VpnService.prepare(this)
if (intent == null) {
startV2Ray()
@@ -126,13 +126,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
initGroupTab()
setupViewModel()
migrateLegacy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RxPermissions(this)
.request(Manifest.permission.POST_NOTIFICATIONS)
.subscribe {
if (!it)
toast(R.string.toast_permission_denied)
toast(R.string.toast_permission_denied_notification)
}
}
@@ -172,7 +173,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
}
mainViewModel.startListenBroadcast()
mainViewModel.copyAssets(assets)
mainViewModel.initAssets(assets)
}
private fun migrateLegacy() {
lifecycleScope.launch(Dispatchers.IO) {
val result = MigrateManager.migrateServerConfig2Profile()
launch(Dispatchers.Main) {
if (result) {
toast(getString(R.string.migration_success))
mainViewModel.reloadServerList()
} else {
//toast(getString(R.string.migration_fail))
}
}
}
}
private fun initGroupTab() {
@@ -504,6 +520,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
toast(R.string.toast_success)
mainViewModel.reloadServerList()
}
countSub > 0 -> initGroupTab()
else -> toast(R.string.toast_failure)
}
@@ -606,10 +623,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
//toast(R.string.toast_success)
toast(R.string.connection_test_testing)
toast(R.string.toast_success)
mainViewModel.reloadServerList()
mainViewModel.testAllRealPing()
} else {
toast(R.string.toast_failure)
}

View File

@@ -17,12 +17,11 @@ import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
@@ -130,6 +129,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
holder.itemMainBinding.layoutEdit.setOnClickListener {
val intent = Intent().putExtra("guid", guid)
.putExtra("isRunning", isRunning)
.putExtra("createConfigType", profile.configType.value)
if (profile.configType == EConfigType.CUSTOM) {
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
} else {
@@ -138,7 +138,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
}
holder.itemMainBinding.layoutRemove.setOnClickListener {
if (guid != MmkvManager.getSelectServer()) {
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
removeServer(guid, position)

View File

@@ -17,8 +17,8 @@ import com.v2ray.ang.databinding.ActivityBypassListBinding
import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
@@ -41,7 +41,7 @@ class PerAppProxyActivity : BaseActivity() {
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
binding.recyclerView.addItemDecoration(dividerItemDecoration)
val blacklist = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
AppManagerUtil.rxLoadNetworkAppList(this)
.subscribeOn(Schedulers.io())
@@ -132,14 +132,14 @@ class PerAppProxyActivity : BaseActivity() {
***/
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY, isChecked)
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
}
binding.switchPerAppProxy.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
binding.switchPerAppProxy.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
binding.switchBypassApps.setOnCheckedChangeListener { _, isChecked ->
settingsStorage.encode(AppConfig.PREF_BYPASS_APPS, isChecked)
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
}
binding.switchBypassApps.isChecked = settingsStorage.getBoolean(AppConfig.PREF_BYPASS_APPS, false)
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
/***
et_search.setOnEditorActionListener { v, actionId, event ->
@@ -175,7 +175,7 @@ class PerAppProxyActivity : BaseActivity() {
override fun onPause() {
super.onPause()
adapter?.let {
settingsStorage.encode(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
}
}
@@ -215,7 +215,7 @@ class PerAppProxyActivity : BaseActivity() {
}
it.notifyDataSetChanged()
true
} ?: false
} == true
R.id.select_proxy_app -> {
selectProxyApp()

View File

@@ -1,7 +1,6 @@
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
@@ -10,7 +9,7 @@ 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.handler.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -38,7 +37,7 @@ class RoutingEditActivity : BaseActivity() {
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
binding.chkLocked.isChecked = rulesetItem.looked ?: false
binding.chkLocked.isChecked = rulesetItem.looked == true
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
binding.etPort.text = Utils.getEditable(rulesetItem.port)

View File

@@ -1,26 +1,27 @@
package com.v2ray.ang.ui
import android.Manifest
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.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.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
@@ -52,14 +53,14 @@ class RoutingSettingActivity : BaseActivity() {
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
val found = Utils.arrayFind(routing_domain_strategy, settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
settingsStorage.encode(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position])
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position])
}
}
}
@@ -85,13 +86,13 @@ class RoutingSettingActivity : BaseActivity() {
true
}
R.id.import_rulesets -> {
R.id.import_predefined_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)
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
launch(Dispatchers.Main) {
refreshData()
toast(R.string.toast_success)
@@ -122,7 +123,7 @@ class RoutingSettingActivity : BaseActivity() {
return@setPositiveButton
}
lifecycleScope.launch(Dispatchers.IO) {
val result = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
val result = SettingsManager.resetRoutingRulesets(clipboard)
withContext(Dispatchers.Main) {
if (result) {
refreshData()
@@ -140,6 +141,18 @@ class RoutingSettingActivity : BaseActivity() {
true
}
R.id.import_rulesets_from_qrcode -> {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
}
true
}
R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets()
@@ -155,6 +168,34 @@ class RoutingSettingActivity : BaseActivity() {
else -> super.onOptionsItemSelected(item)
}
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importRulesetsFromQRcode(it.data?.getStringExtra("SCAN_RESULT"))
}
}
private fun importRulesetsFromQRcode(qrcode: String?): Boolean {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
val result = SettingsManager.resetRoutingRulesets(qrcode)
withContext(Dispatchers.Main) {
if (result) {
refreshData()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do nothing
}
.show()
return true
}
fun refreshData() {
rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())

View File

@@ -8,10 +8,9 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.databinding.ItemRecyclerRoutingSettingBinding
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.ui.MainRecyclerAdapter.BaseViewHolder
import com.v2ray.ang.util.SettingsManager
class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : RecyclerView.Adapter<RoutingSettingRecyclerAdapter.MainViewHolder>(),
ItemTouchHelperAdapter {
@@ -26,7 +25,7 @@ class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : Recy
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.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.looked == true
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
@@ -37,7 +36,7 @@ class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : Recy
}
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if( !it.isPressed) return@setOnCheckedChangeListener
if (!it.isPressed) return@setOnCheckedChangeListener
ruleset.enabled = isChecked
SettingsManager.saveRoutingRuleset(position, ruleset)
}

View File

@@ -7,7 +7,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.handler.AngConfigManager
class ScScannerActivity : BaseActivity() {

View File

@@ -12,7 +12,7 @@ import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.QRCodeDecoder
import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanCustomCode
@@ -25,7 +25,7 @@ class ScannerActivity : BaseActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (settingsStorage?.decodeBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
launchScan()
}
}

View File

@@ -2,6 +2,7 @@ package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -13,22 +14,23 @@ import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_PORT
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.REALITY
import com.v2ray.ang.AppConfig.TLS
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.Utils.getIpv6Address
class ServerActivity : BaseActivity() {
@@ -79,6 +81,10 @@ class ServerActivity : BaseActivity() {
private val alpns: Array<out String> by lazy {
resources.getStringArray(R.array.streamsecurity_alpn)
}
private val xhttpMode: Array<out String> by lazy {
resources.getStringArray(R.array.xhttp_mode)
}
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
// We don't use AndroidViewBinding because, it is better to share similar logics for different
@@ -87,7 +93,6 @@ class ServerActivity : BaseActivity() {
private val et_address: EditText by lazy { findViewById(R.id.et_address) }
private val et_port: EditText by lazy { findViewById(R.id.et_port) }
private val et_id: EditText by lazy { findViewById(R.id.et_id) }
private val et_alterId: EditText? by lazy { findViewById(R.id.et_alterId) }
private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
@@ -108,17 +113,21 @@ class ServerActivity : BaseActivity() {
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
private val et_preshared_key: EditText? by lazy { findViewById(R.id.et_preshared_key) }
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
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_reserved2: EditText? by lazy { findViewById(R.id.et_reserved2) }
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -142,7 +151,7 @@ class ServerActivity : BaseActivity() {
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
id: Long,
) {
val types = transportTypes(networks[position])
sp_header_type?.isEnabled = types.size > 1
@@ -150,25 +159,49 @@ class ServerActivity : BaseActivity() {
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
sp_header_type?.adapter = adapter
sp_header_type_title?.text = if (networks[position] == "grpc")
getString(R.string.server_lab_mode_type) else
getString(R.string.server_lab_head_type)
config?.getProxyOutbound()?.getTransportSettingDetails()?.let { transportDetails ->
sp_header_type?.setSelection(Utils.arrayFind(types, transportDetails[0]))
et_request_host?.text = Utils.getEditable(transportDetails[1])
et_path?.text = Utils.getEditable(transportDetails[2])
}
sp_header_type_title?.text =
when (networks[position]) {
NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type)
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode)
else -> getString(R.string.server_lab_head_type)
}.orEmpty()
sp_header_type?.setSelection(
Utils.arrayFind(
types,
when (networks[position]) {
NetworkType.GRPC.type -> config?.mode
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> config?.xhttpMode
else -> config?.headerType
}.orEmpty()
)
)
et_request_host?.text = Utils.getEditable(
when (networks[position]) {
//"quic" -> config?.quicSecurity
NetworkType.GRPC.type -> config?.authority
else -> config?.host
}.orEmpty()
)
et_path?.text = Utils.getEditable(
when (networks[position]) {
NetworkType.KCP.type -> config?.seed
//"quic" -> config?.quicKey
NetworkType.GRPC.type -> config?.serviceName
else -> config?.path
}.orEmpty()
)
tv_request_host?.text = Utils.getEditable(
getString(
when (networks[position]) {
"tcp" -> R.string.server_lab_request_host_http
"ws" -> R.string.server_lab_request_host_ws
"httpupgrade" -> R.string.server_lab_request_host_httpupgrade
"splithttp" -> R.string.server_lab_request_host_splithttp
"h2" -> R.string.server_lab_request_host_h2
"quic" -> R.string.server_lab_request_host_quic
"grpc" -> R.string.server_lab_request_host_grpc
NetworkType.TCP.type -> R.string.server_lab_request_host_http
NetworkType.WS.type -> R.string.server_lab_request_host_ws
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp
NetworkType.H2.type -> R.string.server_lab_request_host_h2
//"quic" -> R.string.server_lab_request_host_quic
NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc
else -> R.string.server_lab_request_host
}
)
@@ -177,17 +210,29 @@ class ServerActivity : BaseActivity() {
tv_path?.text = Utils.getEditable(
getString(
when (networks[position]) {
"kcp" -> R.string.server_lab_path_kcp
"ws" -> R.string.server_lab_path_ws
"httpupgrade" -> R.string.server_lab_path_httpupgrade
"splithttp" -> R.string.server_lab_path_splithttp
"h2" -> R.string.server_lab_path_h2
"quic" -> R.string.server_lab_path_quic
"grpc" -> R.string.server_lab_path_grpc
NetworkType.KCP.type -> R.string.server_lab_path_kcp
NetworkType.WS.type -> R.string.server_lab_path_ws
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
NetworkType.H2.type -> R.string.server_lab_path_h2
//"quic" -> R.string.server_lab_path_quic
NetworkType.GRPC.type -> R.string.server_lab_path_grpc
else -> R.string.server_lab_path
}
)
)
et_extra?.text = Utils.getEditable(
when (networks[position]) {
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> config?.xhttpExtra
else -> null
}.orEmpty()
)
layout_extra?.visibility =
when (networks[position]) {
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> View.VISIBLE
else -> View.GONE
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@@ -199,7 +244,7 @@ class ServerActivity : BaseActivity() {
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
id: Long,
) {
val isBlank = streamSecuritys[position].isBlank()
val isTLS = streamSecuritys[position] == TLS
@@ -243,7 +288,6 @@ class ServerActivity : BaseActivity() {
).forEach { it?.visibility = View.VISIBLE }
}
}
}
override fun onNothingSelected(p0: AdapterView<*>?) {
@@ -260,110 +304,81 @@ class ServerActivity : BaseActivity() {
/**
* binding selected server config
*/
private fun bindingServer(config: ServerConfig): Boolean {
val outbound = config.getProxyOutbound() ?: return false
private fun bindingServer(config: ProfileItem): Boolean {
et_remarks.text = Utils.getEditable(config.remarks)
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty())
et_port.text =
Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString())
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
et_alterId?.text =
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString())
if (config.configType == EConfigType.SOCKS
|| config.configType == EConfigType.HTTP
) {
et_security?.text =
Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
et_address.text = Utils.getEditable(config.server.orEmpty())
et_port.text = Utils.getEditable(config.serverPort ?: DEFAULT_PORT.toString())
et_id.text = Utils.getEditable(config.password.orEmpty())
if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
et_security?.text = Utils.getEditable(config.username.orEmpty())
} else if (config.configType == EConfigType.VLESS) {
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty())
val flow = Utils.arrayFind(
flows,
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty()
)
et_security?.text = Utils.getEditable(config.method.orEmpty())
val flow = Utils.arrayFind(flows, config.flow.orEmpty())
if (flow >= 0) {
sp_flow?.setSelection(flow)
}
} else if (config.configType == EConfigType.WIREGUARD) {
et_public_key?.text =
Utils.getEditable(outbound.settings?.peers?.get(0)?.publicKey.orEmpty())
if (outbound.settings?.reserved == null) {
et_reserved1?.text = Utils.getEditable("0")
et_reserved2?.text = Utils.getEditable("0")
et_reserved3?.text = Utils.getEditable("0")
} else {
et_reserved1?.text =
Utils.getEditable(outbound.settings?.reserved?.get(0).toString())
et_reserved2?.text =
Utils.getEditable(outbound.settings?.reserved?.get(1).toString())
et_reserved3?.text =
Utils.getEditable(outbound.settings?.reserved?.get(2).toString())
}
if (outbound.settings?.address == null) {
et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
} else {
val list = outbound.settings?.address as List<*>
et_local_address?.text = Utils.getEditable(list.joinToString(","))
}
if (outbound.settings?.mtu == null) {
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
} else {
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString())
}
et_id.text = Utils.getEditable(config.secretKey.orEmpty())
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
et_preshared_key?.visibility = View.VISIBLE
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
et_local_address?.text = Utils.getEditable(
config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6"
)
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
} else if (config.configType == EConfigType.HYSTERIA2) {
et_obfs_password?.text = Utils.getEditable(outbound.settings?.obfsPassword)
et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
et_port_hop?.text = Utils.getEditable(config.portHopping)
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
}
val securityEncryptions =
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
val security =
Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
if (security >= 0) {
sp_security?.setSelection(security)
}
val streamSetting = config.outboundBean?.streamSettings ?: return true
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
val streamSecurity = Utils.arrayFind(streamSecuritys, config.security.orEmpty())
if (streamSecurity >= 0) {
sp_stream_security?.setSelection(streamSecurity)
(streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting ->
container_sni?.visibility = View.VISIBLE
container_fingerprint?.visibility = View.VISIBLE
container_alpn?.visibility = View.VISIBLE
et_sni?.text = Utils.getEditable(tlsSetting.serverName)
tlsSetting.fingerprint?.let {
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint)
et_sni?.text = Utils.getEditable(config.sni)
config.fingerPrint?.let {
val utlsIndex = Utils.arrayFind(uTlsItems, it)
sp_stream_fingerprint?.setSelection(utlsIndex)
}
tlsSetting.alpn?.let {
val alpnIndex = Utils.arrayFind(
alpns,
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
)
config.alpn?.let {
val alpnIndex = Utils.arrayFind(alpns, it)
sp_stream_alpn?.setSelection(alpnIndex)
}
if (streamSetting.tlsSettings != null) {
if (config.security == TLS) {
container_allow_insecure?.visibility = View.VISIBLE
val allowinsecure =
Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
val allowinsecure = Utils.arrayFind(allowinsecures, config.insecure.toString())
if (allowinsecure >= 0) {
sp_allow_insecure?.setSelection(allowinsecure)
}
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else { // reality settings
} else if (config.security == REALITY) {
container_public_key?.visibility = View.VISIBLE
et_public_key?.text = Utils.getEditable(tlsSetting.publicKey.orEmpty())
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
container_short_id?.visibility = View.VISIBLE
et_short_id?.text = Utils.getEditable(tlsSetting.shortId.orEmpty())
et_short_id?.text = Utils.getEditable(config.shortId.orEmpty())
container_spider_x?.visibility = View.VISIBLE
et_spider_x?.text = Utils.getEditable(tlsSetting.spiderX.orEmpty())
et_spider_x?.text = Utils.getEditable(config.spiderX.orEmpty())
container_allow_insecure?.visibility = View.GONE
}
}
if (streamSetting.tlsSettings == null && streamSetting.realitySettings == null) {
if (config.security.isNullOrEmpty()) {
container_sni?.visibility = View.GONE
container_fingerprint?.visibility = View.GONE
container_alpn?.visibility = View.GONE
@@ -372,8 +387,7 @@ class ServerActivity : BaseActivity() {
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
}
}
val network = Utils.arrayFind(networks, streamSetting.network)
val network = Utils.arrayFind(networks, config.network.orEmpty())
if (network >= 0) {
sp_network?.setSelection(network)
}
@@ -388,7 +402,6 @@ class ServerActivity : BaseActivity() {
et_address.text = null
et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
et_id.text = null
et_alterId?.text = Utils.getEditable("0")
sp_security?.setSelection(0)
sp_network?.setSelection(0)
@@ -402,9 +415,7 @@ class ServerActivity : BaseActivity() {
//et_security.text = null
sp_flow?.setSelection(0)
et_public_key?.text = null
et_reserved1?.text = Utils.getEditable("0")
et_reserved2?.text = Utils.getEditable("0")
et_reserved3?.text = Utils.getEditable("0")
et_reserved1?.text = Utils.getEditable("0,0,0")
et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
@@ -423,13 +434,14 @@ class ServerActivity : BaseActivity() {
toast(R.string.server_lab_address)
return false
}
val port = Utils.parseInt(et_port.text.toString())
if (port <= 0) {
if (createConfigType != EConfigType.HYSTERIA2) {
if (Utils.parseInt(et_port.text.toString()) <= 0) {
toast(R.string.server_lab_port)
return false
}
}
val config =
MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
if (config.configType != EConfigType.SOCKS
&& config.configType != EConfigType.HTTP
&& TextUtils.isEmpty(et_id.text.toString())
@@ -450,125 +462,82 @@ class ServerActivity : BaseActivity() {
return false
}
}
et_alterId?.let {
val alterId = Utils.parseInt(it.text.toString())
if (alterId < 0) {
toast(R.string.server_lab_alterid)
if (et_extra?.text?.toString().isNotNullEmpty()) {
if (JsonUtil.parseString(et_extra?.text?.toString()) == null) {
toast(R.string.server_lab_xhttp_extra)
return false
}
}
config.remarks = et_remarks.text.toString().trim()
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
saveVnext(vnext, port, config)
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
saveServers(server, port, config)
}
val wireguard = config.outboundBean?.settings
wireguard?.peers?.get(0)?.let { _ ->
savePeer(wireguard, port)
}
saveCommon(config)
saveStreamSettings(config)
saveTls(config)
config.outboundBean?.streamSettings?.let {
val sni = saveStreamSettings(it)
saveTls(it, sni)
}
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty()
}
if (config.configType == EConfigType.HYSTERIA2) {
config.outboundBean?.settings?.obfsPassword = et_obfs_password?.text?.toString()
}
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "")
MmkvManager.encodeServerConfig(editGuid, config)
toast(R.string.toast_success)
finish()
return true
}
private fun saveVnext(
vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean,
port: Int,
config: ServerConfig
) {
vnext.address = et_address.text.toString().trim()
vnext.port = port
vnext.users[0].id = et_id.text.toString().trim()
private fun saveCommon(config: ProfileItem) {
config.remarks = et_remarks.text.toString().trim()
config.server = et_address.text.toString().trim()
config.serverPort = et_port.text.toString().trim()
config.password = et_id.text.toString().trim()
if (config.configType == EConfigType.VMESS) {
vnext.users[0].alterId = Utils.parseInt(et_alterId?.text.toString())
vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0]
config.method = securitys[sp_security?.selectedItemPosition ?: 0]
} else if (config.configType == EConfigType.VLESS) {
vnext.users[0].encryption = et_security?.text.toString().trim()
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0]
}
}
private fun saveServers(
server: V2rayConfig.OutboundBean.OutSettingsBean.ServersBean,
port: Int,
config: ServerConfig
) {
server.address = et_address.text.toString().trim()
server.port = port
if (config.configType == EConfigType.SHADOWSOCKS) {
server.password = et_id.text.toString().trim()
server.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
config.method = et_security?.text.toString().trim()
config.flow = flows[sp_flow?.selectedItemPosition ?: 0]
} else if (config.configType == EConfigType.SHADOWSOCKS) {
config.method = shadowsocksSecuritys[sp_security?.selectedItemPosition ?: 0]
} else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) {
server.users = null
} else {
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = et_security?.text.toString().trim()
socksUsersBean.pass = et_id.text.toString().trim()
server.users = listOf(socksUsersBean)
if (!TextUtils.isEmpty(et_security?.text) || !TextUtils.isEmpty(et_id.text)) {
config.username = et_security?.text.toString().trim()
}
} else if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.HYSTERIA2) {
server.password = et_id.text.toString().trim()
} else if (config.configType == EConfigType.TROJAN) {
} else if (config.configType == EConfigType.WIREGUARD) {
config.secretKey = et_id.text.toString().trim()
config.publicKey = et_public_key?.text.toString().trim()
config.preSharedKey = et_preshared_key?.text.toString().trim()
config.reserved = et_reserved1?.text.toString().trim()
config.localAddress = et_local_address?.text.toString().trim()
config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
} else if (config.configType == EConfigType.HYSTERIA2) {
config.obfsPassword = et_obfs_password?.text?.toString()
config.portHopping = et_port_hop?.text?.toString()
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
config.pinSHA256 = et_pinsha256?.text?.toString()
}
}
private fun savePeer(wireguard: V2rayConfig.OutboundBean.OutSettingsBean, port: Int) {
wireguard.secretKey = et_id.text.toString().trim()
wireguard.peers?.get(0)?.publicKey = et_public_key?.text.toString().trim()
wireguard.peers?.get(0)?.endpoint =
getIpv6Address(et_address.text.toString().trim()) + ":" + port
val reserved1 = Utils.parseInt(et_reserved1?.text.toString())
val reserved2 = Utils.parseInt(et_reserved2?.text.toString())
val reserved3 = Utils.parseInt(et_reserved3?.text.toString())
if (reserved1 > 0 || reserved2 > 0 || reserved3 > 0) {
wireguard.reserved = listOf(reserved1, reserved2, reserved3)
} else {
wireguard.reserved = null
}
wireguard.address = et_local_address?.text.toString().removeWhiteSpace().split(",")
wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString())
private fun saveStreamSettings(profileItem: ProfileItem) {
val network = sp_network?.selectedItemPosition ?: return
val type = sp_header_type?.selectedItemPosition ?: return
val requestHost = et_request_host?.text?.toString()?.trim() ?: return
val path = et_path?.text?.toString()?.trim() ?: return
profileItem.network = networks[network]
profileItem.headerType = transportTypes(networks[network])[type]
profileItem.host = requestHost
profileItem.path = path
profileItem.seed = path
profileItem.quicSecurity = requestHost
profileItem.quicKey = path
profileItem.mode = transportTypes(networks[network])[type]
profileItem.serviceName = path
profileItem.authority = requestHost
profileItem.xhttpMode = transportTypes(networks[network])[type]
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
}
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean): String? {
val network = sp_network?.selectedItemPosition ?: return null
val type = sp_header_type?.selectedItemPosition ?: return null
val requestHost = et_request_host?.text?.toString()?.trim() ?: return null
val path = et_path?.text?.toString()?.trim() ?: return null
val sni = streamSetting.populateTransportSettings(
transport = networks[network],
headerType = transportTypes(networks[network])[type],
host = requestHost,
path = path,
seed = path,
quicSecurity = requestHost,
key = path,
mode = transportTypes(networks[network])[type],
serviceName = path,
authority = requestHost,
)
return sni
}
private fun saveTls(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean, sni: String?) {
private fun saveTls(config: ProfileItem) {
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
val sniField = et_sni?.text?.toString()?.trim()
val allowInsecureField = sp_allow_insecure?.selectedItemPosition
@@ -580,37 +549,39 @@ class ServerActivity : BaseActivity() {
val allowInsecure =
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE)
} else {
allowinsecures[allowInsecureField].toBoolean()
}
streamSetting.populateTlsSettings(
streamSecurity = streamSecuritys[streamSecurity],
allowInsecure = allowInsecure,
sni = sniField ?: sni ?: "",
fingerprint = uTlsItems[utlsIndex],
alpns = alpns[alpnIndex],
publicKey = publicKey,
shortId = shortId,
spiderX = spiderX
)
config.security = streamSecuritys[streamSecurity]
config.insecure = allowInsecure
config.sni = sniField
config.fingerPrint = uTlsItems[utlsIndex]
config.alpn = alpns[alpnIndex]
config.publicKey = publicKey
config.shortId = shortId
config.spiderX = spiderX
}
private fun transportTypes(network: String?): Array<out String> {
return when (network) {
"tcp" -> {
NetworkType.TCP.type -> {
tcpTypes
}
"kcp", "quic" -> {
NetworkType.KCP.type -> {
kcpAndQuicTypes
}
"grpc" -> {
NetworkType.GRPC.type -> {
grpcModes
}
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
xhttpMode
}
else -> {
arrayOf("---")
}
@@ -618,12 +589,12 @@ class ServerActivity : BaseActivity() {
}
/**
* save server config
* delete server config
*/
private fun deleteServer(): Boolean {
if (editGuid.isNotEmpty()) {
if (editGuid != MmkvManager.getSelectServer()) {
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
MmkvManager.removeServer(editGuid)

View File

@@ -8,15 +8,13 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.utils.EditorTheme
import com.blacksquircle.ui.language.json.JsonLanguage
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat
@@ -50,10 +48,10 @@ class ServerCustomConfigActivity : BaseActivity() {
/**
* Binding selected server config
*/
private fun bindingServer(config: ServerConfig): Boolean {
private fun bindingServer(config: ProfileItem): Boolean {
binding.etRemarks.text = Utils.getEditable(config.remarks)
val raw = MmkvManager.decodeServerRaw(editGuid)
val configContent = raw ?: config.fullConfig?.toPrettyPrinting().orEmpty()
val configContent = raw.orEmpty()
binding.editor.setTextContent(Utils.getEditable(configContent))
return true
@@ -76,17 +74,20 @@ class ServerCustomConfigActivity : BaseActivity() {
return false
}
val v2rayConfig = try {
JsonUtil.fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
val profileItem = try {
CustomFmt.parse(binding.editor.text.toString())
} catch (e: Exception) {
e.printStackTrace()
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
return false
}
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM)
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString()
config.fullConfig = v2rayConfig
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM)
binding.etRemarks.text.let {
config.remarks = if (it.isNullOrEmpty()) profileItem?.remarks.orEmpty() else it.toString()
}
config.server = profileItem?.server
config.serverPort = profileItem?.serverPort
MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())

View File

@@ -17,8 +17,8 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R
import com.v2ray.ang.extension.toLongEx
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.SubscriptionUpdater
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.SettingsViewModel
import java.util.concurrent.TimeUnit
@@ -172,33 +172,33 @@ class SettingsActivity : BaseActivity() {
override fun onStart() {
super.onStart()
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, VPN))
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_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)
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false))
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)
muxConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8")
muxXudpConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false))
mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")
muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
updateFragment(settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false))
fragment?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false)
fragmentPackets?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
fragmentLength?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false))
fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)
fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
autoUpdateInterval?.summary =
settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
httpPort?.summary = settingsStorage.decodeString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
remoteDns?.summary = settingsStorage.decodeString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
domesticDns?.summary = settingsStorage.decodeString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
delayTestUrl?.summary = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
httpPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
initSharedPreference()
}
@@ -225,7 +225,7 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_SNIFFING_ENABLED,
).forEach { key ->
findPreference<CheckBoxPreference>(key)?.isChecked =
settingsStorage.decodeBool(key, true)
MmkvManager.decodeSettingsBool(key, true)
}
listOf(
@@ -240,7 +240,7 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_ALLOW_INSECURE
).forEach { key ->
findPreference<CheckBoxPreference>(key)?.isChecked =
settingsStorage.decodeBool(key, false)
MmkvManager.decodeSettingsBool(key, false)
}
listOf(
@@ -252,8 +252,8 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_LOGLEVEL,
AppConfig.PREF_MODE
).forEach { key ->
if (settingsStorage.decodeString(key) != null) {
findPreference<ListPreference>(key)?.value = settingsStorage.decodeString(key)
if (MmkvManager.decodeSettingsString(key) != null) {
findPreference<ListPreference>(key)?.value = MmkvManager.decodeSettingsString(key)
}
}
}
@@ -261,14 +261,14 @@ class SettingsActivity : BaseActivity() {
private fun updateMode(mode: String?) {
val vpn = mode == VPN
perAppProxy?.isEnabled = vpn
perAppProxy?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false)
perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
localDns?.isEnabled = vpn
fakeDns?.isEnabled = vpn
localDnsPort?.isEnabled = vpn
vpnDns?.isEnabled = vpn
if (vpn) {
updateLocalDns(
settingsStorage.getBoolean(
MmkvManager.decodeSettingsBool(
AppConfig.PREF_LOCAL_DNS_ENABLED,
false
)
@@ -310,8 +310,8 @@ class SettingsActivity : BaseActivity() {
muxXudpConcurrency?.isEnabled = enabled
muxXudpQuic?.isEnabled = enabled
if (enabled) {
updateMuxConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
updateMuxXudpConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
updateMuxConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
updateMuxXudpConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
}
}
@@ -336,9 +336,9 @@ class SettingsActivity : BaseActivity() {
fragmentLength?.isEnabled = enabled
fragmentInterval?.isEnabled = enabled
if (enabled) {
updateFragmentPackets(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
updateFragmentLength(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
}
}

View File

@@ -10,7 +10,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

View File

@@ -13,9 +13,9 @@ import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

View File

@@ -12,11 +12,11 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter {
@@ -45,7 +45,7 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
}
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if( !it.isPressed) return@setOnCheckedChangeListener
if (!it.isPressed) return@setOnCheckedChangeListener
subItem.enabled = isChecked
MmkvManager.encodeSubscription(subId, subItem)

View File

@@ -11,7 +11,7 @@ import android.widget.ListView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityTaskerBinding
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.handler.MmkvManager
class TaskerActivity : BaseActivity() {
private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) }

View File

@@ -7,7 +7,7 @@ import android.util.Log
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.handler.AngConfigManager
import java.net.URLDecoder
class UrlSchemeActivity : BaseActivity() {

View File

@@ -29,12 +29,12 @@ import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
@@ -75,6 +75,7 @@ class UserAssetActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.add_file -> showFileChooser().let { true }
R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
R.id.add_qrcode -> importAssetFromQRcode().let { true }
R.id.download_file -> downloadGeoFiles().let { true }
else -> super.onOptionsItemSelected(item)
}
@@ -156,6 +157,40 @@ class UserAssetActivity : BaseActivity() {
null
}
private fun importAssetFromQRcode(): Boolean {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
}
return true
}
private val scanQRCodeForAssetURL = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importAsset(it.data?.getStringExtra("SCAN_RESULT"))
}
}
private fun importAsset(url: String?): Boolean {
try {
if (!Utils.isValidUrl(url)) {
toast(R.string.toast_invalid_url)
return false
}
// Send URL to UserAssetUrlActivity for Processing
startActivity(Intent(this, UserAssetUrlActivity::class.java)
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url))
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun downloadGeoFiles() {
val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
@@ -240,6 +275,15 @@ class UserAssetActivity : BaseActivity() {
return list + assets
}
fun initAssets() {
lifecycleScope.launch(Dispatchers.Default) {
SettingsManager.initAssets(this@UserAssetActivity, assets)
withContext(Dispatchers.Main) {
binding.recyclerView.adapter?.notifyDataSetChanged()
}
}
}
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
return UserAssetViewHolder(
@@ -271,10 +315,10 @@ class UserAssetActivity : BaseActivity() {
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
holder.itemUserAssetBinding.layoutRemove.visibility = GONE
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
} else {
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
//holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
}
holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
@@ -283,9 +327,16 @@ class UserAssetActivity : BaseActivity() {
startActivity(intent)
}
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
file?.delete()
MmkvManager.removeAssetUrl(item.first)
binding.recyclerView.adapter?.notifyItemRemoved(position)
initAssets()
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
}
}

View File

@@ -9,11 +9,16 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.io.File
class UserAssetUrlActivity : BaseActivity() {
// Receive QRcode URL from UserAssetActivity
companion object {
const val ASSET_URL_QRCODE = "ASSET_URL_QRCODE"
}
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null
@@ -28,10 +33,15 @@ class UserAssetUrlActivity : BaseActivity() {
title = getString(R.string.title_user_asset_add_url)
val assetItem = MmkvManager.decodeAsset(editAssetId)
if (assetItem != null) {
bindingAsset(assetItem)
} else {
clearAsset()
val assetUrlQrcode = intent.getStringExtra(ASSET_URL_QRCODE)
val assetNameQrcode = File(assetUrlQrcode.toString()).name
when {
assetItem != null -> bindingAsset(assetItem)
assetUrlQrcode != null -> {
binding.etRemarks.setText(assetNameQrcode)
binding.etUrl.setText(assetUrlQrcode)
}
else -> clearAsset()
}
}

View File

@@ -13,13 +13,11 @@ object AppManagerUtil {
val apps = ArrayList<AppInfo>()
for (pkg in packages) {
//if (!pkg.hasInternetPermission && pkg.packageName != "android") continue
val applicationInfo = pkg.applicationInfo
val applicationInfo = pkg.applicationInfo ?: continue
val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager)
val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo)

View File

@@ -2,6 +2,8 @@ package com.v2ray.ang.util
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
@@ -15,11 +17,13 @@ object JsonUtil {
return gson.toJson(src)
}
fun <T> fromJson(json: String, cls: Class<T>): T {
return gson.fromJson(json, cls)
fun <T> fromJson(src: String, cls: Class<T>): T {
return gson.fromJson(src, cls)
}
fun toJsonPretty(src: Any?): String {
fun toJsonPretty(src: Any?): String? {
if (src == null)
return null
val gsonPre = GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
@@ -34,4 +38,15 @@ object JsonUtil {
.create()
return gsonPre.toJson(src)
}
fun parseString(src: String?): JsonObject? {
if (src == null)
return null
try {
return JsonParser.parseString(src).getAsJsonObject()
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}

View File

@@ -5,30 +5,30 @@ 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.dto.ProfileItem
import com.v2ray.ang.fmt.Hysteria2Fmt
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
private val procService: ProcessService by lazy {
ProcessService()
}
// fun initPlugin(name: String): PluginManager.InitResult {
// return PluginManager.init(name)!!
// }
fun runPlugin(context: Context, config: ServerConfig?, domainPort: String?) {
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
Log.d(TAG, "runPlugin")
val outbound = config?.getProxyOutbound() ?: return
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) {
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
val configFile = genConfigHy2(context, config, domainPort) ?: return
val cmd = genCmdHy2(context, configFile)
procService = ProcessService()
procService.runProcess(context, cmd)
}
}
@@ -37,12 +37,11 @@ object PluginUtil {
stopHy2()
}
fun realPingHy2(context: Context, config: ServerConfig?): Long {
fun realPingHy2(context: Context, config: ProfileItem?): Long {
Log.d(TAG, "realPingHy2")
val retFailure = -1L
val outbound = config?.getProxyOutbound() ?: return retFailure
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) {
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
val socksPort = Utils.findFreePort(listOf(0))
val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure
val cmd = genCmdHy2(context, configFile)
@@ -58,7 +57,7 @@ object PluginUtil {
return retFailure
}
private fun genConfigHy2(context: Context, config: ServerConfig, domainPort: String?): File? {
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
Log.d(TAG, "runPlugin $HYSTERIA2")
val socksPort = domainPort?.split(":")?.last()

View File

@@ -17,6 +17,7 @@ import android.util.Log
import android.util.Patterns
import android.webkit.URLUtil
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
@@ -24,8 +25,8 @@ import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.Language
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import java.io.IOException
import java.net.*
import java.util.*
@@ -130,7 +131,8 @@ object Utils {
* get remote dns servers from preference
*/
fun getRemoteDnsServers(): List<String> {
val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
val remoteDns =
MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) {
return listOf(AppConfig.DNS_PROXY)
@@ -139,7 +141,7 @@ object Utils {
}
fun getVpnDnsServers(): List<String> {
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
return vpnDns.split(",").filter { isPureIpAddress(it) }
// allow empty, in that case dns will use system default
}
@@ -148,7 +150,8 @@ object Utils {
* get remote dns servers from preference
*/
fun getDomesticDnsServers(): List<String> {
val domesticDns = settingsStorage?.decodeString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
val domesticDns =
MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) {
return listOf(AppConfig.DNS_DIRECT)
@@ -159,8 +162,11 @@ object Utils {
/**
* is ip address
*/
fun isIpAddress(value: String): Boolean {
fun isIpAddress(value: String?): Boolean {
try {
if (value.isNullOrEmpty()) {
return false
}
var addr = value
if (addr.isEmpty() || addr.isBlank()) {
return false
@@ -289,7 +295,7 @@ object Utils {
fun urlEncode(url: String): String {
return try {
URLEncoder.encode(url, Charsets.UTF_8.toString())
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
} catch (e: Exception) {
e.printStackTrace()
url
@@ -353,7 +359,11 @@ object Utils {
}
@Throws(IOException::class)
fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String {
fun getUrlContentWithCustomUserAgent(
urlStr: String?,
timeout: Int = 30000,
httpPort: Int = 0
): String {
val url = URL(urlStr)
val conn = if (httpPort == 0) {
url.openConnection()
@@ -386,8 +396,8 @@ object Utils {
}
fun setNightMode(context: Context) {
when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
fun setNightMode() {
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
@@ -406,7 +416,8 @@ object Utils {
}
fun getLocale(): Locale {
val langCode = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
val langCode =
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
val language = Language.fromCode(langCode)
return when (language) {
@@ -418,12 +429,11 @@ object Utils {
Language.RUSSIAN -> Locale("ru")
Language.PERSIAN -> Locale("fa")
Language.BANGLA -> Locale("bn")
Language.BAKHTIARI -> Locale("bqi", "IR")
}
}
private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LocaleList.getDefault()[0]
} else {
@@ -453,7 +463,8 @@ object Utils {
return if (second) {
AppConfig.DelayTestUrl2
} else {
settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
?: AppConfig.DelayTestUrl
}
}
@@ -481,5 +492,11 @@ object Utils {
return false
}
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.RECEIVER_EXPORTED
} else {
ContextCompat.RECEIVER_NOT_EXPORTED
}
}

View File

@@ -8,6 +8,7 @@ import android.content.IntentFilter
import android.content.res.AssetManager
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
@@ -15,17 +16,15 @@ import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope
@@ -33,15 +32,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.Collections
class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = MmkvManager.decodeServerList()
var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
//var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
//var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
var keywordFilter = ""
val serversCache = mutableListOf<ServersCache>()
val isRunning by lazy { MutableLiveData<Boolean>() }
@@ -49,20 +46,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val updateTestResultAction by lazy { MutableLiveData<String>() }
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
/**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
*/
fun startListenBroadcast() {
isRunning.value = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getApplication<AngApplication>().registerReceiver(
mMsgReceiver,
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY),
Context.RECEIVER_EXPORTED
)
} else {
getApplication<AngApplication>().registerReceiver(
mMsgReceiver,
IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
)
}
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
ContextCompat.registerReceiver(getApplication(), mMsgReceiver, mFilter, Utils.receiverFlags())
MessageUtil.sendMsg2Service(getApplication(), AppConfig.MSG_REGISTER_CLIENT, "")
}
@@ -95,21 +87,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
&& server.contains("routing")
) {
try {
val config = ServerConfig.create(EConfigType.CUSTOM)
val config = CustomFmt.parse(server) ?: return false
config.subscriptionId = subscriptionId
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
serverList.add(0, key)
val profile = ProfileItem(
configType = config.configType,
subscriptionId = config.subscriptionId,
remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
serversCache.add(0, ServersCache(key, profile))
// val profile = ProfileLiteItem(
// configType = config.configType,
// subscriptionId = config.subscriptionId,
// remarks = config.remarks,
// server = config.getProxyOutbound()?.getServerAddress(),
// serverPort = config.getProxyOutbound()?.getServerPort(),
// )
serversCache.add(0, ServersCache(key, config))
return true
} catch (e: Exception) {
e.printStackTrace()
@@ -119,7 +109,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
fun swapServer(fromPosition: Int, toPosition: Int) {
if (subscriptionId.isEmpty()) {
Collections.swap(serverList, fromPosition, toPosition)
} else {
val fromPosition2 = serverList.indexOf(serversCache[fromPosition].guid)
val toPosition2 = serverList.indexOf(serversCache[toPosition].guid)
Collections.swap(serverList, fromPosition2, toPosition2)
}
Collections.swap(serversCache, fromPosition, toPosition)
MmkvManager.encodeServerList(serverList)
}
@@ -128,18 +124,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun updateCache() {
serversCache.clear()
for (guid in serverList) {
var profile = MmkvManager.decodeProfileConfig(guid)
if (profile == null) {
val config = MmkvManager.decodeServerConfig(guid) ?: continue
profile = ProfileItem(
configType = config.configType,
subscriptionId = config.subscriptionId,
remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
MmkvManager.encodeServerConfig(guid, config)
}
var profile = MmkvManager.decodeServerConfig(guid) ?: continue
// var profile = MmkvManager.decodeProfileConfig(guid)
// if (profile == null) {
// val config = MmkvManager.decodeServerConfig(guid) ?: continue
// profile = ProfileLiteItem(
// configType = config.configType,
// subscriptionId = config.subscriptionId,
// remarks = config.remarks,
// server = config.getProxyOutbound()?.getServerAddress(),
// serverPort = config.getProxyOutbound()?.getServerPort(),
// )
// MmkvManager.encodeServerConfig(guid, config)
// }
if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
continue
@@ -189,7 +186,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val serverPort = outbound.serverPort
if (serverAddress != null && serverPort != null) {
tcpingTestScope.launch {
val testResult = SpeedtestUtil.tcping(serverAddress, serverPort)
val testResult = SpeedtestUtil.tcping(serverAddress, serverPort.toInt())
launch(Dispatchers.Main) {
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
updateListAction.value = getPosition(item.guid)
@@ -220,7 +217,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun subscriptionIdChanged(id: String) {
if (subscriptionId != id) {
subscriptionId = id
MmkvManager.settingsStorage.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
MmkvManager.encodeSettings(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
reloadServerList()
}
}
@@ -252,7 +249,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
fun removeDuplicateServer(): Int {
val serversCacheCopy = mutableListOf<Pair<String, ServerConfig>>()
val serversCacheCopy = mutableListOf<Pair<String, ProfileItem>>()
for (it in serversCache) {
val config = MmkvManager.decodeServerConfig(it.guid) ?: continue
serversCacheCopy.add(Pair(it.guid, config))
@@ -260,11 +257,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val deleteServer = mutableListOf<String>()
serversCacheCopy.forEachIndexed { index, it ->
val outbound = it.second.getProxyOutbound()
val outbound = it.second
serversCacheCopy.forEachIndexed { index2, it2 ->
if (index2 > index) {
val outbound2 = it2.second.getProxyOutbound()
if (outbound == outbound2 && !deleteServer.contains(it2.first)) {
val outbound2 = it2.second
if (outbound.equals(outbound2) && !deleteServer.contains(it2.first)) {
deleteServer.add(it2.first)
}
}
@@ -318,30 +315,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
MmkvManager.encodeServerList(serverList)
}
fun copyAssets(assets: AssetManager) {
val extFolder = Utils.userAssetPath(getApplication<AngApplication>())
fun initAssets(assets: AssetManager) {
viewModelScope.launch(Dispatchers.Default) {
try {
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
SettingsManager.initAssets(getApplication<AngApplication>(), assets)
}
}
@@ -350,7 +326,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return
}
keywordFilter = keyword
MmkvManager.settingsStorage.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
reloadServerList()
}

Some files were not shown because too many files have changed in this diff Show More