Compare commits

...

77 Commits

Author SHA1 Message Date
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
2dust
e60eb703c6 up 1.9.9 2024-10-28 13:54:49 +08:00
2dust
cdede639f2 Adjustment of preset rule sets 2024-10-28 13:45:00 +08:00
2dust
23264d71f0 Test connection after updating subscription 2024-10-28 12:05:17 +08:00
2dust
9caafc4303 Add toast insecure protocol 2024-10-27 20:44:57 +08:00
Tamim Hossain
48a3690a39 refactor steam visiblity (#3786)
refactor steam visiblity
2024-10-25 20:07:43 +08:00
Tamim Hossain
b66a8ca44d Refactor QRCodeDecoder for readability and performance (#3782)
- Improved the `createQRCode` function by replacing manual loops with Kotlin idioms and using `runCatching` for safer error handling.
- Refactored `syncDecodeQRCode` to simplify control flow and avoid redundant null checks.
- Enhanced error handling and logging by using `runCatching` and more concise exception handling.
2024-10-25 19:19:22 +08:00
Tamim Hossain
82087e1187 Refactor UserAssetActivity for readability and efficiency (#3781)
- Refactored the onOptionsItemSelected function to use `when` and `let` for cleaner conditional logic.
- Simplified the `chooseFile` function with `runCatching` for better error handling.
- Extracted repeated code into reusable functions to improve maintainability.
- Improved coroutine handling by ensuring file I/O happens on the IO thread.
2024-10-25 19:17:35 +08:00
Tamim Hossain
fdfd0438c3 Refactor updateMuxConcurrency for Null Safety and Code Simplification (#3780)
Refactored the updateMuxConcurrency function by removing redundant null checks and improving code readability. The concurrency value now defaults to 8 if null or invalid input is provided.
2024-10-25 19:14:28 +08:00
Tamim Hossain
28027b5288 Fix Comment and Improve Null Safety in ServerCustomConfigActivity (#3779)
Fixed the comment typo in ServerCustomConfigActivity and improved null safety in the bindingServer function by ensuring proper fallback for raw content. Simplified logic for setting editor content.
2024-10-25 19:13:34 +08:00
Tamim Hossain
ba54005753 Improve Permission Handling and QR Code Import Logic in ScScannerActivity (#3777)
Refactored permission request handling in ScScannerActivity to improve readability and simplify the flow. Improved QR code import logic to ensure better handling of the scanned result and enhance user feedback.
2024-10-25 19:10:14 +08:00
Tamim Hossain
c6758b11b5 Improve Permission Request and QR Code Decoding in ScannerActivity (#3776)
Streamlined the permission request process using RxPermissions and improved error handling for file selection and QR code decoding in ScannerActivity. Enhanced user feedback for decoding failures and file processing errors.
2024-10-25 19:09:41 +08:00
Tamim Hossain
6c29e5e9a4 Optimize refreshData in RoutingSettingActivity (#3775)
Optimized the refreshData method by clearing the existing rulesets list and adding the new items to avoid unnecessary reallocation and improve memory management.
2024-10-25 19:07:55 +08:00
Tamim Hossain
17e0db2ffc Refactor Import Rulesets from Clipboard (#3774)
Refactored the import_rulesets_from_clipboard method in RoutingSettingActivity. Improved error handling by isolating the clipboard fetching inside a try-catch block and ensuring the routing ruleset reset logic is handled more cleanly.
2024-10-25 19:06:44 +08:00
Tamim Hossain
490ea59499 Refactor saveServer Method to Improve Null-Safety and Readability (#3773)
Refactored the saveServer method in RoutingEditActivity to improve null-safety and readability by using apply and takeIf. This ensures cleaner and more concise code while processing user inputs like domain, ip, protocol, and network.
2024-10-25 19:02:52 +08:00
Tamim Hossain
f4db6bcf63 Refactor Binding Logic in AppViewHolder of PerAppProxyAdapter (#3772)
Simplified the binding logic in AppViewHolder by improving readability and removing redundant code. Consolidated logic for setting the app name and handling system apps. This refactor improves the maintainability of the PerAppProxyAdapter class.
2024-10-25 19:01:56 +08:00
Tamim Hossain
90f89de957 Improve Error Handling in Batch Config Import and Progress Bar Management (#3771)
Enhanced error handling in the importBatchConfig method by adding a try-catch block. Ensured that the progress bar is hidden in both success and failure cases. Replaced nested if statements with a cleaner when clause for better readability.
2024-10-25 18:58:20 +08:00
Tamim Hossain
9612b868f2 Improve Error Handling in LogcatActivity (#3770)
Refined the error handling in the logcat function of LogcatActivity by ensuring that the progress bar is hidden in both success and failure cases. Added user-friendly error messages using toast and cleaned up the code by using Kotlin's linkedSetOf for the logcat command list.
2024-10-25 18:56:59 +08:00
Tamim Hossain
5f8ea93f36 Refactor attachBaseContext for Null Safety and Clean Code (#3769)
Simplified the attachBaseContext function by removing redundant let block and ensuring null safety with concise null handling. This refactor improves readability and ensures that null cases are handled directly.
2024-10-25 18:55:56 +08:00
Tamim Hossain
fc132f7282 Improve Exception Handling in File Chooser and Restore Process (#3768)
Enhanced exception handling in the file chooser and restore configuration process by adding detailed logging with Log.e(). Simplified the intent creation in showFileChooser and combined nested try-catch blocks for better readability and error management.
2024-10-25 18:36:17 +08:00
Tamim Hossain
6f9bb6caa7 Improve RxJava Disposable Handling in V2RayServiceManager (#3767)
Refactored the RxJava disposable handling in the V2RayServiceManager to ensure proper disposal when the service stops. This change prevents potential memory leaks by disposing of the disposable only when it's initialized, using a more concise and reliable approach with Kotlin's let function.
2024-10-25 18:35:03 +08:00
Tamim Hossain
0e5b88de8f remove parentheses (#3766)
remove parentheses
2024-10-25 18:34:19 +08:00
any116
5b4f51981e Fallback go ver for normal use (#3758)
* Update build.yml

* Delete .github/dependabot.yml
2024-10-24 09:14:38 +08:00
DecorativeFamily
3dcee45e9f Update strings.xml (#3741)
* Update strings.xml

update persian strings

* Update strings.xml

update persian strings

* Update strings.xml

update persian strings

* Update strings.xml

update persian strings
2024-10-23 09:14:27 +08:00
Tamim Hossain
3573a3bec3 Remove unnecessary null check from subscriptionId (#3739)
- Replaced `subscriptionId.isNullOrEmpty()` with `subscriptionId.isEmpty()` since `subscriptionId` is not nullable.
- This refactor simplifies the logic and improves code readability by eliminating the redundant null check.
2024-10-22 20:22:58 +08:00
Tamim Hossain
796bad1c1c Remove redundant qualifier from RulesBean initialization (#3738)
- Removed the redundant qualifier `V2rayConfig.RoutingBean.RulesBean` in favor of directly using `RulesBean`.
- This simplifies the code and improves readability by removing unnecessary fully qualified names.
- The change ensures cleaner and more maintainable code without altering functionality.
2024-10-22 20:22:46 +08:00
Tamim Hossain
77042f6fae Remove unnecessary null check from keywordFilter (#3737)
* Remove unnecessary null check from keywordFilter

- Replaced `keywordFilter.isNullOrEmpty()` with `keywordFilter.isEmpty()` since `keywordFilter` is guaranteed to never be null.
- Simplified the logic by removing the redundant null check, improving code readability.

* Remove unnecessary null check from keywordFilter

- Replaced `keywordFilter.isNullOrEmpty()` with `keywordFilter.isEmpty()` since `keywordFilter` is guaranteed to never be null.
- Simplified the logic by removing the redundant null check, improving code readability.
2024-10-22 20:22:23 +08:00
Tamim Hossain
013ac308f7 Refactor allowInsecure variable declaration (#3736)
- Changed `allowInsecure` from `var` to `val` to ensure immutability.
- Replaced the nullable safe call `settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false` with `settingsStorage.decodeBool(AppConfig.PREF_ALLOW_INSECURE, false)` for more concise and readable code.
- This ensures `allowInsecure` is always initialized with a default value of `false` in a cleaner and more efficient way.
2024-10-22 20:21:57 +08:00
any116
d703582f19 refine upload-artifact and fix version (#3735)
* refine upload-artifact and fix api version

* refine upload-artifact and fix api version

* refine upload-artifact and fix version

* use Dependabot keep actions updated to latest

like: v2-->v3
2024-10-22 20:21:16 +08:00
any116
1db80f740d Update SettingsManager.kt (#3732) 2024-10-22 09:18:13 +08:00
DecorativeFamily
a0d2740280 Update build.yml (#3731) 2024-10-22 09:17:59 +08:00
DecorativeFamily
297083f3c4 Update build.yml (#3730) 2024-10-22 09:17:39 +08:00
DecorativeFamily
15a4ad978a Update libs.versions.toml (#3728)
Co-authored-by: DECORATIVEFAMILYNG <185765765+DECORATIVEFAMILYNG@users.noreply.github.com>
2024-10-22 09:17:15 +08:00
DECORATIVEFAMILYNG
63f4cfac83 Update strings.xml (#3726)
update persian strings
2024-10-21 17:39:52 +08:00
Tamim Hossain
ca849fb19e Organize routing files names (#3717)
* Organize routing files names

Organize routing files names

* Use enum for routing type

Use enum for routing type
2024-10-21 17:39:28 +08:00
Tamim Hossain
3de3070ab7 Organize locale (#3716)
* Organize locale

Organize locale

* use enum for locale

use enum for locale
2024-10-21 17:38:41 +08:00
Tamim Hossain
cbea4bab7c Update kotlin version to 2.0.21 (#3724)
Update kotlin version to 2.0.21
2024-10-21 16:32:19 +08:00
Tamim Hossain
fe8b825c34 Fix subs id check (#3714)
* Fix subs id check

Fix subs id check

* Update MainViewModel.kt
2024-10-21 14:43:48 +08:00
2dust
daa0394960 Fix
https://github.com/2dust/v2rayNG/issues/3720
2024-10-21 14:42:00 +08:00
2dust
e5aba5d99b Adding checks for subscription url 2024-10-21 14:25:05 +08:00
any116
c4847eb3de Update SimpleItemTouchHelperCallback.java (#3719) 2024-10-21 14:04:24 +08:00
Tamim Hossain
5a5f911453 Use entries instead of values as its recommended after kotlin 1.9 (#3718)
Use entries instead of values as its  recommended after kotlin 1.9
2024-10-21 14:01:44 +08:00
Tamim Hossain
22cef29c27 organize dns (#3715)
organize dns
2024-10-21 13:55:45 +08:00
Tamim Hossain
ef41641680 Update bangla translation (#3713)
Update bangla translation
2024-10-21 13:52:11 +08:00
Tamim Hossain
69135e8707 Update plugins in ``libs.versions.toml`` (#3712)
Update plugins in ```libs.versions.toml```
2024-10-21 13:51:59 +08:00
2dust
5daef71147 up 1.9.8 2024-10-17 10:59:50 +08:00
Helium-Studio
5ffc5ec502 Clean up custom routing white (#3699)
Some IPs or domains are already included in geoip / geosite
2024-10-17 10:23:27 +08:00
2dust
35063db3e6 Improvement share uri 2024-10-17 10:21:13 +08:00
MMR
868c24bb8b Add Iran whitelist routing option (#3696)
* Add Iran whitelist routing option

* Update SettingsManager.kt

* Add files via upload

* Update custom_routing_white_iran

* Update strings.xml

* Update strings.xml

* Update strings.xml
2024-10-15 21:04:04 +08:00
99 changed files with 2721 additions and 2084 deletions

View File

@@ -21,23 +21,28 @@ jobs:
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '21'
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.22.2' go-version: '1.23.2'
cache: false
- name: Patch Go use 600296
#https://go-review.googlesource.com/c/go/+/600296
run: |
cd "$(go env GOROOT)"
curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1
- name: Install gomobile - name: Install gomobile
run: | run: |
go install golang.org/x/mobile/cmd/gomobile@latest go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240806205939-81131f6468ab
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Setup Android environment - name: Setup Android environment
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
- name: Build dependencies - name: Build dependencies
run: | run: |
mkdir ${{ github.workspace }}/build mkdir ${{ github.workspace }}/build
@@ -56,8 +61,33 @@ jobs:
chmod 755 gradlew chmod 755 gradlew
./gradlew assembleDebug ./gradlew assembleDebug
- name: Upload APK - name: Upload arm64-v8a APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: arm64-v8a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
- name: Upload armeabi-v7a APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: armeabi-v7a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
- name: Upload x86 APK
uses: actions/upload-artifact@v4
if: ${{ success() }}
with:
name: x86-apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk
- name: Upload Other APKs
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: apk name: others-apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/ path: |
${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk

View File

@@ -3,7 +3,7 @@
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core) A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
[![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop) [![API](https://img.shields.io/badge/API-21%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-1.9.23-blue.svg)](https://kotlinlang.org) [![Kotlin Version](https://img.shields.io/badge/Kotlin-2.0.21-blue.svg)](https://kotlinlang.org)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master)
[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng) [![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng)
[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases) [![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases)

View File

@@ -10,9 +10,9 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 35
versionCode = 601 versionCode = 605
versionName = "1.9.7" versionName = "1.9.11"
multiDexEnabled = true multiDexEnabled = true
splits { splits {
abi { abi {
@@ -91,6 +91,8 @@ android {
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar")))) implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.org.mockito.mockito.inline)
testImplementation(libs.mockito.kotlin)
implementation(libs.flexbox) implementation(libs.flexbox)
// Androidx // Androidx

View File

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

View File

@@ -42,15 +42,7 @@
] ]
}, },
{ {
"remarks": "代理GFW", "remarks": "代理IP",
"outboundTag": "proxy",
"domain": [
"geosite:gfw",
"geosite:greatfire"
]
},
{
"remarks": "代理Google等",
"outboundTag": "proxy", "outboundTag": "proxy",
"ip": [ "ip": [
"1.0.0.1", "1.0.0.1",
@@ -65,6 +57,14 @@
"geoip:twitter" "geoip:twitter"
] ]
}, },
{
"remarks": "代理GFW",
"outboundTag": "proxy",
"domain": [
"geosite:gfw",
"geosite:greatfire"
]
},
{ {
"remarks": "最终直连", "remarks": "最终直连",
"port": "0-65535", "port": "0-65535",

View File

@@ -34,20 +34,6 @@
"geosite:private" "geosite:private"
] ]
}, },
{
"remarks": "绕过中国域名",
"outboundTag": "direct",
"domain": [
"domain:dns.alidns.com",
"domain:dns.pub",
"domain:doh.pub",
"domain:dot.pub",
"domain:doh.360.cn",
"domain:dot.360.cn",
"geosite:cn",
"geosite:geolocation-cn"
]
},
{ {
"remarks": "绕过中国IP", "remarks": "绕过中国IP",
"outboundTag": "direct", "outboundTag": "direct",
@@ -74,9 +60,22 @@
"geoip:cn" "geoip:cn"
] ]
}, },
{
"remarks": "绕过中国域名",
"outboundTag": "direct",
"domain": [
"domain:dns.alidns.com",
"domain:doh.pub",
"domain:dot.pub",
"domain:doh.360.cn",
"domain:dot.360.cn",
"geosite:cn",
"geosite:geolocation-cn"
]
},
{ {
"remarks": "最终代理", "remarks": "最终代理",
"port": "0-65535", "port": "0-65535",
"outboundTag": "proxy" "outboundTag": "proxy"
} }
] ]

View File

@@ -0,0 +1,49 @@
[
{
"remarks": "Block udp443",
"outboundTag": "block",
"port": "443",
"network": "udp"
},
{
"remarks": "Block ads and trackers",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "Direct LAN IP",
"outboundTag": "direct",
"ip": [
"geoip:private"
]
},
{
"remarks": "Direct LAN domains",
"outboundTag": "direct",
"domain": [
"geosite:private"
]
},
{
"remarks": "Bypass Iran domains",
"outboundTag": "direct",
"domain": [
"domain:ir",
"geosite:category-ir"
]
},
{
"remarks": "Bypass Iran IP",
"outboundTag": "direct",
"ip": [
"geoip:ir"
]
},
{
"remarks": "Final Agent",
"port": "0-65535",
"outboundTag": "proxy"
}
]

View File

@@ -16,14 +16,15 @@
package com.v2ray.ang.helper; package com.v2ray.ang.helper;
import android.animation.ValueAnimator;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.view.animation.DecelerateInterpolator;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jetbrains.annotations.NotNull;
/** /**
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and * An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/> * swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
@@ -36,9 +37,12 @@ import org.jetbrains.annotations.NotNull;
*/ */
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
public static final float ALPHA_FULL = 1.0f; private static final float ALPHA_FULL = 1.0f;
private static final float SWIPE_THRESHOLD = 0.25f;
private static final long ANIMATION_DURATION = 200;
private final ItemTouchHelperAdapter mAdapter; private final ItemTouchHelperAdapter mAdapter;
private ValueAnimator mReturnAnimator;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) { public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
mAdapter = adapter; mAdapter = adapter;
@@ -51,15 +55,14 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override @Override
public boolean isItemViewSwipeEnabled() { public boolean isItemViewSwipeEnabled() {
return false; return true;
} }
@Override @Override
public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) { public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// Set movement flags based on the layout manager
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) { if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
final int swipeFlags = 0; final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags); return makeMovementFlags(dragFlags, swipeFlags);
} else { } else {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
@@ -69,61 +72,89 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
} }
@Override @Override
public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) { if (source.getItemViewType() != target.getItemViewType()) {
return false; return false;
} }
// Notify the adapter of the move
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition()); mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
return true; return true;
} }
@Override @Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
// Notify the adapter of the dismissal // 不执行删除操作,仅返回项目到原位
mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition()); returnViewToOriginalPosition(viewHolder);
} }
@Override @Override
public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX, public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
float dY, int actionState, boolean isCurrentlyActive) { @NonNull RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
// Fade out the view as it is swiped out of the parent's bounds float maxSwipeDistance = viewHolder.itemView.getWidth() * SWIPE_THRESHOLD;
final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); float swipeAmount = Math.abs(dX);
float direction = Math.signum(dX);
// 限制最大滑动距离
float translationX = Math.min(swipeAmount, maxSwipeDistance) * direction;
float alpha = ALPHA_FULL - Math.min(swipeAmount, maxSwipeDistance) / maxSwipeDistance;
viewHolder.itemView.setTranslationX(translationX);
viewHolder.itemView.setAlpha(alpha); viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX);
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
returnViewToOriginalPosition(viewHolder);
}
} else { } else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
} }
} }
private void returnViewToOriginalPosition(RecyclerView.ViewHolder viewHolder) {
if (mReturnAnimator != null && mReturnAnimator.isRunning()) {
mReturnAnimator.cancel();
}
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.getTranslationX(), 0f);
mReturnAnimator.addUpdateListener(animation -> {
float value = (float) animation.getAnimatedValue();
viewHolder.itemView.setTranslationX(value);
viewHolder.itemView.setAlpha(1f - Math.abs(value) / (viewHolder.itemView.getWidth() * SWIPE_THRESHOLD));
});
mReturnAnimator.setInterpolator(new DecelerateInterpolator());
mReturnAnimator.setDuration(ANIMATION_DURATION);
mReturnAnimator.start();
}
@Override @Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
// We only want the active item to change
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof ItemTouchHelperViewHolder) { if (viewHolder instanceof ItemTouchHelperViewHolder) {
// Let the view holder know that this item is being moved or dragged
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected(); itemViewHolder.onItemSelected();
} }
} }
super.onSelectedChanged(viewHolder, actionState); super.onSelectedChanged(viewHolder, actionState);
} }
@Override @Override
public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) { public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder); super.clearView(recyclerView, viewHolder);
mAdapter.onItemMoveCompleted();
viewHolder.itemView.setAlpha(ALPHA_FULL); viewHolder.itemView.setAlpha(ALPHA_FULL);
if (viewHolder instanceof ItemTouchHelperViewHolder) { if (viewHolder instanceof ItemTouchHelperViewHolder) {
// Tell the view holder it's time to restore the idle state
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder; ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear(); itemViewHolder.onItemClear();
} }
mAdapter.onItemMoveCompleted();
}
@Override
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return 1.1f; // 设置一个大于1的值确保不会触发默认的滑动删除操作
}
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
return defaultValue * 10; // 增加滑动逃逸速度,使得更难触发滑动
} }
} }

View File

@@ -7,7 +7,7 @@ import androidx.multidex.MultiDexApplication
import androidx.work.Configuration import androidx.work.Configuration
import androidx.work.WorkManager import androidx.work.WorkManager
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.util.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
class AngApplication : MultiDexApplication() { class AngApplication : MultiDexApplication() {

View File

@@ -60,8 +60,6 @@ object AppConfig {
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter" const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */ /** Protocol identifiers. */
const val PROTOCOL_HTTP: String = "http://"
const val PROTOCOL_HTTPS: String = "https://"
const val PROTOCOL_FREEDOM: String = "freedom" const val PROTOCOL_FREEDOM: String = "freedom"
/** Broadcast actions. */ /** Broadcast actions. */
@@ -158,4 +156,27 @@ object AppConfig {
/** Give a good name to this, IDK*/ /** Give a good name to this, IDK*/
const val VPN = "VPN" const val VPN = "VPN"
// Google API rule constants
const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
// Android Private DNS constants
const val DNS_PUB_DOMAIN = "dns.pub"
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
const val DNS_ONE_ONE_DOMAIN = "one.one.one.one"
const val DNS_GOOGLE_DOMAIN = "dns.google"
val DNS_PUB_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_ONE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8
const val DEFAULT_NETWORK = "tcp"
const val TLS = "tls"
const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http"
} }

View File

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

View File

@@ -16,6 +16,6 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
HTTP(10, AppConfig.HTTP); HTTP(10, AppConfig.HTTP);
companion object { companion object {
fun fromInt(value: Int) = values().firstOrNull { it.value == value } fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
} }
} }

View File

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

View File

@@ -0,0 +1,18 @@
package com.v2ray.ang.dto
enum class Language(val code: String) {
AUTO("auto"),
ENGLISH("en"),
CHINA("zh-rCN"),
TRADITIONAL_CHINESE("zh-rTW"),
VIETNAMESE("vi"),
RUSSIAN("ru"),
PERSIAN("fa"),
BANGLA("bn");
companion object {
fun fromCode(code: String): Language {
return entries.find { it.code == code } ?: AUTO
}
}
}

View File

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

View File

@@ -0,0 +1,9 @@
package com.v2ray.ang.dto
data class ProfileLiteItem(
val configType: EConfigType,
var subscriptionId: String = "",
var remarks: String = "",
var server: String?,
var serverPort: Int?,
)

View File

@@ -0,0 +1,20 @@
package com.v2ray.ang.dto
enum class RoutingType(val fileName: String) {
WHITE("custom_routing_white"),
BLACK("custom_routing_black"),
GLOBAL("custom_routing_global"),
WHITE_IRAN("custom_routing_white_iran");
companion object {
fun fromIndex(index: Int): RoutingType {
return when (index) {
0 -> WHITE
1 -> BLACK
2 -> GLOBAL
3 -> WHITE_IRAN
else -> WHITE
}
}
}
}

View File

@@ -7,6 +7,9 @@ import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.*
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.*
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.lang.reflect.Type import java.lang.reflect.Type
@@ -27,16 +30,6 @@ data class V2rayConfig(
var observatory: Any? = null, var observatory: Any? = null,
var burstObservatory: Any? = null var burstObservatory: Any? = null
) { ) {
companion object {
const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8
const val DEFAULT_NETWORK = "tcp"
const val TLS = "tls"
const val REALITY = "reality"
const val HTTP = "http"
}
data class LogBean( data class LogBean(
val access: String, val access: String,
@@ -82,6 +75,49 @@ data class V2rayConfig(
val sendThrough: String? = null, val sendThrough: String? = null,
var mux: MuxBean? = MuxBean(false) var mux: MuxBean? = MuxBean(false)
) { ) {
companion object {
fun create(configType: EConfigType): OutboundBean? {
return when (configType) {
EConfigType.VMESS,
EConfigType.VLESS ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
vnext = listOf(
VnextBean(
users = listOf(UsersBean())
)
)
),
streamSettings = StreamSettingsBean()
)
EConfigType.SHADOWSOCKS,
EConfigType.SOCKS,
EConfigType.HTTP,
EConfigType.TROJAN,
EConfigType.HYSTERIA2 ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
servers = listOf(ServersBean())
),
streamSettings = StreamSettingsBean()
)
EConfigType.WIREGUARD ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
secretKey = "",
peers = listOf(WireGuardBean())
)
)
EConfigType.CUSTOM -> null
}
}
}
data class OutSettingsBean( data class OutSettingsBean(
var vnext: List<VnextBean>? = null, var vnext: List<VnextBean>? = null,
@@ -110,17 +146,17 @@ data class V2rayConfig(
data class VnextBean( data class VnextBean(
var address: String = "", var address: String = "",
var port: Int = DEFAULT_PORT, var port: Int = AppConfig.DEFAULT_PORT,
var users: List<UsersBean> var users: List<UsersBean>
) { ) {
data class UsersBean( data class UsersBean(
var id: String = "", var id: String = "",
var alterId: Int? = null, var alterId: Int? = null,
var security: String = DEFAULT_SECURITY, var security: String? = null,
var level: Int = DEFAULT_LEVEL, var level: Int = AppConfig.DEFAULT_LEVEL,
var encryption: String = "", var encryption: String? = null,
var flow: String = "" var flow: String? = null
) )
} }
@@ -141,8 +177,8 @@ data class V2rayConfig(
var method: String? = null, var method: String? = null,
var ota: Boolean = false, var ota: Boolean = false,
var password: String? = null, var password: String? = null,
var port: Int = DEFAULT_PORT, var port: Int = AppConfig.DEFAULT_PORT,
var level: Int = DEFAULT_LEVEL, var level: Int = AppConfig.DEFAULT_LEVEL,
val email: String? = null, val email: String? = null,
var flow: String? = null, var flow: String? = null,
val ivCheck: Boolean? = null, val ivCheck: Boolean? = null,
@@ -151,7 +187,7 @@ data class V2rayConfig(
data class SocksUsersBean( data class SocksUsersBean(
var user: String = "", var user: String = "",
var pass: String = "", var pass: String = "",
var level: Int = DEFAULT_LEVEL var level: Int = AppConfig.DEFAULT_LEVEL
) )
} }
@@ -164,7 +200,7 @@ data class V2rayConfig(
} }
data class StreamSettingsBean( data class StreamSettingsBean(
var network: String = DEFAULT_NETWORK, var network: String = AppConfig.DEFAULT_NETWORK,
var security: String = "", var security: String = "",
var tcpSettings: TcpSettingsBean? = null, var tcpSettings: TcpSettingsBean? = null,
var kcpSettings: KcpSettingsBean? = null, var kcpSettings: KcpSettingsBean? = null,
@@ -224,7 +260,7 @@ data class V2rayConfig(
} }
data class WsSettingsBean( data class WsSettingsBean(
var path: String = "", var path: String? = null,
var headers: HeadersBean = HeadersBean(), var headers: HeadersBean = HeadersBean(),
val maxEarlyData: Int? = null, val maxEarlyData: Int? = null,
val useBrowserForwarding: Boolean? = null, val useBrowserForwarding: Boolean? = null,
@@ -234,21 +270,21 @@ data class V2rayConfig(
} }
data class HttpupgradeSettingsBean( data class HttpupgradeSettingsBean(
var path: String = "", var path: String? = null,
var host: String = "", var host: String? = null,
val acceptProxyProtocol: Boolean? = null val acceptProxyProtocol: Boolean? = null
) )
data class SplithttpSettingsBean( data class SplithttpSettingsBean(
var path: String = "", var path: String? = null,
var host: String = "", var host: String? = null,
val maxUploadSize: Int? = null, val maxUploadSize: Int? = null,
val maxConcurrentUploads: Int? = null val maxConcurrentUploads: Int? = null
) )
data class HttpSettingsBean( data class HttpSettingsBean(
var host: List<String> = ArrayList(), var host: List<String> = ArrayList(),
var path: String = "" var path: String? = null
) )
data class SockoptBean( data class SockoptBean(
@@ -262,7 +298,7 @@ data class V2rayConfig(
data class TlsSettingsBean( data class TlsSettingsBean(
var allowInsecure: Boolean = false, var allowInsecure: Boolean = false,
var serverName: String = "", var serverName: String? = null,
val alpn: List<String>? = null, val alpn: List<String>? = null,
val minVersion: String? = null, val minVersion: String? = null,
val maxVersion: String? = null, val maxVersion: String? = null,
@@ -311,18 +347,18 @@ data class V2rayConfig(
transport: String, headerType: String?, host: String?, path: String?, seed: String?, transport: String, headerType: String?, host: String?, path: String?, seed: String?,
quicSecurity: String?, key: String?, mode: String?, serviceName: String?, quicSecurity: String?, key: String?, mode: String?, serviceName: String?,
authority: String? authority: String?
): String { ): String? {
var sni = "" var sni: String? = null
network = transport network = transport
when (network) { when (network) {
"tcp" -> { "tcp" -> {
val tcpSetting = TcpSettingsBean() val tcpSetting = TcpSettingsBean()
if (headerType == HTTP) { if (headerType == AppConfig.HEADER_TYPE_HTTP) {
tcpSetting.header.type = HTTP tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
val requestObj = TcpSettingsBean.HeaderBean.RequestBean() val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
requestObj.headers.Host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() } requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = (path.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() } requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
tcpSetting.header.request = requestObj tcpSetting.header.request = requestObj
sni = requestObj.headers.Host?.getOrNull(0) ?: sni sni = requestObj.headers.Host?.getOrNull(0) ?: sni
} }
@@ -371,7 +407,7 @@ data class V2rayConfig(
"h2", "http" -> { "h2", "http" -> {
network = "h2" network = "h2"
val h2Setting = HttpSettingsBean() val h2Setting = HttpSettingsBean()
h2Setting.host = (host.orEmpty()).split(",").map { it.trim() }.filter { it.isNotEmpty() } h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
sni = h2Setting.host.getOrNull(0) ?: sni sni = h2Setting.host.getOrNull(0) ?: sni
h2Setting.path = path ?: "/" h2Setting.path = path ?: "/"
httpSettings = h2Setting httpSettings = h2Setting
@@ -400,7 +436,7 @@ data class V2rayConfig(
} }
fun populateTlsSettings( fun populateTlsSettings(
streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?, streamSecurity: String, allowInsecure: Boolean, sni: String?, fingerprint: String?, alpns: String?,
publicKey: String?, shortId: String?, spiderX: String? publicKey: String?, shortId: String?, spiderX: String?
) { ) {
security = streamSecurity security = streamSecurity
@@ -413,10 +449,10 @@ data class V2rayConfig(
shortId = shortId, shortId = shortId,
spiderX = spiderX spiderX = spiderX
) )
if (security == TLS) { if (security == AppConfig.TLS) {
tlsSettings = tlsSetting tlsSettings = tlsSetting
realitySettings = null realitySettings = null
} else if (security == REALITY) { } else if (security == AppConfig.REALITY) {
tlsSettings = null tlsSettings = null
realitySettings = tlsSetting realitySettings = tlsSetting
} }
@@ -501,7 +537,7 @@ data class V2rayConfig(
} }
} }
fun getTransportSettingDetails(): List<String>? { fun getTransportSettingDetails(): List<String?>? {
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true) || protocol.equals(EConfigType.VLESS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true) || protocol.equals(EConfigType.TROJAN.name, true)
@@ -601,7 +637,8 @@ data class V2rayConfig(
var port: Int? = null, var port: Int? = null,
var domains: List<String>? = null, var domains: List<String>? = null,
var expectIPs: List<String>? = null, var expectIPs: List<String>? = null,
val clientIp: String? = null val clientIp: String? = null,
val skipFallback: Boolean? = null,
) )
} }

View File

@@ -93,4 +93,6 @@ inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = whe
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when { inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java) Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
} }
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,84 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.Utils
import java.net.URI
open class FmtBase {
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
val query = if (dicQuery != null)
("?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
else ""
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(userInfo ?: ""),
Utils.getIpv6Address(config.server),
config.serverPort
)
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
}
fun getQueryParam(uri: URI): Map<String, String> {
return uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
}
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
val dicQuery = HashMap<String, String>()
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() }
config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
dicQuery["type"] = config.network?.ifEmpty { AppConfig.DEFAULT_NETWORK }.orEmpty()
when (config.network) {
"tcp" -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
}
"kcp" -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
}
"ws", "httpupgrade", "splithttp" -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
"http", "h2" -> {
dicQuery["type"] = "http"
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
"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() }
}
"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?.get(0)?.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,65 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.idnHost.isEmpty() || uri.userInfo.isEmpty()) 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(":")
} else {
Utils.decode(uri.userInfo).split(":")
}
if (result.count() == 2) {
config.method = result.first()
config.password = result.last()
}
if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri)
if (queryParam["plugin"] == "obfs-local" && queryParam["obfs"] == "http") {
config.network = "tcp"
config.headerType = "http"
config.host = queryParam["obfs-host"]
config.path = queryParam["path"]
}
}
return config
}
fun toUri(config: ProfileItem): String {
val pw = "${config.method}:${config.password}"
return toUri(config, pw, null)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.method = profileItem.method
}
return outboundBean
}
}

View File

@@ -0,0 +1,61 @@
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
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(":")
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, pw, null)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
outboundBean?.settings?.servers?.get(0)?.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,103 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object TrojanFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
if (uri.rawQuery.isNullOrEmpty()) {
config.security = AppConfig.TLS
config.insecure = allowInsecure
} else {
val queryParam = getQueryParam(uri)
config.network = queryParam["type"] ?: "tcp"
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.security = queryParam["security"]
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["allowInsecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
config.publicKey = queryParam["pbk"]
config.shortId = queryParam["sid"]
config.spiderX = queryParam["spx"]
config.flow = queryParam["flow"]
}
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
return toUri(config, config.password, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
outboundBean?.settings?.servers?.get(0)?.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,104 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = queryParam["encryption"] ?: "none"
config.network = queryParam["type"] ?: "tcp"
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.security = queryParam["security"]
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["allowInsecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
config.publicKey = queryParam["pbk"]
config.shortId = queryParam["sid"]
config.spiderX = queryParam["spx"]
config.flow = queryParam["flow"]
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
dicQuery["encryption"] = config.method ?: "none"
return toUri(config, config.password, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VLESS)
outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].encryption = profileItem.method
vnext.users[0].flow = profileItem.flow
}
outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}

View File

@@ -0,0 +1,194 @@
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.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object VmessFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str)
}
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
return null
}
config.remarks = vmessQRCode.ps
config.server = vmessQRCode.add
config.serverPort = vmessQRCode.port
config.password = vmessQRCode.id
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
config.network = vmessQRCode.net ?: "tcp"
config.headerType = vmessQRCode.type
config.host = vmessQRCode.host
config.path = vmessQRCode.path
when (config.network) {
"kcp" -> {
config.seed = vmessQRCode.path
}
"quic" -> {
config.quicSecurity = vmessQRCode.host
config.quicKey = vmessQRCode.path
}
"grpc" -> {
config.mode = vmessQRCode.type
config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host
}
}
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 (config.network) {
"kcp" -> {
vmessQRCode.path = config.seed.orEmpty()
}
"quic" -> {
vmessQRCode.host = config.quicSecurity.orEmpty()
vmessQRCode.path = config.quicKey.orEmpty()
}
"grpc" -> {
vmessQRCode.type = config.mode.orEmpty()
vmessQRCode.path = config.serviceName.orEmpty()
vmessQRCode.host = config.authority.orEmpty()
}
}
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? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = AppConfig.DEFAULT_SECURITY
config.network = queryParam["type"] ?: "tcp"
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.security = queryParam["security"]
config.insecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
return config
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VMESS)
outboundBean?.settings?.vnext?.get(0)?.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,95 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object WireguardFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
config.publicKey = queryParam["publickey"].orEmpty()
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = (queryParam["reserved"] ?: "0,0,0")
return config
}
fun parseWireguardConfFile(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val queryParam: MutableMap<String, String> = mutableMapOf()
var currentSection: String? = null
str.lines().forEach { line ->
val trimmedLine = line.trim()
when {
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
currentSection != null -> {
val (key, value) = trimmedLine.split("=").map { it.trim() }
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
}
}
}
config.secretKey = queryParam["privatekey"].orEmpty()
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
config.publicKey = queryParam["publickey"].orEmpty()
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = (queryParam["reserved"] ?: "0,0,0")
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] = config.publicKey.orEmpty()
if (config.reserved != null) {
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty()
}
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty()
if (config.mtu != null) {
dicQuery["mtu"] = config.mtu.toString()
}
return toUri(config, config.secretKey, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.get(0)?.publicKey = profileItem.publicKey.orEmpty()
wireguard.peers?.get(0)?.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
wireguard.mtu = profileItem.mtu?.toInt()
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
}
return outboundBean
}
}

View File

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

View File

@@ -0,0 +1,190 @@
package com.v2ray.ang.handler
import android.util.Log
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object MigrateManager {
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
fun migrateServerConfig2Profile(): Boolean {
if (serverStorage.count().toInt() == 0) {
return false
}
var 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?.get(0)?.users[0]?.flow ?: outbound?.settings?.servers?.get(0)?.flow
outbound.getTransportSettingDetails()?.let { transportDetails ->
config.network = "tcp"
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?.get(0)?.users?.get(0)?.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?.get(0)?.users?.get(0)?.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 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.ProfileItem
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerAffiliationInfo import com.v2ray.ang.dto.ServerAffiliationInfo
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object MmkvManager { object MmkvManager {
//region private //region private
//private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
private const val ID_MAIN = "MAIN" private const val ID_MAIN = "MAIN"
private const val ID_SERVER_CONFIG = "SERVER_CONFIG" private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
private const val ID_SERVER_RAW = "SERVER_RAW" private const val ID_SERVER_RAW = "SERVER_RAW"
private const val ID_SERVER_AFF = "SERVER_AFF" private const val ID_SERVER_AFF = "SERVER_AFF"
private const val ID_SUB = "SUB" private const val ID_SUB = "SUB"
@@ -27,14 +28,14 @@ object MmkvManager {
private const val KEY_ANG_CONFIGS = "ANG_CONFIGS" private const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
private const val KEY_SUB_IDS = "SUB_IDS" private const val KEY_SUB_IDS = "SUB_IDS"
//private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) } private val 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 profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) } private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) } private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) } private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
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 //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()) { if (guid.isBlank()) {
return null return null
} }
val json = profileStorage.decodeString(guid) val json = profileFullStorage.decodeString(guid)
if (json.isNullOrBlank()) { if (json.isNullOrBlank()) {
return null return null
} }
return JsonUtil.fromJson(json, ProfileItem::class.java) 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() } val key = guid.ifBlank { Utils.getUuid() }
serverStorage.encode(key, JsonUtil.toJson(config)) profileFullStorage.encode(key, JsonUtil.toJson(config))
val serverList = decodeServerList() val serverList = decodeServerList()
if (!serverList.contains(key)) { if (!serverList.contains(key)) {
serverList.add(0, key) serverList.add(0, key)
@@ -94,14 +96,14 @@ object MmkvManager {
mainStorage.encode(KEY_SELECTED_SERVER, key) mainStorage.encode(KEY_SELECTED_SERVER, key)
} }
} }
val profile = ProfileItem( // val profile = ProfileLiteItem(
configType = config.configType, // configType = config.configType,
subscriptionId = config.subscriptionId, // subscriptionId = config.subscriptionId,
remarks = config.remarks, // remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(), // server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(), // serverPort = config.getProxyOutbound()?.getServerPort(),
) // )
profileStorage.encode(key, JsonUtil.toJson(profile)) // profileStorage.encode(key, JsonUtil.toJson(profile))
return key return key
} }
@@ -115,8 +117,8 @@ object MmkvManager {
val serverList = decodeServerList() val serverList = decodeServerList()
serverList.remove(guid) serverList.remove(guid)
encodeServerList(serverList) encodeServerList(serverList)
serverStorage.remove(guid) profileFullStorage.remove(guid)
profileStorage.remove(guid) //profileStorage.remove(guid)
serverAffStorage.remove(guid) serverAffStorage.remove(guid)
} }
@@ -124,7 +126,7 @@ object MmkvManager {
if (subid.isBlank()) { if (subid.isBlank()) {
return return
} }
serverStorage.allKeys()?.forEach { key -> profileFullStorage.allKeys()?.forEach { key ->
decodeServerConfig(key)?.let { config -> decodeServerConfig(key)?.let { config ->
if (config.subscriptionId == subid) { if (config.subscriptionId == subid) {
removeServer(key) removeServer(key)
@@ -164,8 +166,8 @@ object MmkvManager {
fun removeAllServer() { fun removeAllServer() {
mainStorage.clearAll() mainStorage.clearAll()
serverStorage.clearAll() profileFullStorage.clearAll()
profileStorage.clearAll() //profileStorage.clearAll()
serverAffStorage.clearAll() serverAffStorage.clearAll()
} }
@@ -192,7 +194,7 @@ object MmkvManager {
} }
fun decodeServerRaw(guid: String): String? { fun decodeServerRaw(guid: String): String? {
return serverRawStorage.decodeString(guid) ?: return null return serverRawStorage.decodeString(guid)
} }
//endregion //endregion
@@ -302,9 +304,51 @@ object MmkvManager {
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) { fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
if (rulesetList.isNullOrEmpty()) if (rulesetList.isNullOrEmpty())
settingsStorage.encode(PREF_ROUTING_RULESET, "") encodeSettings(PREF_ROUTING_RULESET, "")
else 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)
}
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 //endregion
@@ -312,11 +356,11 @@ object MmkvManager {
//region Others //region Others
fun encodeStartOnBoot(startOnBoot: Boolean) { fun encodeStartOnBoot(startOnBoot: Boolean) {
settingsStorage.encode(PREF_IS_BOOTED, startOnBoot) MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot)
} }
fun decodeStartOnBoot(): Boolean { fun decodeStartOnBoot(): Boolean {
return settingsStorage.decodeBool(PREF_IS_BOOTED, false) return decodeSettingsBool(PREF_IS_BOOTED, false)
} }
//endregion //endregion

View File

@@ -1,20 +1,26 @@
package com.v2ray.ang.util package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.content.res.AssetManager
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RoutingType
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.MmkvManager.decodeProfileConfig import com.v2ray.ang.handler.MmkvManager.decodeServerList
import com.v2ray.ang.util.MmkvManager.decodeServerConfig import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager.decodeServerList import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils.parseInt import com.v2ray.ang.util.Utils.parseInt
import java.io.File
import java.io.FileOutputStream
import java.util.Collections import java.util.Collections
import kotlin.Int
object SettingsManager { object SettingsManager {
@@ -27,12 +33,7 @@ object SettingsManager {
} }
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? { private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
val fileName = when (index) { val fileName = RoutingType.fromIndex(index).fileName
0 -> "custom_routing_white"
1 -> "custom_routing_black"
2 -> "custom_routing_global"
else -> "custom_routing_white"
}
val assets = Utils.readTextFromAssets(context, fileName) val assets = Utils.readTextFromAssets(context, fileName)
if (TextUtils.isEmpty(assets)) { if (TextUtils.isEmpty(assets)) {
return null return null
@@ -41,6 +42,7 @@ object SettingsManager {
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList() return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
} }
fun resetRoutingRulesets(context: Context, index: Int) { fun resetRoutingRulesets(context: Context, index: Int) {
val rulesetList = getPresetRoutingRulesets(context, index) ?: return val rulesetList = getPresetRoutingRulesets(context, index) ?: return
resetRoutingRulesetsCommon(rulesetList) resetRoutingRulesetsCommon(rulesetList)
@@ -134,26 +136,51 @@ object SettingsManager {
MmkvManager.encodeSubsList(subsList) MmkvManager.encodeSubsList(subsList)
} }
fun getServerViaRemarks(remarks: String?): ServerConfig? { fun getServerViaRemarks(remarks: String?): ProfileItem? {
if (remarks == null) { if (remarks == null) {
return null return null
} }
val serverList = decodeServerList() val serverList = decodeServerList()
for (guid in serverList) { for (guid in serverList) {
val profile = decodeProfileConfig(guid) val profile = decodeServerConfig(guid)
if (profile != null && profile.remarks == remarks) { if (profile != null && profile.remarks == remarks) {
return decodeServerConfig(guid) return profile
} }
} }
return null return null
} }
fun getSocksPort(): Int { 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 { 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,15 +1,26 @@
package com.v2ray.ang.util package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_PUB_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_PUB_DOMAIN
import com.v2ray.ang.AppConfig.GEOIP_CN import com.v2ray.ang.AppConfig.GEOIP_CN
import com.v2ray.ang.AppConfig.GEOSITE_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.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.PROTOCOL_FREEDOM
import com.v2ray.ang.AppConfig.TAG_BLOCKED import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
@@ -19,32 +30,34 @@ import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.ConfigResult import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_NETWORK
import com.v2ray.ang.dto.V2rayConfig.Companion.HTTP
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean 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 { fun getV2rayConfig(context: Context, guid: String): ConfigResult {
try { try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
if (config.configType == EConfigType.CUSTOM) { if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
val customConfig = if (raw.isNullOrBlank()) { val domainPort = config.getServerAddressAndPort()
config.fullConfig?.toPrettyPrinting() ?: return ConfigResult(false) return ConfigResult(true, guid, raw, domainPort)
} else {
raw
}
val domainPort = config.getProxyOutbound()?.getServerAddressAndPort()
return ConfigResult(true, guid, customConfig, domainPort)
} }
val result = getV2rayNonCustomConfig(context, config) val result = getV2rayNonCustomConfig(context, config)
//Log.d(ANG_PACKAGE, result.content) Log.d(ANG_PACKAGE, result.content)
result.guid = guid result.guid = guid
return result return result
} catch (e: Exception) { } catch (e: Exception) {
@@ -53,11 +66,10 @@ object V2rayConfigUtil {
} }
} }
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): ConfigResult { private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult {
val result = ConfigResult(false) val result = ConfigResult(false)
val outbound = config.getProxyOutbound() ?: return result val address = config.server ?: return result
val address = outbound.getServerAddress() ?: return result
if (!Utils.isIpAddress(address)) { if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) { if (!Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain") Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
@@ -71,14 +83,14 @@ object V2rayConfigUtil {
return result return result
} }
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning" v2rayConfig.log.loglevel =
MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.remarks = config.remarks v2rayConfig.remarks = config.remarks
inbounds(v2rayConfig) inbounds(v2rayConfig)
val isPlugin = outbound.protocol.equals(EConfigType.HYSTERIA2.name, true) val isPlugin = config.configType == EConfigType.HYSTERIA2
val retOut = outbounds(v2rayConfig, outbound, isPlugin) val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin) val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
routing(v2rayConfig) routing(v2rayConfig)
@@ -87,10 +99,10 @@ object V2rayConfigUtil {
dns(v2rayConfig) dns(v2rayConfig)
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
customLocalDns(v2rayConfig) customLocalDns(v2rayConfig)
} }
if (settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) != true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
v2rayConfig.stats = null v2rayConfig.stats = null
v2rayConfig.policy = null v2rayConfig.policy = null
} }
@@ -107,20 +119,18 @@ object V2rayConfigUtil {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds.forEach { curInbound -> 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 //bind all inbounds to localhost if the user requests
curInbound.listen = LOOPBACK curInbound.listen = LOOPBACK
} }
} }
v2rayConfig.inbounds[0].port = socksPort v2rayConfig.inbounds[0].port = socksPort
val fakedns = settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
?: false
val sniffAllTlsAndHttp = val sniffAllTlsAndHttp =
settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true) MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
?: true
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
v2rayConfig.inbounds[0].sniffing?.routeOnly = v2rayConfig.inbounds[0].sniffing?.routeOnly =
settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false) MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
if (!sniffAllTlsAndHttp) { if (!sniffAllTlsAndHttp) {
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear() v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
} }
@@ -143,7 +153,7 @@ object V2rayConfigUtil {
return true 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) { if (isPlugin) {
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0)) val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
val outboundNew = V2rayConfig.OutboundBean( val outboundNew = V2rayConfig.OutboundBean(
@@ -166,8 +176,9 @@ object V2rayConfigUtil {
return Pair(true, outboundNew.getServerAddressAndPort()) return Pair(true, outboundNew.getServerAddressAndPort())
} }
val outbound = getProxyOutbound(config) ?: return null
val ret = updateOutboundWithGlobalSettings(outbound) val ret = updateOutboundWithGlobalSettings(outbound)
if (!ret) return Pair(false, "") if (!ret) return null
if (v2rayConfig.outbounds.isNotEmpty()) { if (v2rayConfig.outbounds.isNotEmpty()) {
v2rayConfig.outbounds[0] = outbound v2rayConfig.outbounds[0] = outbound
@@ -176,12 +187,12 @@ object V2rayConfigUtil {
} }
updateOutboundFragment(v2rayConfig) updateOutboundFragment(v2rayConfig)
return Pair(true, outbound.getServerAddressAndPort()) return Pair(true, config.getServerAddressAndPort())
} }
private fun fakedns(v2rayConfig: V2rayConfig) { private fun fakedns(v2rayConfig: V2rayConfig) {
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
&& settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true && MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
) { ) {
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean()) v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
} }
@@ -190,7 +201,9 @@ object V2rayConfigUtil {
private fun routing(v2rayConfig: V2rayConfig): Boolean { private fun routing(v2rayConfig: V2rayConfig): Boolean {
try { try {
v2rayConfig.routing.domainStrategy = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "IPIfNonMatch" v2rayConfig.routing.domainStrategy =
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
?: "IPIfNonMatch"
val rulesetItems = MmkvManager.decodeRoutingRulesets() val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key -> rulesetItems?.forEach { key ->
@@ -239,7 +252,7 @@ object V2rayConfigUtil {
private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean { private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
try { 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 geositeCn = arrayListOf(GEOSITE_CN)
val proxyDomain = userRule2Domain(TAG_PROXY) val proxyDomain = userRule2Domain(TAG_PROXY)
val directDomain = userRule2Domain(TAG_DIRECT) val directDomain = userRule2Domain(TAG_DIRECT)
@@ -263,7 +276,7 @@ object V2rayConfigUtil {
) )
val localDnsPort = Utils.parseInt( val localDnsPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT),
AppConfig.PORT_LOCAL_DNS.toInt() AppConfig.PORT_LOCAL_DNS.toInt()
) )
v2rayConfig.inbounds.add( v2rayConfig.inbounds.add(
@@ -293,7 +306,7 @@ object V2rayConfigUtil {
// DNS routing tag // DNS routing tag
v2rayConfig.routing.rules.add( v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean( 0, RulesBean(
inboundTag = arrayListOf("dns-in"), inboundTag = arrayListOf("dns-in"),
outboundTag = "dns-out", outboundTag = "dns-out",
domain = null domain = null
@@ -320,10 +333,8 @@ object V2rayConfigUtil {
if (proxyDomain.size > 0) { if (proxyDomain.size > 0) {
servers.add( servers.add(
V2rayConfig.DnsBean.ServersBean( V2rayConfig.DnsBean.ServersBean(
remoteDns.first(), address = remoteDns.first(),
53, domains = proxyDomain,
proxyDomain,
null
) )
) )
} }
@@ -336,17 +347,17 @@ object V2rayConfigUtil {
if (directDomain.size > 0) { if (directDomain.size > 0) {
servers.add( servers.add(
V2rayConfig.DnsBean.ServersBean( V2rayConfig.DnsBean.ServersBean(
domesticDns.first(), address = domesticDns.first(),
53, domains = directDomain,
directDomain, expectIPs = if (isCnRoutingMode) geoipCn else null,
if (isCnRoutingMode) geoipCn else null skipFallback = true
) )
) )
} }
if (Utils.isPureIpAddress(domesticDns.first())) { if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add( v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean( 0, RulesBean(
outboundTag = TAG_DIRECT, outboundTag = TAG_DIRECT,
port = "53", port = "53",
ip = arrayListOf(domesticDns.first()), ip = arrayListOf(domesticDns.first()),
@@ -362,13 +373,14 @@ object V2rayConfigUtil {
} }
// hardcode googleapi rule to fix play store problems // hardcode googleapi rule to fix play store problems
hosts["domain:googleapis.cn"] = "googleapis.com" hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
// hardcode popular Android Private DNS rule to fix localhost DNS problem // hardcode popular Android Private DNS rule to fix localhost DNS problem
hosts["dns.pub"] = arrayListOf("1.12.12.12", "120.53.53.53") hosts[DNS_PUB_DOMAIN] = DNS_PUB_ADDRESSES
hosts["dns.alidns.com"] = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
hosts["one.one.one.one"] = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001") hosts[DNS_ONE_ONE_DOMAIN] = DNS_ONE_ONE_ADDRESSES
hosts["dns.google"] = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844") hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
// DNS dns对象 // DNS dns对象
v2rayConfig.dns = V2rayConfig.DnsBean( v2rayConfig.dns = V2rayConfig.DnsBean(
@@ -379,7 +391,7 @@ object V2rayConfigUtil {
// DNS routing // DNS routing
if (Utils.isPureIpAddress(remoteDns.first())) { if (Utils.isPureIpAddress(remoteDns.first())) {
v2rayConfig.routing.rules.add( v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean( 0, RulesBean(
outboundTag = TAG_PROXY, outboundTag = TAG_PROXY,
port = "53", port = "53",
ip = arrayListOf(remoteDns.first()), ip = arrayListOf(remoteDns.first()),
@@ -396,7 +408,7 @@ object V2rayConfigUtil {
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean { private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
try { try {
var muxEnabled = settingsStorage?.decodeBool(AppConfig.PREF_MUX_ENABLED, false) var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
val protocol = outbound.protocol val protocol = outbound.protocol
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true) || protocol.equals(EConfigType.SOCKS.name, true)
@@ -414,11 +426,11 @@ object V2rayConfigUtil {
if (muxEnabled == true) { if (muxEnabled == true) {
outbound.mux?.enabled = true outbound.mux?.enabled = true
outbound.mux?.concurrency = outbound.mux?.concurrency =
settingsStorage?.decodeInt(AppConfig.PREF_MUX_CONCURRENCY) ?: 8 MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_CONCURRENCY, 8)
outbound.mux?.xudpConcurrency = outbound.mux?.xudpConcurrency =
settingsStorage?.decodeInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY) ?: 8 MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY, 16)
outbound.mux?.xudpProxyUDP443 = outbound.mux?.xudpProxyUDP443 =
settingsStorage?.decodeString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject" MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject"
} else { } else {
outbound.mux?.enabled = false outbound.mux?.enabled = false
outbound.mux?.concurrency = -1 outbound.mux?.concurrency = -1
@@ -430,20 +442,20 @@ object V2rayConfigUtil {
} else { } else {
outbound.settings?.address as List<*> 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()) localTunAddr = listOf(localTunAddr.first())
} }
outbound.settings?.address = localTunAddr outbound.settings?.address = localTunAddr
} }
if (outbound.streamSettings?.network == DEFAULT_NETWORK 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 path = outbound.streamSettings?.tcpSettings?.header?.request?.path
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
val requestString: String by lazy { val requestString: String by lazy {
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (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"Safari/537.36,"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
} }
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson( outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString, requestString,
@@ -468,11 +480,11 @@ object V2rayConfigUtil {
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean { private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
try { try {
if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
return true return true
} }
if (v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.TLS if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS
&& v2rayConfig.outbounds[0].streamSettings?.security != V2rayConfig.REALITY && v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY
) { ) {
return true return true
} }
@@ -485,12 +497,12 @@ object V2rayConfigUtil {
) )
var packets = var packets =
settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello" MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY
&& packets == "tlshello" && packets == "tlshello"
) { ) {
packets = "1-3" 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"
) { ) {
packets = "tlshello" packets = "tlshello"
@@ -499,16 +511,16 @@ object V2rayConfigUtil {
fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean( fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean( fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
packets = packets, packets = packets,
length = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_LENGTH) length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
?: "50-100", ?: "50-100",
interval = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL) interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
?: "10-20" ?: "10-20"
), ),
noises = listOf( noises = listOf(
V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean( V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
type = "rand", type = "rand",
packet = "100-200", packet = "10-20",
delay = "10-20", delay = "10-16",
) )
), ),
) )
@@ -532,7 +544,11 @@ object V2rayConfigUtil {
return true return true
} }
private fun moreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String, isPlugin: Boolean): Pair<Boolean, String> { private fun moreOutbounds(
v2rayConfig: V2rayConfig,
subscriptionId: String,
isPlugin: Boolean
): Pair<Boolean, String> {
val returnPair = Pair(false, "") val returnPair = Pair(false, "")
var domainPort: String = "" var domainPort: String = ""
@@ -540,11 +556,11 @@ object V2rayConfigUtil {
return returnPair return returnPair
} }
//fragment proxy //fragment proxy
if (settingsStorage?.decodeBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
return returnPair return returnPair
} }
if (subscriptionId.isNullOrEmpty()) { if (subscriptionId.isEmpty()) {
return returnPair return returnPair
} }
try { try {
@@ -556,7 +572,7 @@ object V2rayConfigUtil {
//Previous proxy //Previous proxy
val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile) val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
if (prevNode != null) { if (prevNode != null) {
val prevOutbound = prevNode.getProxyOutbound() val prevOutbound = getProxyOutbound(prevNode)
if (prevOutbound != null) { if (prevOutbound != null) {
updateOutboundWithGlobalSettings(prevOutbound) updateOutboundWithGlobalSettings(prevOutbound)
prevOutbound.tag = TAG_PROXY + "2" prevOutbound.tag = TAG_PROXY + "2"
@@ -565,14 +581,14 @@ object V2rayConfigUtil {
V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean( V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
dialerProxy = prevOutbound.tag dialerProxy = prevOutbound.tag
) )
domainPort = prevOutbound.getServerAddressAndPort() domainPort = prevNode.getServerAddressAndPort()
} }
} }
//Next proxy //Next proxy
val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile) val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
if (nextNode != null) { if (nextNode != null) {
val nextOutbound = nextNode.getProxyOutbound() val nextOutbound = getProxyOutbound(nextNode)
if (nextOutbound != null) { if (nextOutbound != null) {
updateOutboundWithGlobalSettings(nextOutbound) updateOutboundWithGlobalSettings(nextOutbound)
nextOutbound.tag = TAG_PROXY nextOutbound.tag = TAG_PROXY
@@ -594,4 +610,20 @@ object V2rayConfigUtil {
} }
return returnPair 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

@@ -29,8 +29,9 @@ class PluginList : ArrayList<Plugin>() {
init { init {
addAll( addAll(
AngApplication.application.packageManager.queryIntentContentProviders( 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) }) )
.filter { it.providerInfo.exported }.map { NativePlugin(it) })
} }
val lookup = mutableMapOf<String, Plugin>().apply { val lookup = mutableMapOf<String, Plugin>().apply {
@@ -39,13 +40,13 @@ class PluginList : ArrayList<Plugin>() {
if (old != null && old != plugin) { if (old != null && old != plugin) {
this@PluginList.remove(old) this@PluginList.remove(old)
} }
/* if (old != null && old !== plugin) { /* if (old != null && old !== plugin) {
val packages = this@PluginList.filter { it.id == plugin.id } val packages = this@PluginList.filter { it.id == plugin.id }
.joinToString { it.packageName } .joinToString { it.packageName }
val message = "Conflicting plugins found from: $packages" val message = "Conflicting plugins found from: $packages"
Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show() Toast.makeText(SagerNet.application, message, Toast.LENGTH_LONG).show()
throw IllegalStateException(message) throw IllegalStateException(message)
}*/ }*/
} }
check(put(plugin.id, plugin)) check(put(plugin.id, plugin))
} }

View File

@@ -43,6 +43,7 @@ import java.io.FileNotFoundException
object PluginManager { object PluginManager {
class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin) class PluginNotFoundException(val plugin: String) : FileNotFoundException(plugin)
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var cachedPlugins: PluginList? = null private var cachedPlugins: PluginList? = null
fun fetchPlugins() = synchronized(this) { fun fetchPlugins() = synchronized(this) {
@@ -88,30 +89,34 @@ object PluginManager {
flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE flags or PackageManager.MATCH_DIRECT_BOOT_UNAWARE or PackageManager.MATCH_DIRECT_BOOT_AWARE
} }
var providers = AngApplication.application.packageManager.queryIntentContentProviders( 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 } .filter { it.providerInfo.exported }
if (providers.isEmpty()) { if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders( 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 } )
.filter { it.providerInfo.exported }
} }
if (providers.isEmpty()) { if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders( 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 } )
.filter { it.providerInfo.exported }
} }
if (providers.isEmpty()) { if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders( 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 } )
.filter { it.providerInfo.exported }
} }
if (providers.isEmpty()) { if (providers.isEmpty()) {
providers = AngApplication.application.packageManager.queryIntentContentProviders( providers = AngApplication.application.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA
).filter { ).filter {
it.providerInfo.exported && it.providerInfo.exported &&
it.providerInfo.metaData.containsKey(METADATA_KEY_ID) && it.providerInfo.metaData.containsKey(METADATA_KEY_ID) &&
it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId it.providerInfo.metaData.getString(METADATA_KEY_ID) == pluginId
} }
if (providers.size > 1) { if (providers.size > 1) {
providers = listOf(providers[0]) // What if there is more than one? providers = listOf(providers[0]) // What if there is more than one?
@@ -129,7 +134,7 @@ object PluginManager {
try { try {
initNativeFaster(provider)?.also { return InitResult(it) } initNativeFaster(provider)?.also { return InitResult(it) }
} catch (t: Throwable) { } catch (t: Throwable) {
// Logs.w("Initializing native plugin faster mode failed") // Logs.w("Initializing native plugin faster mode failed")
failure = t failure = t
} }
@@ -138,19 +143,23 @@ object PluginManager {
authority(provider.authority) authority(provider.authority)
}.build() }.build()
try { try {
return initNativeFast(AngApplication.application.contentResolver, return initNativeFast(
AngApplication.application.contentResolver,
pluginId, pluginId,
uri)?.let { InitResult(it) } uri
)?.let { InitResult(it) }
} catch (t: Throwable) { } catch (t: Throwable) {
// Logs.w("Initializing native plugin fast mode failed") // Logs.w("Initializing native plugin fast mode failed")
failure?.also { t.addSuppressed(it) } failure?.also { t.addSuppressed(it) }
failure = t failure = t
} }
try { try {
return initNativeSlow(AngApplication.application.contentResolver, return initNativeSlow(
AngApplication.application.contentResolver,
pluginId, pluginId,
uri)?.let { InitResult(it) } uri
)?.let { InitResult(it) }
} catch (t: Throwable) { } catch (t: Throwable) {
failure?.also { t.addSuppressed(it) } failure?.also { t.addSuppressed(it) }
throw t throw t
@@ -180,11 +189,13 @@ object PluginManager {
throw IndexOutOfBoundsException("Plugin entry binary not found") throw IndexOutOfBoundsException("Plugin entry binary not found")
val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin") val pluginDir = File(AngApplication.application.noBackupFilesDir, "plugin")
(cr.query(uri, (cr.query(
uri,
arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE),
null, null,
null, null,
null) null
)
?: return null).use { cursor -> ?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound() if (!cursor.moveToFirst()) entryNotFound()
pluginDir.deleteRecursively() pluginDir.deleteRecursively()
@@ -197,11 +208,13 @@ object PluginManager {
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream -> cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) } file.outputStream().use { outStream -> inStream.copyTo(outStream) }
} }
Os.chmod(file.absolutePath, when (cursor.getType(1)) { Os.chmod(
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1) file.absolutePath, when (cursor.getType(1)) {
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8) Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
else -> throw IllegalArgumentException("File mode should be of type int") Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
}) else -> throw IllegalArgumentException("File mode should be of type int")
}
)
if (path == pluginId) initialized = true if (path == pluginId) initialized = true
} while (cursor.moveToNext()) } while (cursor.moveToNext())
} }
@@ -213,6 +226,7 @@ object PluginManager {
is String -> value is String -> value
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo) is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
.getString(value) .getString(value)
null -> null null -> null
else -> error("meta-data $key has invalid type ${value.javaClass}") else -> error("meta-data $key has invalid type ${value.javaClass}")
} }

View File

@@ -3,8 +3,8 @@ package com.v2ray.ang.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {

View File

@@ -5,8 +5,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.text.TextUtils import android.text.TextUtils
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
class TaskerReceiver : BroadcastReceiver() { class TaskerReceiver : BroadcastReceiver() {

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
class ProcessService { class ProcessService {
private val TAG = ANG_PACKAGE private val TAG = ANG_PACKAGE
private lateinit var process: Process private var process: Process? = null
fun runProcess(context: Context, cmd: MutableList<String>) { fun runProcess(context: Context, cmd: MutableList<String>) {
Log.d(TAG, cmd.toString()) Log.d(TAG, cmd.toString())
@@ -24,7 +24,7 @@ class ProcessService {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(50L) Thread.sleep(50L)
Log.d(TAG, "runProcess check") Log.d(TAG, "runProcess check")
process.waitFor() process?.waitFor()
Log.d(TAG, "runProcess exited") Log.d(TAG, "runProcess exited")
} }
Log.d(TAG, process.toString()) Log.d(TAG, process.toString())
@@ -42,4 +42,4 @@ class ProcessService {
Log.d(TAG, e.toString()) Log.d(TAG, e.toString())
} }
} }
} }

View File

@@ -37,7 +37,7 @@ class QSTileService : TileService() {
setState(Tile.STATE_INACTIVE) setState(Tile.STATE_INACTIVE)
mMsgReceive = ReceiveMessageHandler(this) mMsgReceive = ReceiveMessageHandler(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), Context.RECEIVER_EXPORTED) registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), RECEIVER_EXPORTED)
} else { } else {
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)) registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
} }

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
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.util.AngConfigManager import com.v2ray.ang.handler.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
object SubscriptionUpdater { object SubscriptionUpdater {

View File

@@ -18,16 +18,15 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toSpeedString import com.v2ray.ang.extension.toSpeedString
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.ui.MainActivity import com.v2ray.ang.ui.MainActivity
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.PluginUtil import com.v2ray.ang.util.PluginUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import go.Seq import go.Seq
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
@@ -55,7 +54,7 @@ object V2RayServiceManager {
Seq.setContext(value?.get()?.getService()?.applicationContext) Seq.setContext(value?.get()?.getService()?.applicationContext)
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
} }
var currentConfig: ServerConfig? = null var currentConfig: ProfileItem? = null
private var lastQueryTime = 0L private var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null private var mBuilder: NotificationCompat.Builder? = null
@@ -65,15 +64,17 @@ object V2RayServiceManager {
fun startV2Ray(context: Context) { fun startV2Ray(context: Context) {
if (v2rayPoint.isRunning) return if (v2rayPoint.isRunning) return
val guid = MmkvManager.getSelectServer() ?: return val guid = MmkvManager.getSelectServer() ?: return
val result = V2rayConfigUtil.getV2rayConfig(context, guid) val config = MmkvManager.decodeServerConfig(guid) ?: return
if (!result.status) return if (!Utils.isValidUrl(config.server) && !Utils.isIpAddress(config.server)) return
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) return
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) == true) {
context.toast(R.string.toast_warning_pref_proxysharing_short) context.toast(R.string.toast_warning_pref_proxysharing_short)
} else { } else {
context.toast(R.string.toast_services_start) context.toast(R.string.toast_services_start)
} }
val intent = if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: VPN) == VPN) { val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
Intent(context.applicationContext, V2RayVpnService::class.java) Intent(context.applicationContext, V2RayVpnService::class.java)
} else { } else {
Intent(context.applicationContext, V2RayProxyOnlyService::class.java) Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
@@ -132,7 +133,7 @@ object V2RayServiceManager {
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
return return
} }
val result = V2rayConfigUtil.getV2rayConfig(service, guid) val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status) if (!result.status)
return return
@@ -155,7 +156,7 @@ object V2RayServiceManager {
currentConfig = config currentConfig = config
try { try {
v2rayPoint.runLoop(settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) ?: false) v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true)
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
@@ -378,7 +379,7 @@ object V2RayServiceManager {
private fun startSpeedNotification() { private fun startSpeedNotification() {
if (mDisposable == null && if (mDisposable == null &&
v2rayPoint.isRunning && v2rayPoint.isRunning &&
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
) { ) {
var lastZeroSpeed = false var lastZeroSpeed = false
val outboundTags = currentConfig?.getAllOutboundTags() val outboundTags = currentConfig?.getAllOutboundTags()
@@ -428,10 +429,11 @@ object V2RayServiceManager {
} }
private fun stopSpeedNotification() { private fun stopSpeedNotification() {
if (mDisposable != null) { mDisposable?.let {
mDisposable?.dispose() //stop queryStats it.dispose() //stop queryStats
mDisposable = null mDisposable = null
updateNotification(currentConfig?.remarks, 0, 0) updateNotification(currentConfig?.remarks, 0, 0)
} }
} }
} }

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.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.serializable
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.PluginUtil import com.v2ray.ang.util.PluginUtil
import com.v2ray.ang.util.SpeedtestUtil import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import go.Seq import go.Seq
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -56,12 +56,12 @@ class V2RayTestService : Service() {
private fun startRealPing(guid: String): Long { private fun startRealPing(guid: String): Long {
val retFailure = -1L val retFailure = -1L
val server = MmkvManager.decodeServerConfig(guid) ?: return retFailure val config = MmkvManager.decodeServerConfig(guid) ?: return retFailure
if (server.getProxyOutbound()?.protocol?.equals(EConfigType.HYSTERIA2.name, true) == true) { if (config.configType == EConfigType.HYSTERIA2) {
val delay = PluginUtil.realPingHy2(this, server) val delay = PluginUtil.realPingHy2(this, config)
return delay return delay
} else { } else {
val config = V2rayConfigUtil.getV2rayConfig(this, guid) val config = V2rayConfigManager.getV2rayConfig(this, guid)
if (!config.status) { if (!config.status) {
return retFailure return retFailure
} }

View File

@@ -20,9 +20,9 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.R
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.MyContextWrapper
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -64,7 +64,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
.build() .build()
} }
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager }
@delegate:RequiresApi(Build.VERSION_CODES.P) @delegate:RequiresApi(Build.VERSION_CODES.P)
private val defaultNetworkCallback by lazy { private val defaultNetworkCallback by lazy {
@@ -130,7 +130,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.addRoute("0.0.0.0", 0) builder.addRoute("0.0.0.0", 0)
} }
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126) builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
if (bypassLan) { if (bypassLan) {
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
@@ -139,7 +139,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) // builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
// } else { // } else {
Utils.getVpnDnsServers() Utils.getVpnDnsServers()
@@ -153,9 +153,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty()) builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
val selfPackageName = BuildConfig.APPLICATION_ID val selfPackageName = BuildConfig.APPLICATION_ID
if (settingsStorage?.decodeBool(AppConfig.PREF_PER_APP_PROXY) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY) == true) {
val apps = settingsStorage?.decodeStringSet(AppConfig.PREF_PER_APP_PROXY_SET) val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val bypassApps = settingsStorage?.decodeBool(AppConfig.PREF_BYPASS_APPS) ?: false val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS) == true
//process self package //process self package
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName) if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
apps?.forEach { apps?.forEach {
@@ -215,12 +215,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
"--loglevel", "notice" "--loglevel", "notice"
) )
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
cmd.add("--netif-ip6addr") cmd.add("--netif-ip6addr")
cmd.add(PRIVATE_VLAN6_ROUTER) cmd.add(PRIVATE_VLAN6_ROUTER)
} }
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt()) val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
cmd.add("--dnsgw") cmd.add("--dnsgw")
cmd.add("$LOOPBACK:${localDnsPort}") cmd.add("$LOOPBACK:${localDnsPort}")
} }

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.tbruyelle.rxpermissions3.RxPermissions import com.tbruyelle.rxpermissions3.RxPermissions
@@ -133,13 +134,15 @@ class AboutActivity : BaseActivity() {
} }
private fun showFileChooser() { private fun showFileChooser() {
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
intent.type = "*/*" type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
}
try { try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: android.content.ActivityNotFoundException) { } catch (ex: android.content.ActivityNotFoundException) {
Log.e(AppConfig.ANG_PACKAGE, "File chooser activity not found: ${ex.message}", ex)
toast(R.string.toast_require_file_manager) toast(R.string.toast_require_file_manager)
} }
} }
@@ -149,27 +152,23 @@ class AboutActivity : BaseActivity() {
val uri = it.data?.data val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) { if (it.resultCode == RESULT_OK && uri != null) {
try { try {
try { val targetFile =
val targetFile = File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip") contentResolver.openInputStream(uri).use { input ->
contentResolver.openInputStream(uri).use { input -> targetFile.outputStream().use { fileOut ->
targetFile.outputStream().use { fileOut -> input?.copyTo(fileOut)
input?.copyTo(fileOut)
}
} }
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} catch (e: Exception) {
e.printStackTrace()
} }
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
toast(e.message.toString()) toast(R.string.toast_failure)
} }
} }
} }
} }

View File

@@ -34,9 +34,6 @@ abstract class BaseActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let { super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, Utils.getLocale()))
MyContextWrapper.wrap(newBase, Utils.getLocale())
}
super.attachBaseContext(context)
} }
} }

View File

@@ -33,46 +33,42 @@ class LogcatActivity : BaseActivity() {
} }
private fun logcat(shouldFlushLog: Boolean) { private fun logcat(shouldFlushLog: Boolean) {
binding.pbWaiting.visibility = View.VISIBLE
try { lifecycleScope.launch(Dispatchers.Default) {
binding.pbWaiting.visibility = View.VISIBLE try {
lifecycleScope.launch(Dispatchers.Default) {
if (shouldFlushLog) { if (shouldFlushLog) {
val lst = LinkedHashSet<String>() val lst = linkedSetOf("logcat", "-c")
lst.add("logcat")
lst.add("-c")
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val process = Runtime.getRuntime().exec(lst.toTypedArray()) val process = Runtime.getRuntime().exec(lst.toTypedArray())
process.waitFor() process.waitFor()
} }
} }
val lst = LinkedHashSet<String>() val lst = linkedSetOf(
lst.add("logcat") "logcat", "-d", "-v", "time", "-s",
lst.add("-d") "GoLog,tun2socks,$ANG_PACKAGE,AndroidRuntime,System.err"
lst.add("-v") )
lst.add("time")
lst.add("-s")
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
val process = withContext(Dispatchers.IO) { val process = withContext(Dispatchers.IO) {
Runtime.getRuntime().exec(lst.toTypedArray()) Runtime.getRuntime().exec(lst.toTypedArray())
} }
// val bufferedReader = BufferedReader(
// InputStreamReader(process.inputStream))
// val allText = bufferedReader.use(BufferedReader::readText)
val allText = process.inputStream.bufferedReader().use { it.readText() } val allText = process.inputStream.bufferedReader().use { it.readText() }
launch(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.tvLogcat.text = allText binding.tvLogcat.text = allText
binding.tvLogcat.movementMethod = ScrollingMovementMethod() binding.tvLogcat.movementMethod = ScrollingMovementMethod()
binding.pbWaiting.visibility = View.GONE binding.pbWaiting.visibility = View.GONE
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) } Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
} }
} catch (e: IOException) {
withContext(Dispatchers.Main) {
binding.pbWaiting.visibility = View.GONE
toast(R.string.toast_failure)
}
e.printStackTrace()
} }
} catch (e: IOException) {
e.printStackTrace()
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_logcat, menu) menuInflater.inflate(R.menu.menu_logcat, menu)
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)

View File

@@ -34,11 +34,11 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityMainBinding import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MigrateManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel import com.v2ray.ang.viewmodel.MainViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@@ -46,6 +46,7 @@ import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -89,7 +90,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.fab.setOnClickListener { binding.fab.setOnClickListener {
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
Utils.stopVService(this) Utils.stopVService(this)
} else if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: VPN) == VPN) { } else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
val intent = VpnService.prepare(this) val intent = VpnService.prepare(this)
if (intent == null) { if (intent == null) {
startV2Ray() startV2Ray()
@@ -125,6 +126,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
initGroupTab() initGroupTab()
setupViewModel() setupViewModel()
migrateLegacy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RxPermissions(this) RxPermissions(this)
@@ -171,7 +173,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
mainViewModel.startListenBroadcast() mainViewModel.startListenBroadcast()
mainViewModel.copyAssets(assets) mainViewModel.initAssets(assets)
}
private fun migrateLegacy() {
lifecycleScope.launch(Dispatchers.IO) {
val result = MigrateManager.migrateServerConfig2Profile()
launch(Dispatchers.Main) {
if (result) {
toast(getString(R.string.migration_success))
mainViewModel.reloadServerList()
} else {
//toast(getString(R.string.migration_fail))
}
}
}
} }
private fun initGroupTab() { private fun initGroupTab() {
@@ -341,11 +358,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
R.id.ping_all -> { R.id.ping_all -> {
toast(R.string.connection_test_testing)
mainViewModel.testAllTcping() mainViewModel.testAllTcping()
true true
} }
R.id.real_ping_all -> { R.id.real_ping_all -> {
toast(R.string.connection_test_testing)
mainViewModel.testAllRealPing() mainViewModel.testAllRealPing()
true true
} }
@@ -489,30 +508,35 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
private fun importBatchConfig(server: String?) { private fun importBatchConfig(server: String?) {
// val dialog = AlertDialog.Builder(this)
// .setView(LayoutProgressBinding.inflate(layoutInflater).root)
// .setCancelable(false)
// .show()
binding.pbWaiting.show() binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true) try {
delay(500L) val (count, countSub) = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
launch(Dispatchers.Main) { delay(500L)
if (count > 0) { withContext(Dispatchers.Main) {
toast(R.string.toast_success) when {
mainViewModel.reloadServerList() count > 0 -> {
} else if (countSub > 0) { toast(R.string.toast_success)
initGroupTab() mainViewModel.reloadServerList()
} else { }
toast(R.string.toast_failure)
countSub > 0 -> initGroupTab()
else -> toast(R.string.toast_failure)
}
binding.pbWaiting.hide()
} }
//dialog.dismiss() } catch (e: Exception) {
binding.pbWaiting.hide() withContext(Dispatchers.Main) {
toast(R.string.toast_failure)
binding.pbWaiting.hide()
}
e.printStackTrace()
} }
} }
} }
private fun importConfigCustomClipboard() private fun importConfigCustomClipboard()
: Boolean { : Boolean {
try { try {

View File

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

View File

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

View File

@@ -59,22 +59,23 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
fun bind(appInfo: AppInfo) { fun bind(appInfo: AppInfo) {
this.appInfo = appInfo this.appInfo = appInfo
// Set app icon and name
itemBypassBinding.icon.setImageDrawable(appInfo.appIcon) itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
// name.text = appInfo.appName itemBypassBinding.name.text = if (appInfo.isSystemApp) {
String.format("** %s", appInfo.appName)
itemBypassBinding.checkBox.isChecked = inBlacklist
itemBypassBinding.packageName.text = appInfo.packageName
if (appInfo.isSystemApp) {
itemBypassBinding.name.text = String.format("** %1s", appInfo.appName)
//name.textColor = Color.RED
} else { } else {
itemBypassBinding.name.text = appInfo.appName appInfo.appName
//name.textColor = Color.DKGRAY
} }
// Set package name and checkbox state
itemBypassBinding.packageName.text = appInfo.packageName
itemBypassBinding.checkBox.isChecked = inBlacklist
// Handle item click to toggle blacklist status
itemView.setOnClickListener(this) itemView.setOnClickListener(this)
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {
if (inBlacklist) { if (inBlacklist) {
blacklist.remove(appInfo.packageName) blacklist.remove(appInfo.packageName)

View File

@@ -1,7 +1,6 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@@ -10,7 +9,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingEditBinding import com.v2ray.ang.databinding.ActivityRoutingEditBinding
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -38,7 +37,7 @@ class RoutingEditActivity : BaseActivity() {
private fun bindingServer(rulesetItem: RulesetItem): Boolean { private fun bindingServer(rulesetItem: RulesetItem): Boolean {
binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks) binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
binding.chkLocked.isChecked = rulesetItem.looked ?: false binding.chkLocked.isChecked = rulesetItem.looked == true
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(",")) binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(",")) binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
binding.etPort.text = Utils.getEditable(rulesetItem.port) binding.etPort.text = Utils.getEditable(rulesetItem.port)
@@ -59,16 +58,21 @@ class RoutingEditActivity : BaseActivity() {
private fun saveServer(): Boolean { private fun saveServer(): Boolean {
val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem() val rulesetItem = SettingsManager.getRoutingRuleset(position) ?: RulesetItem()
rulesetItem.remarks = binding.etRemarks.text.toString() rulesetItem.apply {
rulesetItem.looked = binding.chkLocked.isChecked remarks = binding.etRemarks.text.toString()
binding.etDomain.text.toString().let { rulesetItem.domain = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } } looked = binding.chkLocked.isChecked
binding.etIp.text.toString().let { rulesetItem.ip = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } } domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() }
binding.etProtocol.text.toString().let { rulesetItem.protocol = if (it.isEmpty()) null else it.split(",").map { itt -> itt.trim() }.filter { itt -> itt.isNotEmpty() } } ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
binding.etPort.text.toString().let { rulesetItem.port = it.ifEmpty { null } } ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() }
binding.etNetwork.text.toString().let { rulesetItem.network = it.ifEmpty { null } } ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
rulesetItem.outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition] protocol = binding.etProtocol.text.toString().takeIf { it.isNotEmpty() }
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
port = binding.etPort.text.toString().takeIf { it.isNotEmpty() }
network = binding.etNetwork.text.toString().takeIf { it.isNotEmpty() }
outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
}
if (TextUtils.isEmpty(rulesetItem.remarks)) { if (rulesetItem.remarks.isNullOrEmpty()) {
toast(R.string.sub_setting_remarks) toast(R.string.sub_setting_remarks)
return false return false
} }
@@ -79,6 +83,7 @@ class RoutingEditActivity : BaseActivity() {
return true return true
} }
private fun deleteServer(): Boolean { private fun deleteServer(): Boolean {
if (position >= 0) { if (position >= 0) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)

View File

@@ -10,20 +10,19 @@ import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.JsonUtil 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 com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class RoutingSettingActivity : BaseActivity() { class RoutingSettingActivity : BaseActivity() {
private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) } private val binding by lazy { ActivityRoutingSettingBinding.inflate(layoutInflater) }
@@ -51,14 +50,14 @@ class RoutingSettingActivity : BaseActivity() {
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) 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) } found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
} }
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { 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])
} }
} }
} }
@@ -113,30 +112,33 @@ class RoutingSettingActivity : BaseActivity() {
R.id.import_rulesets_from_clipboard -> { R.id.import_rulesets_from_clipboard -> {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
try { val clipboard = try {
val clipboard = Utils.getClipboard(this) Utils.getClipboard(this)
lifecycleScope.launch(Dispatchers.IO) {
val ret = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
launch(Dispatchers.Main) {
if (ret) {
refreshData()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
toast(R.string.toast_failure)
return@setPositiveButton
}
lifecycleScope.launch(Dispatchers.IO) {
val result = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
withContext(Dispatchers.Main) {
if (result) {
refreshData()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.no) { _, _ ->
//do noting //do nothing
} }
.show() .show()
true true
} }
R.id.export_rulesets_to_clipboard -> { R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets() val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) { if (rulesetList.isNullOrEmpty()) {
@@ -152,7 +154,9 @@ class RoutingSettingActivity : BaseActivity() {
} }
fun refreshData() { fun refreshData() {
rulesets = MmkvManager.decodeRoutingRulesets() ?: mutableListOf() rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
} }

View File

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

View File

@@ -7,7 +7,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions3.RxPermissions import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
class ScScannerActivity : BaseActivity() { class ScScannerActivity : BaseActivity() {
@@ -20,26 +20,31 @@ class ScScannerActivity : BaseActivity() {
fun importQRcode(): Boolean { fun importQRcode(): Boolean {
RxPermissions(this) RxPermissions(this)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.subscribe { .subscribe { granted ->
if (it) if (granted) {
scanQRCode.launch(Intent(this, ScannerActivity::class.java)) scanQRCode.launch(Intent(this, ScannerActivity::class.java))
else } else {
toast(R.string.toast_permission_denied) toast(R.string.toast_permission_denied)
}
} }
return true return true
} }
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
val (count, countSub) = AngConfigManager.importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"), "", false) val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
if (count + countSub > 0) { if (count + countSub > 0) {
toast(R.string.toast_success) toast(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toast(R.string.toast_failure)
} }
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
} }
finish() finish()
} }
} }

View File

@@ -12,7 +12,7 @@ import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.toast 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 com.v2ray.ang.util.QRCodeDecoder
import io.github.g00fy2.quickie.QRResult import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanCustomCode import io.github.g00fy2.quickie.ScanCustomCode
@@ -25,7 +25,7 @@ class ScannerActivity : BaseActivity() {
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (settingsStorage?.decodeBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_START_SCAN_IMMEDIATE) == true) {
launchScan() launchScan()
} }
} }
@@ -74,19 +74,17 @@ class ScannerActivity : BaseActivity() {
} }
RxPermissions(this) RxPermissions(this)
.request(permission) .request(permission)
.subscribe { .subscribe { granted ->
if (it) { if (granted) {
try { showFileChooser()
showFileChooser() } else {
} catch (e: Exception) {
e.printStackTrace()
}
} else
toast(R.string.toast_permission_denied) toast(R.string.toast_permission_denied)
}
} }
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -107,13 +105,21 @@ class ScannerActivity : BaseActivity() {
val uri = it.data?.data val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) { if (it.resultCode == RESULT_OK && uri != null) {
try { try {
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri)) val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
val text = QRCodeDecoder.syncDecodeQRCode(bitmap) val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
finished(text.orEmpty()) if (text.isNullOrEmpty()) {
toast(R.string.toast_decoding_failed)
} else {
finished(text)
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
toast(e.message.toString()) toast(R.string.toast_decoding_failed)
} }
} }
} }
} }

View File

@@ -2,6 +2,7 @@ package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -13,22 +14,21 @@ import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_PORT
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.REALITY
import com.v2ray.ang.AppConfig.TLS
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.Utils.getIpv6Address
class ServerActivity : BaseActivity() { class ServerActivity : BaseActivity() {
@@ -87,7 +87,6 @@ class ServerActivity : BaseActivity() {
private val et_address: EditText by lazy { findViewById(R.id.et_address) } private val et_address: EditText by lazy { findViewById(R.id.et_address) }
private val et_port: EditText by lazy { findViewById(R.id.et_port) } private val et_port: EditText by lazy { findViewById(R.id.et_port) }
private val et_id: EditText by lazy { findViewById(R.id.et_id) } private val et_id: EditText by lazy { findViewById(R.id.et_id) }
private val et_alterId: EditText? by lazy { findViewById(R.id.et_alterId) }
private val et_security: EditText? by lazy { findViewById(R.id.et_security) } private val et_security: EditText? by lazy { findViewById(R.id.et_security) }
private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) } private val sp_flow: Spinner? by lazy { findViewById(R.id.sp_flow) }
private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) } private val sp_security: Spinner? by lazy { findViewById(R.id.sp_security) }
@@ -114,11 +113,12 @@ class ServerActivity : BaseActivity() {
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) } private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) }
private val container_spider_x: LinearLayout? by lazy { findViewById(R.id.lay_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_reserved1: EditText? by lazy { findViewById(R.id.et_reserved1) }
private val et_reserved2: EditText? by lazy { findViewById(R.id.et_reserved2) }
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) } private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) } private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) } private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -153,12 +153,15 @@ class ServerActivity : BaseActivity() {
sp_header_type_title?.text = if (networks[position] == "grpc") sp_header_type_title?.text = if (networks[position] == "grpc")
getString(R.string.server_lab_mode_type) else getString(R.string.server_lab_mode_type) else
getString(R.string.server_lab_head_type) getString(R.string.server_lab_head_type)
config?.getProxyOutbound()?.getTransportSettingDetails()?.let { transportDetails -> config?.headerType?.let { it ->
sp_header_type?.setSelection(Utils.arrayFind(types, transportDetails[0])) sp_header_type?.setSelection(Utils.arrayFind(types, it))
et_request_host?.text = Utils.getEditable(transportDetails[1]) }
et_path?.text = Utils.getEditable(transportDetails[2]) config?.host?.let { it ->
et_request_host?.text = Utils.getEditable(it)
}
config?.path?.let { it ->
et_path?.text = Utils.getEditable(it)
} }
tv_request_host?.text = Utils.getEditable( tv_request_host?.text = Utils.getEditable(
getString( getString(
when (networks[position]) { when (networks[position]) {
@@ -201,29 +204,46 @@ class ServerActivity : BaseActivity() {
position: Int, position: Int,
id: Long id: Long
) { ) {
if (streamSecuritys[position].isBlank()) { val isBlank = streamSecuritys[position].isBlank()
container_sni?.visibility = View.GONE val isTLS = streamSecuritys[position] == TLS
container_fingerprint?.visibility = View.GONE
container_alpn?.visibility = View.GONE when {
container_allow_insecure?.visibility = View.GONE // Case 1: Null or blank
container_public_key?.visibility = View.GONE isBlank -> {
container_short_id?.visibility = View.GONE listOf(
container_spider_x?.visibility = View.GONE container_sni, container_fingerprint, container_alpn,
} else { container_allow_insecure, container_public_key,
container_sni?.visibility = View.VISIBLE container_short_id, container_spider_x
container_fingerprint?.visibility = View.VISIBLE ).forEach { it?.visibility = View.GONE }
container_alpn?.visibility = View.VISIBLE }
if (streamSecuritys[position] == TLS) {
// Case 2: TLS value
isTLS -> {
listOf(
container_sni,
container_fingerprint,
container_alpn
).forEach { it?.visibility = View.VISIBLE }
container_allow_insecure?.visibility = View.VISIBLE container_allow_insecure?.visibility = View.VISIBLE
container_public_key?.visibility = View.GONE listOf(
container_short_id?.visibility = View.GONE container_public_key,
container_spider_x?.visibility = View.GONE container_short_id,
} else { container_spider_x
container_allow_insecure?.visibility = View.GONE ).forEach { it?.visibility = View.GONE }
}
// Case 3: Other reality values
else -> {
listOf(container_sni, container_fingerprint).forEach {
it?.visibility = View.VISIBLE
}
container_alpn?.visibility = View.GONE container_alpn?.visibility = View.GONE
container_public_key?.visibility = View.VISIBLE container_allow_insecure?.visibility = View.GONE
container_short_id?.visibility = View.VISIBLE listOf(
container_spider_x?.visibility = View.VISIBLE container_public_key,
container_short_id,
container_spider_x
).forEach { it?.visibility = View.VISIBLE }
} }
} }
} }
@@ -242,120 +262,99 @@ class ServerActivity : BaseActivity() {
/** /**
* binding selected server config * binding selected server config
*/ */
private fun bindingServer(config: ServerConfig): Boolean { private fun bindingServer(config: ProfileItem): Boolean {
val outbound = config.getProxyOutbound() ?: return false
et_remarks.text = Utils.getEditable(config.remarks) et_remarks.text = Utils.getEditable(config.remarks)
et_address.text = Utils.getEditable(outbound.getServerAddress().orEmpty()) et_address.text = Utils.getEditable(config.server.orEmpty())
et_port.text = et_port.text = Utils.getEditable(config.serverPort ?: DEFAULT_PORT.toString())
Utils.getEditable(outbound.getServerPort()?.toString() ?: DEFAULT_PORT.toString()) et_id.text = Utils.getEditable(config.password.orEmpty())
et_id.text = Utils.getEditable(outbound.getPassword().orEmpty())
et_alterId?.text = if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
Utils.getEditable(outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()) et_security?.text = Utils.getEditable(config.username.orEmpty())
if (config.configType == EConfigType.SOCKS
|| config.configType == EConfigType.HTTP
) {
et_security?.text =
Utils.getEditable(outbound.settings?.servers?.get(0)?.users?.get(0)?.user.orEmpty())
} else if (config.configType == EConfigType.VLESS) { } else if (config.configType == EConfigType.VLESS) {
et_security?.text = Utils.getEditable(outbound.getSecurityEncryption().orEmpty()) et_security?.text = Utils.getEditable(config.method.orEmpty())
val flow = Utils.arrayFind( val flow = Utils.arrayFind(flows, config.flow.orEmpty())
flows,
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow.orEmpty()
)
if (flow >= 0) { if (flow >= 0) {
sp_flow?.setSelection(flow) sp_flow?.setSelection(flow)
} }
} else if (config.configType == EConfigType.WIREGUARD) { } else if (config.configType == EConfigType.WIREGUARD) {
et_public_key?.text = et_id.text = Utils.getEditable(config.secretKey.orEmpty())
Utils.getEditable(outbound.settings?.peers?.get(0)?.publicKey.orEmpty()) et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
if (outbound.settings?.reserved == null) { if (config.reserved == null) {
et_reserved1?.text = Utils.getEditable("0") et_reserved1?.text = Utils.getEditable("0,0,0")
et_reserved2?.text = Utils.getEditable("0")
et_reserved3?.text = Utils.getEditable("0")
} else { } else {
et_reserved1?.text = et_reserved1?.text = Utils.getEditable(config.reserved?.toString())
Utils.getEditable(outbound.settings?.reserved?.get(0).toString())
et_reserved2?.text =
Utils.getEditable(outbound.settings?.reserved?.get(1).toString())
et_reserved3?.text =
Utils.getEditable(outbound.settings?.reserved?.get(2).toString())
} }
if (outbound.settings?.address == null) { if (config.localAddress == null) {
et_local_address?.text = et_local_address?.text = Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
} else { } else {
val list = outbound.settings?.address as List<*> et_local_address?.text = Utils.getEditable(config.localAddress)
et_local_address?.text = Utils.getEditable(list.joinToString(","))
} }
if (outbound.settings?.mtu == null) { if (config.mtu == null) {
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU) et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
} else { } else {
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString()) et_local_mtu?.text = Utils.getEditable(config.mtu.toString())
} }
} else if (config.configType == EConfigType.HYSTERIA2) { } 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 = val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
val security =
Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
if (security >= 0) { if (security >= 0) {
sp_security?.setSelection(security) sp_security?.setSelection(security)
} }
val streamSetting = config.outboundBean?.streamSettings ?: return true val streamSecurity = Utils.arrayFind(streamSecuritys, config.security.orEmpty())
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
if (streamSecurity >= 0) { if (streamSecurity >= 0) {
sp_stream_security?.setSelection(streamSecurity) sp_stream_security?.setSelection(streamSecurity)
(streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting -> container_sni?.visibility = View.VISIBLE
container_sni?.visibility = View.VISIBLE container_fingerprint?.visibility = View.VISIBLE
container_fingerprint?.visibility = View.VISIBLE container_alpn?.visibility = View.VISIBLE
container_alpn?.visibility = View.VISIBLE
et_sni?.text = Utils.getEditable(tlsSetting.serverName) et_sni?.text = Utils.getEditable(config.sni)
tlsSetting.fingerprint?.let { config.fingerPrint?.let {
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint) val utlsIndex = Utils.arrayFind(uTlsItems, it)
sp_stream_fingerprint?.setSelection(utlsIndex) sp_stream_fingerprint?.setSelection(utlsIndex)
}
tlsSetting.alpn?.let {
val alpnIndex = Utils.arrayFind(
alpns,
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
)
sp_stream_alpn?.setSelection(alpnIndex)
}
if (streamSetting.tlsSettings != null) {
container_allow_insecure?.visibility = View.VISIBLE
val allowinsecure =
Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
if (allowinsecure >= 0) {
sp_allow_insecure?.setSelection(allowinsecure)
}
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else { // reality settings
container_public_key?.visibility = View.VISIBLE
et_public_key?.text = Utils.getEditable(tlsSetting.publicKey.orEmpty())
container_short_id?.visibility = View.VISIBLE
et_short_id?.text = Utils.getEditable(tlsSetting.shortId.orEmpty())
container_spider_x?.visibility = View.VISIBLE
et_spider_x?.text = Utils.getEditable(tlsSetting.spiderX.orEmpty())
container_allow_insecure?.visibility = View.GONE
}
} }
if (streamSetting.tlsSettings == null && streamSetting.realitySettings == null) { config.alpn?.let {
container_sni?.visibility = View.GONE val alpnIndex = Utils.arrayFind(alpns, it)
container_fingerprint?.visibility = View.GONE sp_stream_alpn?.setSelection(alpnIndex)
container_alpn?.visibility = View.GONE }
container_allow_insecure?.visibility = View.GONE if (config.security == TLS) {
container_allow_insecure?.visibility = View.VISIBLE
val allowinsecure = Utils.arrayFind(allowinsecures, config.insecure.toString())
if (allowinsecure >= 0) {
sp_allow_insecure?.setSelection(allowinsecure)
}
container_public_key?.visibility = View.GONE container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE container_spider_x?.visibility = View.GONE
} else if (config.security == REALITY) { // reality settings
container_public_key?.visibility = View.VISIBLE
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
container_short_id?.visibility = View.VISIBLE
et_short_id?.text = Utils.getEditable(config.shortId.orEmpty())
container_spider_x?.visibility = View.VISIBLE
et_spider_x?.text = Utils.getEditable(config.spiderX.orEmpty())
container_allow_insecure?.visibility = View.GONE
} }
} }
val network = Utils.arrayFind(networks, streamSetting.network)
if (config.security.isNullOrEmpty()) {
container_sni?.visibility = View.GONE
container_fingerprint?.visibility = View.GONE
container_alpn?.visibility = View.GONE
container_allow_insecure?.visibility = View.GONE
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
}
val network = Utils.arrayFind(networks, config.network.orEmpty())
if (network >= 0) { if (network >= 0) {
sp_network?.setSelection(network) sp_network?.setSelection(network)
} }
@@ -370,7 +369,6 @@ class ServerActivity : BaseActivity() {
et_address.text = null et_address.text = null
et_port.text = Utils.getEditable(DEFAULT_PORT.toString()) et_port.text = Utils.getEditable(DEFAULT_PORT.toString())
et_id.text = null et_id.text = null
et_alterId?.text = Utils.getEditable("0")
sp_security?.setSelection(0) sp_security?.setSelection(0)
sp_network?.setSelection(0) sp_network?.setSelection(0)
@@ -384,9 +382,7 @@ class ServerActivity : BaseActivity() {
//et_security.text = null //et_security.text = null
sp_flow?.setSelection(0) sp_flow?.setSelection(0)
et_public_key?.text = null et_public_key?.text = null
et_reserved1?.text = Utils.getEditable("0") et_reserved1?.text = Utils.getEditable("0,0,0")
et_reserved2?.text = Utils.getEditable("0")
et_reserved3?.text = Utils.getEditable("0")
et_local_address?.text = et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}") Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU) et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
@@ -405,13 +401,13 @@ class ServerActivity : BaseActivity() {
toast(R.string.server_lab_address) toast(R.string.server_lab_address)
return false return false
} }
val port = Utils.parseInt(et_port.text.toString()) if (createConfigType != EConfigType.HYSTERIA2) {
if (port <= 0) { if (Utils.parseInt(et_port.text.toString()) <= 0) {
toast(R.string.server_lab_port) toast(R.string.server_lab_port)
return false return false
}
} }
val config = val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType)
MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(createConfigType)
if (config.configType != EConfigType.SOCKS if (config.configType != EConfigType.SOCKS
&& config.configType != EConfigType.HTTP && config.configType != EConfigType.HTTP
&& TextUtils.isEmpty(et_id.text.toString()) && TextUtils.isEmpty(et_id.text.toString())
@@ -432,125 +428,73 @@ class ServerActivity : BaseActivity() {
return false return false
} }
} }
et_alterId?.let {
val alterId = Utils.parseInt(it.text.toString())
if (alterId < 0) {
toast(R.string.server_lab_alterid)
return false
}
}
config.remarks = et_remarks.text.toString().trim() saveCommon(config)
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext -> saveStreamSettings(config)
saveVnext(vnext, port, config) saveTls(config)
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
saveServers(server, port, config)
}
val wireguard = config.outboundBean?.settings
wireguard?.peers?.get(0)?.let { _ ->
savePeer(wireguard, port)
}
config.outboundBean?.streamSettings?.let {
val sni = saveStreamSettings(it)
saveTls(it, sni)
}
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty() config.subscriptionId = subscriptionId.orEmpty()
} }
if (config.configType == EConfigType.HYSTERIA2) { Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config))
config.outboundBean?.settings?.obfsPassword = et_obfs_password?.text?.toString()
}
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
toast(R.string.toast_success) toast(R.string.toast_success)
finish() finish()
return true return true
} }
private fun saveVnext( private fun saveCommon(config: ProfileItem) {
vnext: V2rayConfig.OutboundBean.OutSettingsBean.VnextBean, config.remarks = et_remarks.text.toString().trim()
port: Int, config.server = et_address.text.toString().trim()
config: ServerConfig config.serverPort = et_port.text.toString().trim()
) { config.password = et_id.text.toString().trim()
vnext.address = et_address.text.toString().trim()
vnext.port = port
vnext.users[0].id = et_id.text.toString().trim()
if (config.configType == EConfigType.VMESS) { if (config.configType == EConfigType.VMESS) {
vnext.users[0].alterId = Utils.parseInt(et_alterId?.text.toString()) config.method = securitys[sp_security?.selectedItemPosition ?: 0]
vnext.users[0].security = securitys[sp_security?.selectedItemPosition ?: 0]
} else if (config.configType == EConfigType.VLESS) { } else if (config.configType == EConfigType.VLESS) {
vnext.users[0].encryption = et_security?.text.toString().trim() config.method = et_security?.text.toString().trim()
vnext.users[0].flow = flows[sp_flow?.selectedItemPosition ?: 0] config.flow = flows[sp_flow?.selectedItemPosition ?: 0]
} } else if (config.configType == EConfigType.SHADOWSOCKS) {
} config.method = shadowsocksSecuritys[sp_security?.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]
} else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) { } else if (config.configType == EConfigType.SOCKS || config.configType == EConfigType.HTTP) {
if (TextUtils.isEmpty(et_security?.text) && TextUtils.isEmpty(et_id.text)) { if (!TextUtils.isEmpty(et_security?.text) || !TextUtils.isEmpty(et_id.text)) {
server.users = null config.username = et_security?.text.toString().trim()
} else {
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = et_security?.text.toString().trim()
socksUsersBean.pass = et_id.text.toString().trim()
server.users = listOf(socksUsersBean)
} }
} else if (config.configType == EConfigType.TROJAN || config.configType == EConfigType.HYSTERIA2) { } else if (config.configType == EConfigType.TROJAN) {
server.password = et_id.text.toString().trim() } else if (config.configType == EConfigType.WIREGUARD) {
config.secretKey = et_id.text.toString().trim()
config.publicKey = et_public_key?.text.toString().trim()
config.reserved = et_reserved1?.text.toString().trim()
config.localAddress = et_local_address?.text.toString().trim()
config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
} else if (config.configType == EConfigType.HYSTERIA2) {
config.obfsPassword = et_obfs_password?.text?.toString()
config.portHopping = et_port_hop?.text?.toString()
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
config.pinSHA256 = et_pinsha256?.text?.toString()
} }
} }
private fun savePeer(wireguard: V2rayConfig.OutboundBean.OutSettingsBean, port: Int) {
wireguard.secretKey = et_id.text.toString().trim() private fun saveStreamSettings(profileItem: ProfileItem) {
wireguard.peers?.get(0)?.publicKey = et_public_key?.text.toString().trim() val network = sp_network?.selectedItemPosition ?: return
wireguard.peers?.get(0)?.endpoint = val type = sp_header_type?.selectedItemPosition ?: return
getIpv6Address(et_address.text.toString().trim()) + ":" + port val requestHost = et_request_host?.text?.toString()?.trim() ?: return
val reserved1 = Utils.parseInt(et_reserved1?.text.toString()) val path = et_path?.text?.toString()?.trim() ?: return
val reserved2 = Utils.parseInt(et_reserved2?.text.toString())
val reserved3 = Utils.parseInt(et_reserved3?.text.toString()) profileItem.network = networks[network]
if (reserved1 > 0 || reserved2 > 0 || reserved3 > 0) { profileItem.headerType = transportTypes(networks[network])[type]
wireguard.reserved = listOf(reserved1, reserved2, reserved3) profileItem.host = requestHost
} else { profileItem.path = path
wireguard.reserved = null profileItem.seed = path
} profileItem.quicSecurity = requestHost
wireguard.address = et_local_address?.text.toString().removeWhiteSpace().split(",") profileItem.quicKey = path
wireguard.mtu = Utils.parseInt(et_local_mtu?.text.toString()) profileItem.mode = transportTypes(networks[network])[type]
profileItem.serviceName = path
profileItem.authority = requestHost
} }
private fun saveStreamSettings(streamSetting: V2rayConfig.OutboundBean.StreamSettingsBean): String? { private fun saveTls(config: ProfileItem) {
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?) {
val streamSecurity = sp_stream_security?.selectedItemPosition ?: return val streamSecurity = sp_stream_security?.selectedItemPosition ?: return
val sniField = et_sni?.text?.toString()?.trim() val sniField = et_sni?.text?.toString()?.trim()
val allowInsecureField = sp_allow_insecure?.selectedItemPosition val allowInsecureField = sp_allow_insecure?.selectedItemPosition
@@ -560,22 +504,21 @@ class ServerActivity : BaseActivity() {
val shortId = et_short_id?.text?.toString() val shortId = et_short_id?.text?.toString()
val spiderX = et_spider_x?.text?.toString() val spiderX = et_spider_x?.text?.toString()
val allowInsecure = if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) { val allowInsecure =
settingsStorage?.decodeBool(PREF_ALLOW_INSECURE) ?: false if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
} else { MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE) == true
allowinsecures[allowInsecureField].toBoolean() } else {
} allowinsecures[allowInsecureField].toBoolean()
}
streamSetting.populateTlsSettings( config.security = streamSecuritys[streamSecurity]
streamSecurity = streamSecuritys[streamSecurity], config.insecure = allowInsecure
allowInsecure = allowInsecure, config.sni = sniField
sni = sniField ?: sni ?: "", config.fingerPrint = uTlsItems[utlsIndex]
fingerprint = uTlsItems[utlsIndex], config.alpn = alpns[alpnIndex]
alpns = alpns[alpnIndex], config.publicKey = publicKey
publicKey = publicKey, config.shortId = shortId
shortId = shortId, config.spiderX = spiderX
spiderX = spiderX
)
} }
private fun transportTypes(network: String?): Array<out String> { private fun transportTypes(network: String?): Array<out String> {
@@ -599,12 +542,12 @@ class ServerActivity : BaseActivity() {
} }
/** /**
* save server config * delete server config
*/ */
private fun deleteServer(): Boolean { private fun deleteServer(): Boolean {
if (editGuid.isNotEmpty()) { if (editGuid.isNotEmpty()) {
if (editGuid != MmkvManager.getSelectServer()) { if (editGuid != MmkvManager.getSelectServer()) {
if (settingsStorage?.decodeBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
MmkvManager.removeServer(editGuid) MmkvManager.removeServer(editGuid)

View File

@@ -8,15 +8,14 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.utils.EditorTheme import com.blacksquircle.ui.editorkit.utils.EditorTheme
import com.blacksquircle.ui.language.json.JsonLanguage import com.blacksquircle.ui.language.json.JsonLanguage
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
@@ -48,16 +47,14 @@ class ServerCustomConfigActivity : BaseActivity() {
} }
/** /**
* bingding seleced server config * Binding selected server config
*/ */
private fun bindingServer(config: ServerConfig): Boolean { private fun bindingServer(config: ProfileItem): Boolean {
binding.etRemarks.text = Utils.getEditable(config.remarks) binding.etRemarks.text = Utils.getEditable(config.remarks)
val raw = MmkvManager.decodeServerRaw(editGuid) val raw = MmkvManager.decodeServerRaw(editGuid)
if (raw.isNullOrBlank()) { val configContent = raw.orEmpty()
binding.editor.setTextContent(Utils.getEditable(config.fullConfig?.toPrettyPrinting().orEmpty()))
} else { binding.editor.setTextContent(Utils.getEditable(configContent))
binding.editor.setTextContent(Utils.getEditable(raw))
}
return true return true
} }
@@ -86,9 +83,8 @@ class ServerCustomConfigActivity : BaseActivity() {
return false return false
} }
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM) val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM)
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString() config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString()
config.fullConfig = v2rayConfig
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) 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.AppConfig.VPN
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.toLongEx import com.v2ray.ang.extension.toLongEx
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.SubscriptionUpdater import com.v2ray.ang.service.SubscriptionUpdater
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.SettingsViewModel import com.v2ray.ang.viewmodel.SettingsViewModel
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -172,33 +172,33 @@ class SettingsActivity : BaseActivity() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
updateMode(settingsStorage.decodeString(AppConfig.PREF_MODE, VPN)) updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
localDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false) localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
fakeDns?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FAKE_DNS_ENABLED, false) fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
localDnsPort?.summary = settingsStorage.decodeString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS) localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
vpnDns?.summary = settingsStorage.decodeString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN) vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
updateMux(settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false)) updateMux(MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false))
mux?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_MUX_ENABLED, false) mux?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
muxConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8") muxConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8")
muxXudpConcurrency?.summary = settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8") muxXudpConcurrency?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")
updateFragment(settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false)) updateFragment(MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false))
fragment?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_FRAGMENT_ENABLED, false) fragment?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false)
fragmentPackets?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello") fragmentPackets?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")
fragmentLength?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100") fragmentLength?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")
fragmentInterval?.summary = settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20") fragmentInterval?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")
autoUpdateCheck?.isChecked = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) autoUpdateCheck?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
autoUpdateInterval?.summary = autoUpdateInterval?.summary =
settingsStorage.decodeString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL) MmkvManager.decodeSettingsString(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
autoUpdateInterval?.isEnabled = settingsStorage.getBoolean(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
socksPort?.summary = settingsStorage.decodeString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS) socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
httpPort?.summary = settingsStorage.decodeString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP) httpPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
remoteDns?.summary = settingsStorage.decodeString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY) remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
domesticDns?.summary = settingsStorage.decodeString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
delayTestUrl?.summary = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl) delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
initSharedPreference() initSharedPreference()
} }
@@ -225,7 +225,7 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_SNIFFING_ENABLED, AppConfig.PREF_SNIFFING_ENABLED,
).forEach { key -> ).forEach { key ->
findPreference<CheckBoxPreference>(key)?.isChecked = findPreference<CheckBoxPreference>(key)?.isChecked =
settingsStorage.decodeBool(key, true) MmkvManager.decodeSettingsBool(key, true)
} }
listOf( listOf(
@@ -240,7 +240,7 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_ALLOW_INSECURE AppConfig.PREF_ALLOW_INSECURE
).forEach { key -> ).forEach { key ->
findPreference<CheckBoxPreference>(key)?.isChecked = findPreference<CheckBoxPreference>(key)?.isChecked =
settingsStorage.decodeBool(key, false) MmkvManager.decodeSettingsBool(key, false)
} }
listOf( listOf(
@@ -252,8 +252,8 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_LOGLEVEL, AppConfig.PREF_LOGLEVEL,
AppConfig.PREF_MODE AppConfig.PREF_MODE
).forEach { key -> ).forEach { key ->
if (settingsStorage.decodeString(key) != null) { if (MmkvManager.decodeSettingsString(key) != null) {
findPreference<ListPreference>(key)?.value = settingsStorage.decodeString(key) findPreference<ListPreference>(key)?.value = MmkvManager.decodeSettingsString(key)
} }
} }
} }
@@ -261,14 +261,14 @@ class SettingsActivity : BaseActivity() {
private fun updateMode(mode: String?) { private fun updateMode(mode: String?) {
val vpn = mode == VPN val vpn = mode == VPN
perAppProxy?.isEnabled = vpn perAppProxy?.isEnabled = vpn
perAppProxy?.isChecked = settingsStorage.getBoolean(AppConfig.PREF_PER_APP_PROXY, false) perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY, false)
localDns?.isEnabled = vpn localDns?.isEnabled = vpn
fakeDns?.isEnabled = vpn fakeDns?.isEnabled = vpn
localDnsPort?.isEnabled = vpn localDnsPort?.isEnabled = vpn
vpnDns?.isEnabled = vpn vpnDns?.isEnabled = vpn
if (vpn) { if (vpn) {
updateLocalDns( updateLocalDns(
settingsStorage.getBoolean( MmkvManager.decodeSettingsBool(
AppConfig.PREF_LOCAL_DNS_ENABLED, AppConfig.PREF_LOCAL_DNS_ENABLED,
false false
) )
@@ -310,19 +310,17 @@ class SettingsActivity : BaseActivity() {
muxXudpConcurrency?.isEnabled = enabled muxXudpConcurrency?.isEnabled = enabled
muxXudpQuic?.isEnabled = enabled muxXudpQuic?.isEnabled = enabled
if (enabled) { if (enabled) {
updateMuxConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_CONCURRENCY, "8")) updateMuxConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8"))
updateMuxXudpConcurrency(settingsStorage.decodeString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8")) updateMuxXudpConcurrency(MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "8"))
} }
} }
private fun updateMuxConcurrency(value: String?) { private fun updateMuxConcurrency(value: String?) {
if (value == null) { val concurrency = value?.toIntOrNull() ?: 8
} else { muxConcurrency?.summary = concurrency.toString()
val concurrency = value.toIntOrNull() ?: 8
muxConcurrency?.summary = concurrency.toString()
}
} }
private fun updateMuxXudpConcurrency(value: String?) { private fun updateMuxXudpConcurrency(value: String?) {
if (value == null) { if (value == null) {
muxXudpQuic?.isEnabled = true muxXudpQuic?.isEnabled = true
@@ -338,9 +336,9 @@ class SettingsActivity : BaseActivity() {
fragmentLength?.isEnabled = enabled fragmentLength?.isEnabled = enabled
fragmentInterval?.isEnabled = enabled fragmentInterval?.isEnabled = enabled
if (enabled) { if (enabled) {
updateFragmentPackets(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello")) updateFragmentPackets(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS, "tlshello"))
updateFragmentLength(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100")) updateFragmentLength(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH, "50-100"))
updateFragmentInterval(settingsStorage.decodeString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20")) updateFragmentInterval(MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL, "10-20"))
} }
} }

View File

@@ -10,7 +10,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -81,10 +81,17 @@ class SubEditActivity : BaseActivity() {
toast(R.string.sub_setting_remarks) toast(R.string.sub_setting_remarks)
return false return false
} }
// if (TextUtils.isEmpty(subItem.url)) { if (subItem.url.isNotEmpty()) {
// toast(R.string.sub_setting_url) if (!Utils.isValidUrl(subItem.url)) {
// return false toast(R.string.toast_invalid_url)
// } return false
}
if (!Utils.isValidSubUrl(subItem.url)) {
toast(R.string.toast_insecure_url_protocol)
//return false
}
}
MmkvManager.encodeSubscription(editSubId, subItem) MmkvManager.encodeSubscription(editSubId, subItem)
toast(R.string.toast_success) toast(R.string.toast_success)

View File

@@ -13,9 +13,9 @@ import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.LayoutProgressBinding import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback 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.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch 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.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>(), ItemTouchHelperAdapter { 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 -> holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if( !it.isPressed) return@setOnCheckedChangeListener if (!it.isPressed) return@setOnCheckedChangeListener
subItem.enabled = isChecked subItem.enabled = isChecked
MmkvManager.encodeSubscription(subId, subItem) MmkvManager.encodeSubscription(subId, subItem)

View File

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

View File

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

View File

@@ -29,12 +29,12 @@ import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toTrafficString import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -71,23 +71,11 @@ class UserAssetActivity : BaseActivity() {
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { // Use when to streamline the option selection
R.id.add_file -> { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
showFileChooser() R.id.add_file -> showFileChooser().let { true }
true R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
} R.id.download_file -> downloadGeoFiles().let { true }
R.id.add_url -> {
val intent = Intent(this, UserAssetUrlActivity::class.java)
startActivity(intent)
true
}
R.id.download_file -> {
downloadGeoFiles()
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -120,31 +108,29 @@ class UserAssetActivity : BaseActivity() {
} }
} }
private val chooseFile = val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it -> val uri = result.data?.data
val uri = it.data?.data if (result.resultCode == RESULT_OK && uri != null) {
if (it.resultCode == RESULT_OK && uri != null) { val assetId = Utils.getUuid()
val assetId = Utils.getUuid() runCatching {
try { val assetItem = AssetUrlItem(
val assetItem = AssetUrlItem( getCursorName(uri) ?: uri.toString(),
getCursorName(uri) ?: uri.toString(), "file"
"file" )
)
// check remarks unique val assetList = MmkvManager.decodeAssetUrls()
val assetList = MmkvManager.decodeAssetUrls() if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) { toast(R.string.msg_remark_is_duplicate)
toast(R.string.msg_remark_is_duplicate) } else {
return@registerForActivityResult
}
MmkvManager.encodeAsset(assetId, assetItem) MmkvManager.encodeAsset(assetId, assetItem)
copyFile(uri) copyFile(uri)
} catch (e: Exception) {
toast(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId)
} }
}.onFailure {
toast(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId)
} }
} }
}
private fun copyFile(uri: Uri): String { private fun copyFile(uri: Uri): String {
val targetFile = File(extDir, getCursorName(uri) ?: uri.toString()) val targetFile = File(extDir, getCursorName(uri) ?: uri.toString())
@@ -254,6 +240,15 @@ class UserAssetActivity : BaseActivity() {
return list + assets return list + assets
} }
fun initAssets() {
lifecycleScope.launch(Dispatchers.Default) {
SettingsManager.initAssets(this@UserAssetActivity, assets)
withContext(Dispatchers.Main) {
binding.recyclerView.adapter?.notifyDataSetChanged()
}
}
}
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() { inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
return UserAssetViewHolder( return UserAssetViewHolder(
@@ -285,10 +280,10 @@ class UserAssetActivity : BaseActivity() {
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) { if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
holder.itemUserAssetBinding.layoutEdit.visibility = GONE holder.itemUserAssetBinding.layoutEdit.visibility = GONE
holder.itemUserAssetBinding.layoutRemove.visibility = GONE //holder.itemUserAssetBinding.layoutRemove.visibility = GONE
} else { } else {
holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE } holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE //holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
} }
holder.itemUserAssetBinding.layoutEdit.setOnClickListener { holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
@@ -297,9 +292,16 @@ class UserAssetActivity : BaseActivity() {
startActivity(intent) startActivity(intent)
} }
holder.itemUserAssetBinding.layoutRemove.setOnClickListener { holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
file?.delete() AlertDialog.Builder(this@UserAssetActivity).setMessage(R.string.del_config_comfirm)
MmkvManager.removeAssetUrl(item.first) .setPositiveButton(android.R.string.ok) { _, _ ->
binding.recyclerView.adapter?.notifyItemRemoved(position) file?.delete()
MmkvManager.removeAssetUrl(item.first)
initAssets()
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
} }
} }

View File

@@ -9,7 +9,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.io.File import java.io.File

View File

@@ -23,7 +23,7 @@ object JsonUtil {
val gsonPre = GsonBuilder() val gsonPre = GsonBuilder()
.setPrettyPrinting() .setPrettyPrinting()
.disableHtmlEscaping() .disableHtmlEscaping()
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start .registerTypeAdapter( // custom serializer is needed here since JSON by default parse number as Double, core will fail to start
object : TypeToken<Double>() {}.type, object : TypeToken<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? ->
JsonPrimitive( JsonPrimitive(

View File

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

View File

@@ -23,36 +23,19 @@ object QRCodeDecoder {
* create qrcode using zxing * create qrcode using zxing
*/ */
fun createQRCode(text: String, size: Int = 800): Bitmap? { fun createQRCode(text: String, size: Int = 800): Bitmap? {
try { return runCatching {
val hints = HashMap<EncodeHintType, String>() val hints = mapOf(EncodeHintType.CHARACTER_SET to Charsets.UTF_8)
hints[EncodeHintType.CHARACTER_SET] = "utf-8" val bitMatrix = QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, size, size, hints)
val bitMatrix = QRCodeWriter().encode( val pixels = IntArray(size * size) { i ->
text, if (bitMatrix.get(i % size, i / size)) 0xff000000.toInt() else 0xffffffff.toInt()
BarcodeFormat.QR_CODE, size, size, hints
)
val pixels = IntArray(size * size)
for (y in 0 until size) {
for (x in 0 until size) {
if (bitMatrix.get(x, y)) {
pixels[y * size + x] = 0xff000000.toInt()
} else {
pixels[y * size + x] = 0xffffffff.toInt()
}
}
} }
val bitmap = Bitmap.createBitmap( Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
size, size, setPixels(pixels, 0, size, 0, 0, size, size)
Bitmap.Config.ARGB_8888 }
) }.getOrNull()
bitmap.setPixels(pixels, 0, size, 0, 0, size, size)
return bitmap
} catch (e: Exception) {
e.printStackTrace()
return null
}
} }
/** /**
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。 * 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。
* *
@@ -70,40 +53,24 @@ object QRCodeDecoder {
* @return 返回二维码图片里的内容 或 null * @return 返回二维码图片里的内容 或 null
*/ */
fun syncDecodeQRCode(bitmap: Bitmap?): String? { fun syncDecodeQRCode(bitmap: Bitmap?): String? {
if (bitmap == null) { return bitmap?.let {
return null runCatching {
} val pixels = IntArray(it.width * it.height).also { array ->
var source: RGBLuminanceSource? = null it.getPixels(array, 0, it.width, 0, 0, it.width, it.height)
try {
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
source = RGBLuminanceSource(width, height, pixels)
val qrReader = QRCodeReader()
try {
val result = try {
qrReader.decode(
BinaryBitmap(GlobalHistogramBinarizer(source)),
mapOf(DecodeHintType.TRY_HARDER to true)
)
} catch (e: NotFoundException) {
qrReader.decode(
BinaryBitmap(GlobalHistogramBinarizer(source.invert())),
mapOf(DecodeHintType.TRY_HARDER to true)
)
} }
return result.text val source = RGBLuminanceSource(it.width, it.height, pixels)
} catch (e: Exception) { val qrReader = QRCodeReader()
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
return null try {
qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)), mapOf(DecodeHintType.TRY_HARDER to true)).text
} catch (e: NotFoundException) {
qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source.invert())), mapOf(DecodeHintType.TRY_HARDER to true)).text
}
}.getOrNull()
}
} }
/** /**
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR * 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR
* *
@@ -149,6 +116,6 @@ object QRCodeDecoder {
) )
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
HINTS[DecodeHintType.CHARACTER_SET] = "utf-8" HINTS[DecodeHintType.CHARACTER_SET] = Charsets.UTF_8
} }
} }

View File

@@ -22,9 +22,10 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.Language
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import java.io.IOException import java.io.IOException
import java.net.* import java.net.*
import java.util.* import java.util.*
@@ -129,7 +130,7 @@ object Utils {
* get remote dns servers from preference * get remote dns servers from preference
*/ */
fun getRemoteDnsServers(): List<String> { fun getRemoteDnsServers(): List<String> {
val remoteDns = settingsStorage?.decodeString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY val remoteDns = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) } val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) { if (ret.isEmpty()) {
return listOf(AppConfig.DNS_PROXY) return listOf(AppConfig.DNS_PROXY)
@@ -138,7 +139,7 @@ object Utils {
} }
fun getVpnDnsServers(): List<String> { fun getVpnDnsServers(): List<String> {
val vpnDns = settingsStorage?.decodeString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
return vpnDns.split(",").filter { isPureIpAddress(it) } return vpnDns.split(",").filter { isPureIpAddress(it) }
// allow empty, in that case dns will use system default // allow empty, in that case dns will use system default
} }
@@ -147,7 +148,7 @@ object Utils {
* get remote dns servers from preference * get remote dns servers from preference
*/ */
fun getDomesticDnsServers(): List<String> { fun getDomesticDnsServers(): List<String> {
val domesticDns = settingsStorage?.decodeString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT val domesticDns = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) } val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) { if (ret.isEmpty()) {
return listOf(AppConfig.DNS_DIRECT) return listOf(AppConfig.DNS_DIRECT)
@@ -158,8 +159,11 @@ object Utils {
/** /**
* is ip address * is ip address
*/ */
fun isIpAddress(value: String): Boolean { fun isIpAddress(value: String?): Boolean {
try { try {
if (value.isNullOrEmpty()) {
return false
}
var addr = value var addr = value
if (addr.isEmpty() || addr.isBlank()) { if (addr.isEmpty() || addr.isBlank()) {
return false return false
@@ -386,7 +390,7 @@ object Utils {
fun setNightMode(context: Context) { fun setNightMode(context: Context) {
when (settingsStorage?.decodeString(AppConfig.PREF_UI_MODE_NIGHT, "0")) { when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) "0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) "1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) "2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
@@ -405,17 +409,18 @@ object Utils {
} }
fun getLocale(): Locale { fun getLocale(): Locale {
val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto" val langCode = MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
return when (lang) { val language = Language.fromCode(langCode)
"auto" -> getSysLocale()
"en" -> Locale.ENGLISH return when (language) {
"zh-rCN" -> Locale.CHINA Language.AUTO -> getSysLocale()
"zh-rTW" -> Locale.TRADITIONAL_CHINESE Language.ENGLISH -> Locale.ENGLISH
"vi" -> Locale("vi") Language.CHINA -> Locale.CHINA
"ru" -> Locale("ru") Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
"fa" -> Locale("fa") Language.VIETNAMESE -> Locale("vi")
"bn" -> Locale("bn") Language.RUSSIAN -> Locale("ru")
else -> getSysLocale() Language.PERSIAN -> Locale("fa")
Language.BANGLA -> Locale("bn")
} }
} }
@@ -449,7 +454,7 @@ object Utils {
return if (second) { return if (second) {
AppConfig.DelayTestUrl2 AppConfig.DelayTestUrl2
} else { } else {
settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DelayTestUrl
} }
} }
@@ -465,5 +470,17 @@ object Utils {
// if the program gets here, no port in the range was found // if the program gets here, no port in the range was found
throw IOException("no free port found") throw IOException("no free port found")
} }
fun isValidSubUrl(value: String?): Boolean {
try {
if (value.isNullOrEmpty()) return false
if (URLUtil.isHttpsUrl(value)) return true
if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
} }

View File

@@ -31,7 +31,7 @@ object ZipUtil {
} }
} }
} }
if (filesToCompress.isEmpty()) { if (filesToCompress.isEmpty) {
return false return false
} }

View File

@@ -1,112 +0,0 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.Hysteria2Bean
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object Hysteria2Fmt {
fun parse(str: String): ServerConfig {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config.outboundBean?.streamSettings?.populateTlsSettings(
V2rayConfig.TLS,
if ((queryParam["insecure"].orEmpty()) == "1") true else allowInsecure,
queryParam["sni"] ?: uri.idnHost,
null,
queryParam["alpn"],
null,
null,
null
)
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = uri.userInfo
}
if (!queryParam["obfs-password"].isNullOrEmpty()) {
config.outboundBean?.settings?.obfsPassword = queryParam["obfs-password"]
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
streamSetting.tlsSettings?.let { tlsSetting ->
dicQuery["insecure"] = if (tlsSetting.allowInsecure) "1" else "0"
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] = Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
}
if (!outbound.settings?.obfsPassword.isNullOrEmpty()) {
dicQuery["obfs"] = "salamander"
dicQuery["obfs-password"] = outbound.settings?.obfsPassword ?: ""
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
fun toNativeConfig(config: ServerConfig, socksPort: Int): Hysteria2Bean? {
val outbound = config.getProxyOutbound() ?: return null
val tls = outbound.streamSettings?.tlsSettings
val obfs = if (outbound.settings?.obfsPassword.isNullOrEmpty()) null else
Hysteria2Bean.ObfsBean(
type = "salamander",
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
password = outbound.settings?.obfsPassword
)
)
val bean = Hysteria2Bean(
server = outbound.getServerAddressAndPort(),
auth = outbound.getPassword(),
obfs = obfs,
socks5 = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
http = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
tls = Hysteria2Bean.TlsBean(
sni = tls?.serverName ?: outbound.getServerAddress(),
insecure = tls?.allowInsecure
)
)
return bean
}
}

View File

@@ -1,159 +0,0 @@
package com.v2ray.ang.util.fmt
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt {
fun parse(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
if (!tryResolveResolveSip002(str, config)) {
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result)
?: return null
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
server.password = match.groupValues[2]
server.method = match.groupValues[1].lowercase()
}
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
val url = String.format(
"%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + remark
}
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
try {
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
val method: String
val password: String
if (uri.userInfo.contains(":")) {
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
if (arrUserInfo.count() != 2) {
return false
}
method = arrUserInfo[0]
password = Utils.urlDecode(arrUserInfo[1])
} else {
val base64Decode = Utils.decode(uri.userInfo)
val arrUserInfo = base64Decode.split(":").map { it.trim() }
if (arrUserInfo.count() < 2) {
return false
}
method = arrUserInfo[0]
password = base64Decode.substringAfter(":")
}
val query = Utils.urlDecode(uri.query.orEmpty())
if (query != "") {
val queryPairs = HashMap<String, String>()
val pairs = query.split(";")
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
for (pair in pairs) {
val idx = pair.indexOf("=")
if (idx == -1) {
queryPairs[Utils.urlDecode(pair)] = ""
} else {
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
Utils.urlDecode(pair.substring(idx + 1))
}
}
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
var sni: String? = ""
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
"tcp",
"http",
queryPairs["obfs-host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
} else if (queryPairs["plugin"] == "v2ray-plugin") {
var network = "ws"
if (queryPairs["mode"] == "quic") {
network = "quic"
}
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
network,
null,
queryPairs["host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
}
if ("tls" in queryPairs) {
config.outboundBean?.streamSettings?.populateTlsSettings(
"tls", false, sni.orEmpty(), null, null, null, null, null
)
}
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = password
server.method = method
}
return true
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, e.toString())
return false
}
}
}

View File

@@ -1,69 +0,0 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.Utils
object SocksFmt {
fun parse(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.SOCKS)
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
if (indexS > 0) {
result = Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
result = Utils.decode(result)
}
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
val match =
legacyPattern.matchEntire(result) ?: return null
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = match.groupValues[1]
socksUsersBean.pass = match.groupValues[2]
server.users = listOf(socksUsersBean)
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
else
":"
val url = String.format(
"%s@%s:%s",
Utils.encode(pw),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + remark
}
}

View File

@@ -1,173 +0,0 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object TrojanFmt {
fun parse(str: String): ServerConfig {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
var flow = ""
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
if (uri.rawQuery.isNullOrEmpty()) {
config.outboundBean?.streamSettings?.populateTlsSettings(
V2rayConfig.TLS,
allowInsecure,
"",
fingerprint,
null,
null,
null,
null
)
} else {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
fingerprint = queryParam["fp"].orEmpty()
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
config.outboundBean?.streamSettings?.populateTlsSettings(
queryParam["security"] ?: V2rayConfig.TLS,
allowInsecure,
queryParam["sni"] ?: sni.orEmpty(),
fingerprint,
queryParam["alpn"],
null,
null,
null
)
flow = queryParam["flow"].orEmpty()
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = uri.userInfo
server.flow = flow
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
}

View File

@@ -1,164 +0,0 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt {
fun parse(str: String): ServerConfig? {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
vnext.users[0].flow = queryParam["flow"].orEmpty()
}
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
streamSetting.populateTlsSettings(
queryParam["security"].orEmpty(),
allowInsecure,
queryParam["sni"] ?: sni,
queryParam["fp"].orEmpty(),
queryParam["alpn"],
queryParam["pbk"].orEmpty(),
queryParam["sid"].orEmpty(),
queryParam["spx"].orEmpty()
)
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
dicQuery["encryption"] =
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
else outbound.getSecurityEncryption().orEmpty()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId.orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX.orEmpty())
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
}

View File

@@ -1,155 +0,0 @@
package com.v2ray.ang.util.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.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
object VmessFmt {
fun parse(str: String): ServerConfig? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str)
}
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VMESS)
val streamSetting = config.outboundBean?.streamSettings ?: return null
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = 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.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = vmessQRCode.add
vnext.port = Utils.parseInt(vmessQRCode.port)
vnext.users[0].id = vmessQRCode.id
vnext.users[0].security =
if (TextUtils.isEmpty(vmessQRCode.scy)) V2rayConfig.DEFAULT_SECURITY else vmessQRCode.scy
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
}
val sni = streamSetting.populateTransportSettings(
vmessQRCode.net,
vmessQRCode.type,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.path,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.type,
vmessQRCode.path,
vmessQRCode.host
)
val fingerprint = vmessQRCode.fp
streamSetting.populateTlsSettings(
vmessQRCode.tls,
allowInsecure,
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
fingerprint,
vmessQRCode.alpn,
null,
null,
null
)
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = outbound.getServerAddress().orEmpty()
vmessQRCode.port = outbound.getServerPort().toString()
vmessQRCode.id = outbound.getPassword().orEmpty()
vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
vmessQRCode.net = streamSetting.network
vmessQRCode.tls = streamSetting.security
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
vmessQRCode.alpn =
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString(",")).orEmpty()
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
outbound.getTransportSettingDetails()?.let { transportDetails ->
vmessQRCode.type = transportDetails[0]
vmessQRCode.host = transportDetails[1]
vmessQRCode.path = transportDetails[2]
}
val json = JsonUtil.toJson(vmessQRCode)
return Utils.encode(json)
}
fun parseVmessStd(str: String): ServerConfig? {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].security = V2rayConfig.DEFAULT_SECURITY
vnext.users[0].alterId = 0
}
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
allowInsecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
streamSetting.populateTlsSettings(
queryParam["security"].orEmpty(),
allowInsecure,
queryParam["sni"] ?: sni,
queryParam["fp"].orEmpty(),
queryParam["alpn"],
null,
null,
null
)
return config
}
}

View File

@@ -1,107 +0,0 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.Utils
import java.net.URI
object WireguardFmt {
fun parse(str: String): ServerConfig? {
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery != null) {
val config = ServerConfig.create(EConfigType.WIREGUARD)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config.outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = uri.userInfo
wireguard.address =
(queryParam["address"]
?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
.split(",")
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"].orEmpty()
wireguard.peers?.get(0)?.endpoint =
Utils.getIpv6Address(uri.idnHost) + ":${uri.port}"
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
wireguard.reserved =
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
.map { it.toInt() }
}
return config
} else {
return null
}
}
fun parseWireguardConfFile(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.WIREGUARD)
val queryParam: MutableMap<String, String> = mutableMapOf()
var currentSection: String? = null
str.lines().forEach { line ->
val trimmedLine = line.trim()
when {
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments
currentSection != null -> {
val (key, value) = trimmedLine.split("=").map { it.trim() }
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity
}
}
}
config.outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = queryParam["privatekey"].orEmpty()
wireguard.address = (queryParam["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace().split(",")
wireguard.peers?.getOrNull(0)?.publicKey = queryParam["publickey"].orEmpty()
wireguard.peers?.getOrNull(0)?.endpoint = queryParam["endpoint"].orEmpty()
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
wireguard.reserved = (queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",").map { it.toInt() }
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] =
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
if (outbound.settings?.reserved != null) {
dicQuery["reserved"] = Utils.urlEncode(
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString(","))
.toString()
)
}
dicQuery["address"] = Utils.urlEncode(
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString(","))
.toString()
)
if (outbound.settings?.mtu != null) {
dicQuery["mtu"] = outbound.settings?.mtu.toString()
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(outbound.getPassword().toString()),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
}

View File

@@ -15,34 +15,28 @@ import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.ServersCache import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.SpeedtestUtil import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.Collections import java.util.Collections
class MainViewModel(application: Application) : AndroidViewModel(application) { class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = MmkvManager.decodeServerList() private var serverList = MmkvManager.decodeServerList()
var subscriptionId: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
//var keywordFilter: String = MmkvManager.settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:"" //var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
var keywordFilter = "" var keywordFilter = ""
val serversCache = mutableListOf<ServersCache>() val serversCache = mutableListOf<ServersCache>()
val isRunning by lazy { MutableLiveData<Boolean>() } val isRunning by lazy { MutableLiveData<Boolean>() }
@@ -96,21 +90,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
&& server.contains("routing") && server.contains("routing")
) { ) {
try { try {
val config = ServerConfig.create(EConfigType.CUSTOM) val config = CustomFmt.parse(server) ?: return false
config.subscriptionId = subscriptionId config.subscriptionId = subscriptionId
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config) val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server) MmkvManager.encodeServerRaw(key, server)
serverList.add(0, key) serverList.add(0, key)
val profile = ProfileItem( // val profile = ProfileLiteItem(
configType = config.configType, // configType = config.configType,
subscriptionId = config.subscriptionId, // subscriptionId = config.subscriptionId,
remarks = config.remarks, // remarks = config.remarks,
server = config.getProxyOutbound()?.getServerAddress(), // server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(), // serverPort = config.getProxyOutbound()?.getServerPort(),
) // )
serversCache.add(0, ServersCache(key, profile)) serversCache.add(0, ServersCache(key, config))
return true return true
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -120,7 +112,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
fun swapServer(fromPosition: Int, toPosition: Int) { fun swapServer(fromPosition: Int, toPosition: Int) {
Collections.swap(serverList, fromPosition, toPosition) if (subscriptionId.isEmpty()) {
Collections.swap(serverList, fromPosition, toPosition)
} else {
val fromPosition2 = serverList.indexOf(serversCache[fromPosition].guid)
val toPosition2 = serverList.indexOf(serversCache[toPosition].guid)
Collections.swap(serverList, fromPosition2, toPosition2)
}
Collections.swap(serversCache, fromPosition, toPosition) Collections.swap(serversCache, fromPosition, toPosition)
MmkvManager.encodeServerList(serverList) MmkvManager.encodeServerList(serverList)
} }
@@ -129,18 +127,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun updateCache() { fun updateCache() {
serversCache.clear() serversCache.clear()
for (guid in serverList) { for (guid in serverList) {
var profile = MmkvManager.decodeProfileConfig(guid) var profile = MmkvManager.decodeServerConfig(guid) ?: continue
if (profile == null) { // var profile = MmkvManager.decodeProfileConfig(guid)
val config = MmkvManager.decodeServerConfig(guid) ?: continue // if (profile == null) {
profile = ProfileItem( // val config = MmkvManager.decodeServerConfig(guid) ?: continue
configType = config.configType, // profile = ProfileLiteItem(
subscriptionId = config.subscriptionId, // configType = config.configType,
remarks = config.remarks, // subscriptionId = config.subscriptionId,
server = config.getProxyOutbound()?.getServerAddress(), // remarks = config.remarks,
serverPort = config.getProxyOutbound()?.getServerPort(), // server = config.getProxyOutbound()?.getServerAddress(),
) // serverPort = config.getProxyOutbound()?.getServerPort(),
MmkvManager.encodeServerConfig(guid, config) // )
} // MmkvManager.encodeServerConfig(guid, config)
// }
if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) { if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
continue continue
@@ -153,17 +152,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
fun updateConfigViaSubAll(): Int { fun updateConfigViaSubAll(): Int {
if (subscriptionId.isNullOrEmpty()) { if (subscriptionId.isEmpty()) {
return AngConfigManager.updateConfigViaSubAll() return AngConfigManager.updateConfigViaSubAll()
} else { } else {
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0 val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0
return updateConfigViaSub(Pair(subscriptionId, subItem)) return AngConfigManager.updateConfigViaSub(Pair(subscriptionId, subItem))
} }
} }
fun exportAllServer(): Int { fun exportAllServer(): Int {
val serverListCopy = val serverListCopy =
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) { if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
serverList serverList
} else { } else {
serversCache.map { it.guid }.toList() serversCache.map { it.guid }.toList()
@@ -181,16 +180,16 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
tcpingTestScope.coroutineContext[Job]?.cancelChildren() tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestUtil.closeAllTcpSockets() SpeedtestUtil.closeAllTcpSockets()
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
updateListAction.value = -1 // update all //updateListAction.value = -1 // update all
getApplication<AngApplication>().toast(R.string.connection_test_testing) val serversCopy = serversCache.toList() // Create a copy of the list
for (item in serversCache) { for (item in serversCopy) {
item.profile.let { outbound -> item.profile.let { outbound ->
val serverAddress = outbound.server val serverAddress = outbound.server
val serverPort = outbound.serverPort val serverPort = outbound.serverPort
if (serverAddress != null && serverPort != null) { if (serverAddress != null && serverPort != null) {
tcpingTestScope.launch { tcpingTestScope.launch {
val testResult = SpeedtestUtil.tcping(serverAddress, serverPort) val testResult = SpeedtestUtil.tcping(serverAddress, serverPort.toInt())
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult) MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
updateListAction.value = getPosition(item.guid) updateListAction.value = getPosition(item.guid)
@@ -207,8 +206,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
updateListAction.value = -1 // update all updateListAction.value = -1 // update all
val serversCopy = serversCache.toList() // Create a copy of the list val serversCopy = serversCache.toList() // Create a copy of the list
getApplication<AngApplication>().toast(R.string.connection_test_testing)
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
for (item in serversCopy) { for (item in serversCopy) {
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid) MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
@@ -223,7 +220,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun subscriptionIdChanged(id: String) { fun subscriptionIdChanged(id: String) {
if (subscriptionId != id) { if (subscriptionId != id) {
subscriptionId = id subscriptionId = id
MmkvManager.settingsStorage.encode(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId) MmkvManager.encodeSettings(AppConfig.CACHE_SUBSCRIPTION_ID, subscriptionId)
reloadServerList() reloadServerList()
} }
} }
@@ -255,7 +252,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
fun removeDuplicateServer(): Int { fun removeDuplicateServer(): Int {
val serversCacheCopy = mutableListOf<Pair<String, ServerConfig>>() val serversCacheCopy = mutableListOf<Pair<String, ProfileItem>>()
for (it in serversCache) { for (it in serversCache) {
val config = MmkvManager.decodeServerConfig(it.guid) ?: continue val config = MmkvManager.decodeServerConfig(it.guid) ?: continue
serversCacheCopy.add(Pair(it.guid, config)) serversCacheCopy.add(Pair(it.guid, config))
@@ -263,11 +260,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val deleteServer = mutableListOf<String>() val deleteServer = mutableListOf<String>()
serversCacheCopy.forEachIndexed { index, it -> serversCacheCopy.forEachIndexed { index, it ->
val outbound = it.second.getProxyOutbound() val outbound = it.second.getKeyProperty()
serversCacheCopy.forEachIndexed { index2, it2 -> serversCacheCopy.forEachIndexed { index2, it2 ->
if (index2 > index) { if (index2 > index) {
val outbound2 = it2.second.getProxyOutbound() val outbound2 = it2.second.getKeyProperty()
if (outbound == outbound2 && !deleteServer.contains(it2.first)) { if (outbound.equals(outbound2) && !deleteServer.contains(it2.first)) {
deleteServer.add(it2.first) deleteServer.add(it2.first)
} }
} }
@@ -281,7 +278,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
fun removeAllServer() { fun removeAllServer() {
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) { if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
MmkvManager.removeAllServer() MmkvManager.removeAllServer()
} else { } else {
val serversCopy = serversCache.toList() val serversCopy = serversCache.toList()
@@ -292,7 +289,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
fun removeInvalidServer() { fun removeInvalidServer() {
if (subscriptionId.isNullOrEmpty() && keywordFilter.isNullOrEmpty()) { if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
MmkvManager.removeInvalidServer("") MmkvManager.removeInvalidServer("")
} else { } else {
val serversCopy = serversCache.toList() val serversCopy = serversCache.toList()
@@ -321,30 +318,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
MmkvManager.encodeServerList(serverList) MmkvManager.encodeServerList(serverList)
} }
fun initAssets(assets: AssetManager) {
fun copyAssets(assets: AssetManager) {
val extFolder = Utils.userAssetPath(getApplication<AngApplication>())
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
try { SettingsManager.initAssets(getApplication<AngApplication>(), assets)
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
} }
} }
@@ -353,7 +329,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return return
} }
keywordFilter = keyword keywordFilter = keyword
MmkvManager.settingsStorage.encode(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter) MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
reloadServerList() reloadServerList()
} }
@@ -394,4 +370,4 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
} }
} }

View File

@@ -6,7 +6,7 @@ import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.util.MmkvManager.settingsStorage import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
class SettingsViewModel(application: Application) : AndroidViewModel(application), class SettingsViewModel(application: Application) : AndroidViewModel(application),
@@ -44,8 +44,8 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_FRAGMENT_LENGTH, AppConfig.PREF_FRAGMENT_LENGTH,
AppConfig.PREF_FRAGMENT_INTERVAL, AppConfig.PREF_FRAGMENT_INTERVAL,
AppConfig.PREF_MUX_XUDP_QUIC, AppConfig.PREF_MUX_XUDP_QUIC,
-> { -> {
settingsStorage?.encode(key, sharedPreferences.getString(key, "")) MmkvManager.encodeSettings(key, sharedPreferences.getString(key, ""))
} }
AppConfig.PREF_ROUTE_ONLY_ENABLED, AppConfig.PREF_ROUTE_ONLY_ENABLED,
@@ -63,21 +63,21 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.SUBSCRIPTION_AUTO_UPDATE, AppConfig.SUBSCRIPTION_AUTO_UPDATE,
AppConfig.PREF_FRAGMENT_ENABLED, AppConfig.PREF_FRAGMENT_ENABLED,
AppConfig.PREF_MUX_ENABLED, AppConfig.PREF_MUX_ENABLED,
-> { -> {
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false)) MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, false))
} }
AppConfig.PREF_SNIFFING_ENABLED -> { AppConfig.PREF_SNIFFING_ENABLED -> {
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, true)) MmkvManager.encodeSettings(key, sharedPreferences.getBoolean(key, true))
} }
AppConfig.PREF_MUX_CONCURRENCY, AppConfig.PREF_MUX_CONCURRENCY,
AppConfig.PREF_MUX_XUDP_CONCURRENCY -> { AppConfig.PREF_MUX_XUDP_CONCURRENCY -> {
settingsStorage?.encode(key, sharedPreferences.getString(key, "8")) MmkvManager.encodeSettings(key, sharedPreferences.getString(key, "8"))
} }
// AppConfig.PREF_PER_APP_PROXY_SET -> { // AppConfig.PREF_PER_APP_PROXY_SET -> {
// settingsStorage?.encode(key, sharedPreferences.getStringSet(key, setOf())) // MmkvManager.encodeSettings(key, sharedPreferences.getStringSet(key, setOf()))
// } // }
} }
if (key == AppConfig.PREF_UI_MODE_NIGHT) { if (key == AppConfig.PREF_UI_MODE_NIGHT) {

View File

@@ -33,17 +33,13 @@
android:gravity="center" android:gravity="center"
android:orientation="horizontal"> android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_pref_per_app_proxy"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_per_app_proxy" android:id="@+id/switch_per_app_proxy"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_start" /> android:maxLines="2"
android:text="@string/title_pref_per_app_proxy"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout> </LinearLayout>
@@ -56,18 +52,12 @@
android:gravity="center" android:gravity="center"
android:orientation="horizontal"> android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_bypass_apps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/switch_bypass_apps_mode"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_bypass_apps" android:id="@+id/switch_bypass_apps"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_start" /> android:text="@string/switch_bypass_apps_mode"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -13,6 +13,25 @@
<include layout="@layout/layout_address_port" /> <include layout="@layout/layout_address_port" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_margin_top_height"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_id3" />
<EditText
android:id="@+id/et_id"
android:layout_width="match_parent"
android:layout_height="@dimen/edit_height"
android:inputType="text" />
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -41,16 +60,35 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/server_lab_id3" /> android:text="@string/server_lab_port_hop" />
<EditText <EditText
android:id="@+id/et_id" android:id="@+id/et_port_hop"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/edit_height" android:layout_height="@dimen/edit_height"
android:inputType="text" /> android:inputType="text" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_margin_top_height"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_port_hop_interval" />
<EditText
android:id="@+id/et_port_hop_interval"
android:layout_width="match_parent"
android:layout_height="@dimen/edit_height"
android:inputType="number" />
</LinearLayout>
<include layout="@layout/layout_tls_hysteria2" /> <include layout="@layout/layout_tls_hysteria2" />
<LinearLayout <LinearLayout

View File

@@ -32,26 +32,6 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_margin_top_height"
android:orientation="vertical">
<TextView
android:id="@+id/tv_alterId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_alterid" />
<EditText
android:id="@+id/et_alterId"
android:layout_width="match_parent"
android:layout_height="@dimen/edit_height"
android:inputType="number" />
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -70,21 +70,10 @@
<EditText <EditText
android:id="@+id/et_reserved1" android:id="@+id/et_reserved1"
android:layout_width="60dp" android:layout_width="match_parent"
android:layout_height="@dimen/edit_height" android:layout_height="@dimen/edit_height"
android:inputType="number" /> android:inputType="text" />
<EditText
android:id="@+id/et_reserved2"
android:layout_width="60dp"
android:layout_height="@dimen/edit_height"
android:inputType="number" />
<EditText
android:id="@+id/et_reserved3"
android:layout_width="60dp"
android:layout_height="@dimen/edit_height"
android:inputType="number" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -42,6 +42,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clickable="false" android:clickable="false"
android:focusable="false" android:focusable="false"
android:paddingStart="@dimen/padding_start"/> android:paddingStart="@dimen/padding_start" />
</LinearLayout> </LinearLayout>

View File

@@ -6,8 +6,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical"> android:gravity="center_vertical">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/item_cardview" android:id="@+id/item_cardview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -6,8 +6,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical"> android:gravity="center_vertical">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/item_cardview" android:id="@+id/item_cardview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -65,6 +65,27 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/edit_height" android:layout_height="@dimen/edit_height"
android:entries="@array/allowinsecures" /> android:entries="@array/allowinsecures" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_horizontal_margin"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_stream_pinsha256" />
<EditText
android:id="@+id/et_pinsha256"
android:layout_width="match_parent"
android:layout_height="@dimen/edit_height"
android:inputType="text" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -99,6 +99,7 @@
<string name="server_lab_content">المحتوى</string> <string name="server_lab_content">المحتوى</string>
<string name="toast_none_data_clipboard">لا توجد بيانات في الحافظة</string> <string name="toast_none_data_clipboard">لا توجد بيانات في الحافظة</string>
<string name="toast_invalid_url">رابط URL غير صالح</string> <string name="toast_invalid_url">رابط URL غير صالح</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">تأكد من أن منفذ الاتصالات الواردة يتوافق مع الإعدادات</string> <string name="server_lab_need_inbound">تأكد من أن منفذ الاتصالات الواردة يتوافق مع الإعدادات</string>
<string name="toast_malformed_josn">تكوين مشوه</string> <string name="toast_malformed_josn">تكوين مشوه</string>
<string name="server_lab_request_host6">مضيف (SNI) (اختياري)</string> <string name="server_lab_request_host6">مضيف (SNI) (اختياري)</string>
@@ -113,6 +114,9 @@
<string name="msg_remark_is_duplicate">الملاحظات موجودة بالفعل</string> <string name="msg_remark_is_duplicate">الملاحظات موجودة بالفعل</string>
<string name="toast_action_not_allowed">الإجراء غير مسموح به</string> <string name="toast_action_not_allowed">الإجراء غير مسموح به</string>
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">جار التحميل</string> <string name="msg_dialog_progress">جار التحميل</string>

View File

@@ -98,6 +98,7 @@
<string name="server_lab_content">কনটেন্ট</string> <string name="server_lab_content">কনটেন্ট</string>
<string name="toast_none_data_clipboard">ক্লিপবোর্ডে কোনও তথ্য নেই</string> <string name="toast_none_data_clipboard">ক্লিপবোর্ডে কোনও তথ্য নেই</string>
<string name="toast_invalid_url">অবৈধ URL</string> <string name="toast_invalid_url">অবৈধ URL</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">ইনবাউন্ড পোর্ট নিশ্চিত করুন সেটিংসের সাথে সামঞ্জস্যপূর্ণ</string> <string name="server_lab_need_inbound">ইনবাউন্ড পোর্ট নিশ্চিত করুন সেটিংসের সাথে সামঞ্জস্যপূর্ণ</string>
<string name="toast_malformed_josn">কনফিগারেশন বিকৃত</string> <string name="toast_malformed_josn">কনফিগারেশন বিকৃত</string>
<string name="server_lab_request_host6">হোস্ট (SNI) (ঐচ্ছিক)</string> <string name="server_lab_request_host6">হোস্ট (SNI) (ঐচ্ছিক)</string>
@@ -112,6 +113,9 @@
<string name="msg_remark_is_duplicate">মন্তব্য ইতিমধ্যে বিদ্যমান</string> <string name="msg_remark_is_duplicate">মন্তব্য ইতিমধ্যে বিদ্যমান</string>
<string name="toast_action_not_allowed">অ্যাকশন অনুমোদিত নয়</string> <string name="toast_action_not_allowed">অ্যাকশন অনুমোদিত নয়</string>
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">লোড হচ্ছে</string> <string name="msg_dialog_progress">লোড হচ্ছে</string>
@@ -300,4 +304,12 @@
<item>লাইট</item> <item>লাইট</item>
<item>ডার্ক</item> <item>ডার্ক</item>
</string-array> </string-array>
<string-array name="preset_rulesets">
<item>চায়না হোয়াইটলিস্ট</item>
<item>চায়না ব্ল্যাকলিস্ট</item>
<item>গ্লোবাল</item>
<item>ইরান হোয়াইটলিস্ট</item>
</string-array>
</resources> </resources>

View File

@@ -14,8 +14,8 @@
<string name="notification_action_more">برای اطلاعات بیشتر کلیک کنید</string> <string name="notification_action_more">برای اطلاعات بیشتر کلیک کنید</string>
<string name="toast_services_start">شروع خدمات</string> <string name="toast_services_start">شروع خدمات</string>
<string name="toast_services_stop">توقف خدمات</string> <string name="toast_services_stop">توقف خدمات</string>
<string name="toast_services_success">خدمات با موفقیت شروع شد</string> <string name="toast_services_success">شروع خدمات با موفقیت انجام شد</string>
<string name="toast_services_failure">شروع خدمات انجام نشد!</string> <string name="toast_services_failure">شروع خدمات با موفقیت انجام نشد!</string>
<!--ServerActivity--> <!--ServerActivity-->
<string name="title_server">فایل کانفیگ</string> <string name="title_server">فایل کانفیگ</string>
@@ -23,7 +23,7 @@
<string name="menu_item_save_config">ذخیره کانفیگ</string> <string name="menu_item_save_config">ذخیره کانفیگ</string>
<string name="menu_item_del_config">حذف کانفیگ</string> <string name="menu_item_del_config">حذف کانفیگ</string>
<string name="menu_item_import_config_qrcode">کانفیگ را از QRcode وارد کنید</string> <string name="menu_item_import_config_qrcode">کانفیگ را از QRcode وارد کنید</string>
<string name="menu_item_import_config_clipboard">کانفیگ را از کلیپ‌بورد وارد کنید</string> <string name="menu_item_import_config_clipboard">کانفیگ را از کلیپ ‌بورد وارد کنید</string>
<string name="menu_item_import_config_manually_vmess">تایپ دستی[VMess]</string> <string name="menu_item_import_config_manually_vmess">تایپ دستی[VMess]</string>
<string name="menu_item_import_config_manually_vless">تایپ دستی[VLESS]</string> <string name="menu_item_import_config_manually_vless">تایپ دستی[VLESS]</string>
<string name="menu_item_import_config_manually_ss">تایپ دستی[Shadowsocks]</string> <string name="menu_item_import_config_manually_ss">تایپ دستی[Shadowsocks]</string>
@@ -33,12 +33,12 @@
<string name="menu_item_import_config_manually_wireguard">[Wireguard]تایپ دستی</string> <string name="menu_item_import_config_manually_wireguard">[Wireguard]تایپ دستی</string>
<string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string> <string name="menu_item_import_config_manually_hysteria2">Type manually[Hysteria2]</string>
<string name="menu_item_import_config_custom">کانفیگ سفارشی</string> <string name="menu_item_import_config_custom">کانفیگ سفارشی</string>
<string name="menu_item_import_config_custom_clipboard">کانفیگ سفارشی را از کلیپ‌بورد وارد کنید</string> <string name="menu_item_import_config_custom_clipboard">کانفیگ سفارشی را از کلیپ ‌بورد وارد کنید</string>
<string name="menu_item_import_config_custom_local">کانفیگ سفارشی را به صورت محلی وارد کنید</string> <string name="menu_item_import_config_custom_local">کانفیگ سفارشی را به صورت محلی وارد کنید</string>
<string name="menu_item_import_config_custom_url">کانفیگ سفارشی را از طریق نشانی اینترنتی وارد کنید</string> <string name="menu_item_import_config_custom_url">کانفیگ سفارشی را از طریق نشانی اینترنتی وارد کنید</string>
<string name="menu_item_import_config_custom_url_scan">نشانی اینترنتی اسکن کانفیگ سفارشی را وارد کنید</string> <string name="menu_item_import_config_custom_url_scan">نشانی اینترنتی اسکن کانفیگ سفارشی را وارد کنید</string>
<string name="del_config_comfirm">حذف شود؟</string> <string name="del_config_comfirm">حذف شود؟</string>
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string> <string name="del_invalid_config_comfirm">لطفا قبل از حذف کانفیگ نامعتبر تایید کنید! حذف کانفیگ را تایید می کنید؟</string>
<string name="server_lab_remarks">ملاحظات</string> <string name="server_lab_remarks">ملاحظات</string>
<string name="server_lab_address">نشانی</string> <string name="server_lab_address">نشانی</string>
<string name="server_lab_port">پورت</string> <string name="server_lab_port">پورت</string>
@@ -75,7 +75,7 @@
<string name="server_lab_id3">رمز عبور</string> <string name="server_lab_id3">رمز عبور</string>
<string name="server_lab_security3">امنیت</string> <string name="server_lab_security3">امنیت</string>
<string name="server_lab_id4">رمز عبور (اختیاری)</string> <string name="server_lab_id4">رمز عبور (اختیاری)</string>
<string name="server_lab_security4">نام‌کاربری (اختیاری)</string> <string name="server_lab_security4">نام‌ کاربری (اختیاری)</string>
<string name="server_lab_encryption">رمزنگاری</string> <string name="server_lab_encryption">رمزنگاری</string>
<string name="server_lab_flow">جریان</string> <string name="server_lab_flow">جریان</string>
<string name="server_lab_public_key">PublicKey</string> <string name="server_lab_public_key">PublicKey</string>
@@ -87,7 +87,7 @@
<string name="server_lab_local_mtu">Mtu(optional, default 1420)</string> <string name="server_lab_local_mtu">Mtu(optional, default 1420)</string>
<string name="toast_success">با موفقیت انجام شد</string> <string name="toast_success">با موفقیت انجام شد</string>
<string name="toast_failure">شکست</string> <string name="toast_failure">شکست</string>
<string name="toast_none_data">چیزی نیست</string> <string name="toast_none_data">هیچ داده ای وجود ندارد</string>
<string name="toast_incorrect_protocol">پروتکل نادرست</string> <string name="toast_incorrect_protocol">پروتکل نادرست</string>
<string name="toast_decoding_failed">رمزگشایی انجام نشد</string> <string name="toast_decoding_failed">رمزگشایی انجام نشد</string>
<string name="title_file_chooser">انتخاب فایل کانفیگ</string> <string name="title_file_chooser">انتخاب فایل کانفیگ</string>
@@ -95,17 +95,21 @@
<string name="server_customize_config">کانفیگ سفارشی</string> <string name="server_customize_config">کانفیگ سفارشی</string>
<string name="toast_config_file_invalid">کانفیگ معتبر نیست</string> <string name="toast_config_file_invalid">کانفیگ معتبر نیست</string>
<string name="server_lab_content">محتوا</string> <string name="server_lab_content">محتوا</string>
<string name="toast_none_data_clipboard">هیچ داده‌ای در کلیپ‌بورد وجود ندارد</string> <string name="toast_none_data_clipboard">هیچ داده‌ای در کلیپ ‌بورد وجود ندارد</string>
<string name="toast_invalid_url">نشانی اینترنتی معتبر نیست</string> <string name="toast_invalid_url">نشانی اینترنتی معتبر نیست</string>
<string name="toast_insecure_url_protocol">لطفاً از آدرس اشتراک پروتکل HTTP ناامن استفاده نکنید</string>
<string name="server_lab_need_inbound">اطمینان حاصل کنید که پورت ورودی با تنظیمات مطابقت دارد</string> <string name="server_lab_need_inbound">اطمینان حاصل کنید که پورت ورودی با تنظیمات مطابقت دارد</string>
<string name="toast_malformed_josn">کانفیگ درست نیست</string> <string name="toast_malformed_josn">کانفیگ درست نیست</string>
<string name="server_lab_request_host6">میزبان (SNI) (اختیاری)</string> <string name="server_lab_request_host6">میزبان (SNI) (اختیاری)</string>
<string name="toast_asset_copy_failed">کپی فایل انجام نشد، لطفا از برنامه مدیریت فایل استفاده کنید</string> <string name="toast_asset_copy_failed">کپی فایل انجام نشد، لطفا از برنامه مدیریت فایل استفاده کنید</string>
<string name="menu_item_add_file">افزودن فایل‌ها</string> <string name="menu_item_add_file">افزودن فایل ها</string>
<string name="title_url">URL</string> <string name="title_url">URL</string>
<string name="menu_item_download_file">دانلود فایل‌ها</string> <string name="menu_item_download_file">دانلود فایل‌ ها</string>
<string name="toast_action_not_allowed">این عمل ممنوع است</string> <string name="toast_action_not_allowed">این عمل ممنوع است</string>
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">رمز عبور Obfs</string>
<string name="server_lab_port_hop">پورت پرش (درگاه سرور را بازنویسی می کند)</string>
<string name="server_lab_port_hop_interval">فاصله پورت پرش (ثانیه)</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">URL را اضافه کنید</string> <string name="title_user_asset_add_url">URL را اضافه کنید</string>
@@ -114,25 +118,25 @@
<string name="msg_dialog_progress">بارگذاری</string> <string name="msg_dialog_progress">بارگذاری</string>
<string name="menu_item_search">جستجو</string> <string name="menu_item_search">جستجو</string>
<string name="menu_item_select_all">انتخاب همه</string> <string name="menu_item_select_all">انتخاب همه</string>
<string name="msg_enter_keywords">کلیدواژه‌ها را وارد کنید</string> <string name="msg_enter_keywords">کلیدواژه‌ ها را وارد کنید</string>
<string name="switch_bypass_apps_mode">حالت Bypass</string> <string name="switch_bypass_apps_mode">حالت Bypass</string>
<string name="menu_item_select_proxy_app">انتخاب خودکار پروکسی برنامه</string> <string name="menu_item_select_proxy_app">انتخاب خودکار پروکسی برنامه</string>
<string name="msg_downloading_content">در حال دانلود محتوا</string> <string name="msg_downloading_content">در حال دانلود محتوا</string>
<string name="menu_item_export_proxy_app">خروجی گرفتن در کلیپ‌بورد</string> <string name="menu_item_export_proxy_app">خروجی گرفتن در کلیپ‌ بورد</string>
<string name="menu_item_import_proxy_app">وارد کردن از کلیپ‌بورد</string> <string name="menu_item_import_proxy_app">وارد کردن از کلیپ‌ بورد</string>
<!-- Preferences --> <!-- Preferences -->
<string name="title_settings">تنظیمات</string> <string name="title_settings">تنظیمات</string>
<string name="title_advanced">تنظیمات پیشرفته</string> <string name="title_advanced">تنظیمات پیشرفته</string>
<string name="title_vpn_settings">تنظیمات VPN</string> <string name="title_vpn_settings">تنظیمات VPN</string>
<string name="title_pref_per_app_proxy">پروکسی به تفکیک برنامه</string> <string name="title_pref_per_app_proxy">پروکسی به تفکیک برنامه</string>
<string name="summary_pref_per_app_proxy">عمومی: برنامه بررسی شده پروکسی است، اتصال مستقیم بدون بررسی است. \nحالت bypass: برنامه بررسی شده مستقیما متصل است، پراکسی بررسی نشده است. \nگزینهای برای انتخاب خودکار پروکسی برنامه در منو است</string> <string name="summary_pref_per_app_proxy">عمومی: برنامه بررسی شده پروکسی است، اتصال مستقیم بدون بررسی است. \nحالت bypass: برنامه بررسی شده مستقیما متصل است، پراکسی بررسی نشده است. \nگزینهای برای انتخاب خودکار پروکسی برنامه در منو است.</string>
<string name="title_pref_is_booted">اتصال خودکار هنگام راه اندازی</string> <string name="title_pref_is_booted">اتصال خودکار هنگام راه اندازی</string>
<string name="summary_pref_is_booted">هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد</string> <string name="summary_pref_is_booted">هنگام راه اندازی به طور خودکار به سرور انتخابی متصل می شود که ممکن است ناموفق باشد.</string>
<string name="title_mux_settings">تنظیمات Mux</string> <string name="title_mux_settings">تنظیمات Mux</string>
<string name="title_pref_mux_enabled">فعال کردن Mux</string> <string name="title_pref_mux_enabled">فعال کردن Mux</string>
<string name="summary_pref_mux_enabled">سریعتر است، اما ممکن است باعث اتصال ناپایدار شود\nمخزن ترافیک TCP با 8 اتصال پیش‌فرض، نحوه مدیریت UDP و QUIC را در زیر سفارشی کنید</string> <string name="summary_pref_mux_enabled">سریعتر است، اما ممکن است باعث اتصال ناپایدار شود\nمخزن ترافیک TCP با 8 اتصال پیش‌فرض، نحوه مدیریت UDP و QUIC را در زیر سفارشی کنید.</string>
<string name="title_pref_mux_concurency">اتصالات TCP (محدوده -1 تا 1024)</string> <string name="title_pref_mux_concurency">اتصالات TCP (محدوده -1 تا 1024)</string>
<string name="title_pref_mux_xudp_concurency">اتصالات XUDP (محدوده -1 تا 1024)</string> <string name="title_pref_mux_xudp_concurency">اتصالات XUDP (محدوده -1 تا 1024)</string>
<string name="title_pref_mux_xudp_quic">مدیریت QUIC در تونل mux</string> <string name="title_pref_mux_xudp_quic">مدیریت QUIC در تونل mux</string>
@@ -152,10 +156,10 @@
<string name="title_pref_local_dns_enabled">فعال کردن DNS محلی</string> <string name="title_pref_local_dns_enabled">فعال کردن DNS محلی</string>
<string name="summary_pref_local_dns_enabled">DNS پردازش شده توسط ماژول DNS هسته (توصیه میشود، در صورت نیاز به دور زدن LAN و نشانی mainland)</string> <string name="summary_pref_local_dns_enabled">درخواست های DNS به هسته وارد شده و توسط ماژول DNS پردازش می شوند (توصیه می شود در صورت نیاز به مسیریابی برای دور زدن آدرس های LAN و سرزمین اصلی فعال شود)</string>
<string name="title_pref_fake_dns_enabled">فعال کردن DNS جعلی</string> <string name="title_pref_fake_dns_enabled">فعال کردن DNS جعلی</string>
<string name="summary_pref_fake_dns_enabled">DNS محلی آدرس IP جعلی را برمیگرداند (سریعتر میباشد، اما ممکن است برای برخی از برنامهها کار نکند)</string> <string name="summary_pref_fake_dns_enabled">DNS محلی آدرس های آیپی فیک را بر می گرداند (سریع تر می باشد اما ممکن است برای برخی از برنامه ها کار نکند)</string>
<string name="title_pref_prefer_ipv6">ترجیح دادن IPv6</string> <string name="title_pref_prefer_ipv6">ترجیح دادن IPv6</string>
<string name="summary_pref_prefer_ipv6">ترجیح دادن نشانی و مسیر های IPv6</string> <string name="summary_pref_prefer_ipv6">ترجیح دادن نشانی و مسیر های IPv6</string>
@@ -172,11 +176,11 @@
<string name="summary_pref_delay_test_url">Url</string> <string name="summary_pref_delay_test_url">Url</string>
<string name="title_pref_proxy_sharing_enabled">اجازه اتصالات از طریق LAN</string> <string name="title_pref_proxy_sharing_enabled">اجازه اتصالات از طریق LAN</string>
<string name="summary_pref_proxy_sharing_enabled">دستگاه‌های دیگر می‌توانند از طریق socks/http به پراکسی توسط نشانی آی‌پی شما متصل شوند، فقط در شبکه مورد اعتماد فعال می‌شوند تا از اتصال غیرمجاز جلوگیری کنند</string> <string name="summary_pref_proxy_sharing_enabled">دستگاه‌ های دیگر می‌توانند از طریق socks/http به پراکسی توسط نشانی آی‌پی شما متصل شوند، فقط در شبکه مورد اعتماد فعال می‌شوند تا از اتصال غیرمجاز جلوگیری کنند.</string>
<string name="toast_warning_pref_proxysharing_short">اتصالات از طریق LAN را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید</string> <string name="toast_warning_pref_proxysharing_short">اتصالات از طریق LAN را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید</string>
<string name="title_pref_allow_insecure">موز ناامن</string> <string name="title_pref_allow_insecure">مجوز ناامن</string>
<string name="summary_pref_allow_insecure">هنگام استفاده از TLS، به طور پیش‌فرض مجوز ناامن فعال است</string> <string name="summary_pref_allow_insecure">هنگام استفاده از TLS، به طور پیش‌ فرض مجوز ناامن فعال است.</string>
<string name="title_pref_socks_port">پورت پروکسی SOCKS5</string> <string name="title_pref_socks_port">پورت پروکسی SOCKS5</string>
<string name="summary_pref_socks_port">پورت پروکسی SOCKS5</string> <string name="summary_pref_socks_port">پورت پروکسی SOCKS5</string>
@@ -194,7 +198,7 @@
<string name="summary_pref_start_scan_immediate">دوربین را برای اسکن بلافاصله در هنگام راه اندازی باز کنید، در غیر این صورت می توانید کد را اسکن کنید یا عکسی را در نوار ابزار انتخاب کنید.</string> <string name="summary_pref_start_scan_immediate">دوربین را برای اسکن بلافاصله در هنگام راه اندازی باز کنید، در غیر این صورت می توانید کد را اسکن کنید یا عکسی را در نوار ابزار انتخاب کنید.</string>
<string name="title_pref_feedback">بازخورد</string> <string name="title_pref_feedback">بازخورد</string>
<string name="summary_pref_feedback">بازخورد یا گزارش اشکالات در گیت‌هاب</string> <string name="summary_pref_feedback">بازخورد یا گزارش اشکالات در گیت‌ هاب</string>
<string name="summary_pref_tg_group">عضویت در گروه تلگرام</string> <string name="summary_pref_tg_group">عضویت در گروه تلگرام</string>
<string name="toast_tg_app_not_found">برنامه تلگرام پیدا نشد</string> <string name="toast_tg_app_not_found">برنامه تلگرام پیدا نشد</string>
@@ -202,32 +206,32 @@
<string name="title_about">درباره</string> <string name="title_about">درباره</string>
<string name="title_source_code">کد منبع</string> <string name="title_source_code">کد منبع</string>
<string name="title_tg_channel">کانال تلگرام</string> <string name="title_tg_channel">کانال تلگرام</string>
<string name="title_configuration_backup">پشتیبانگیری از پیکربندی</string> <string name="title_configuration_backup">پشتیبان گیری از پیکربندی</string>
<string name="summary_configuration_backup">محل ذخیره سازی: [%s], پس از حذف نصب برنامه یا پاک کردن فضای ذخیره سازی، نسخه پشتیبان پاک می شود</string> <string name="summary_configuration_backup">محل ذخیره سازی: [%s], پس از حذف نصب برنامه یا پاک کردن فضای ذخیره سازی، نسخه پشتیبان پاک می شود</string>
<string name="title_configuration_restore">بازیابی پیکربندی</string> <string name="title_configuration_restore">بازیابی پیکربندی</string>
<string name="title_configuration_share">اشتراکگذاری پیکربندی</string> <string name="title_configuration_share">اشتراک گذاری پیکربندی</string>
<string name="title_pref_promotion">تبلیغات</string> <string name="title_pref_promotion">تبلیغات</string>
<string name="summary_pref_promotion">تبلیغات، برای جزئیات بیشتر کلیک کنید (کمک مالی کنید تا حذف شود)</string> <string name="summary_pref_promotion">تبلیغات، برای جزئیات بیشتر کلیک کنید (کمک مالی کنید تا حذف شود)</string>
<string name="title_pref_auto_update_subscription">به‌روزرسانی خودکار اشتراک ها</string> <string name="title_pref_auto_update_subscription">به‌روزرسانی خودکار اشتراک ها</string>
<string name="summary_pref_auto_update_subscription">اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند</string> <string name="summary_pref_auto_update_subscription">اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند.</string>
<string name="title_pref_auto_update_interval">فاصله به‌روزرسانی خودکار (دقیقه، حداقل مقدار 15)</string> <string name="title_pref_auto_update_interval">فاصله به‌روزرسانی خودکار (دقیقه، حداقل مقدار 15)</string>
<string name="title_core_loglevel">سطح گزارشات</string> <string name="title_core_loglevel">سطح گزارشات</string>
<string name="title_mode">حالت</string> <string name="title_mode">حالت</string>
<string name="title_mode_help">برای راهنمایی بیشتر روی این متن، کلیک کنید</string> <string name="title_mode_help">برای راهنمایی بیشتر روی این متن، کلیک کنید</string>
<string name="title_language">زبان</string> <string name="title_language">زبان</string>
<string name="title_ui_settings">تنظیمات رابط کاربری</string> <string name="title_ui_settings">تنظیمات رابط کاربری</string>
<string name="title_pref_ui_mode_night">UI mode settings</string> <string name="title_pref_ui_mode_night">تنظیمات حالت رابط کاربری</string>
<string name="title_logcat">گزارشات</string> <string name="title_logcat">گزارشات</string>
<string name="logcat_copy">کپی</string> <string name="logcat_copy">کپی</string>
<string name="logcat_clear">پاک کردن</string> <string name="logcat_clear">پاک کردن</string>
<string name="title_service_restart">راه‌اندازی مجدد خدمات</string> <string name="title_service_restart">راه‌اندازی مجدد خدمات</string>
<string name="title_del_all_config">حذف تمام کانفیگ</string> <string name="title_del_all_config">حذف تمام کانفیگ های گروه فعلی</string>
<string name="title_del_duplicate_config">حذف کانفیگ های تکراری</string> <string name="title_del_duplicate_config">حذف کانفیگ های تکراری گروه فعلی</string>
<string name="title_del_invalid_config">حذف کانفیگهای نامعتبر (ابتدا آزمایش کنید)</string> <string name="title_del_invalid_config">حذف کانفیگ های نامعتبر گروه فعلی (ابتدا آزمایش کنید)</string>
<string name="title_export_all">خروجی گرفتن کانفیگهای غیرسفارشی در کلیپ‌بورد</string> <string name="title_export_all">خروجی گرفتن کانفیگ های غیرسفارشی گروه فعلی در کلیپ ‌بورد</string>
<string name="title_sub_setting">تنظیمات گروه‌ی اشتراک</string> <string name="title_sub_setting">تنظیمات گروه‌ اشتراک</string>
<string name="sub_setting_remarks">ملاحظات</string> <string name="sub_setting_remarks">ملاحظات</string>
<string name="sub_setting_url">نشانی اینترنتی اختیاری</string> <string name="sub_setting_url">نشانی اینترنتی اختیاری</string>
<string name="sub_setting_filter">Remarks regular filter</string> <string name="sub_setting_filter">Remarks regular filter</string>
@@ -236,11 +240,11 @@
<string name="sub_setting_pre_profile">Previous proxy remarks</string> <string name="sub_setting_pre_profile">Previous proxy remarks</string>
<string name="sub_setting_next_profile">Next proxy remarks</string> <string name="sub_setting_next_profile">Next proxy remarks</string>
<string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string> <string name="sub_setting_pre_profile_tip">The remarks exists and is unique</string>
<string name="title_sub_update">به‌روزرسانی اشتراک</string> <string name="title_sub_update">به‌روزرسانی گروه فعلی اشتراک</string>
<string name="title_ping_all_server">Tcping همه کانفیگ</string> <string name="title_ping_all_server">Tcping کانفیگ های گروه فعلی</string>
<string name="title_real_ping_all_server">تاخیر واقعی همه کانفیگ</string> <string name="title_real_ping_all_server">تاخیر واقعی کانفیگ های گروه فعلی</string>
<string name="title_user_asset_setting">فایل‌های دارایی جغرافیا</string> <string name="title_user_asset_setting">فایل ‌های دارایی جغرافیا</string>
<string name="title_sort_by_test_results">مرتب‌سازی بر اساس نتایج آزمایش</string> <string name="title_sort_by_test_results">مرتب‌ سازی بر اساس نتایج آزمایش</string>
<string name="title_filter_config">فیلتر کردن کانفیگ‌ها</string> <string name="title_filter_config">فیلتر کردن کانفیگ‌ها</string>
<string name="filter_config_all">همه گروه‌های اشتراک</string> <string name="filter_config_all">همه گروه‌های اشتراک</string>
<string name="title_del_duplicate_config_count">حذف %d کانفیگ تکراری</string> <string name="title_del_duplicate_config_count">حذف %d کانفیگ تکراری</string>
@@ -250,7 +254,7 @@
<string name="routing_settings_domain_strategy">استراتژی دامنه</string> <string name="routing_settings_domain_strategy">استراتژی دامنه</string>
<string name="routing_settings_title">تنظیمات مسیریابی</string> <string name="routing_settings_title">تنظیمات مسیریابی</string>
<string name="routing_settings_tips">با کاما (,) از هم جدا شوند، ذخیره کردن فراموش نشود</string> <string name="routing_settings_tips">با کاما (،) از هم جدا شوند، ذخیره کردن فراموش نشود</string>
<string name="routing_settings_save">ذخیره</string> <string name="routing_settings_save">ذخیره</string>
<string name="routing_settings_delete">حذف</string> <string name="routing_settings_delete">حذف</string>
<string name="routing_settings_rule_title">تنظیمات قانون مسیریابی</string> <string name="routing_settings_rule_title">تنظیمات قانون مسیریابی</string>
@@ -279,13 +283,13 @@
<string name="title_pref_fragment_enabled">فعال کردن Fragment</string> <string name="title_pref_fragment_enabled">فعال کردن Fragment</string>
<string-array name="share_method"> <string-array name="share_method">
<item>QRcode</item> <item>QRcode</item>
<item>خروجی گرفتن در کلیپ‌بورد</item> <item>خروجی گرفتن در کلیپ‌ بورد</item>
<item>خروجی گرفتن کانفیگ کامل در کلیپبورد</item> <item>خروجی گرفتن کانفیگ کامل در کلیپ بورد</item>
</string-array> </string-array>
<string-array name="share_sub_method"> <string-array name="share_sub_method">
<item>QRcode</item> <item>QRcode</item>
<item>خروجی گرفتن در کلیپ‌بورد</item> <item>خروجی گرفتن در کلیپ‌ بورد</item>
</string-array> </string-array>
<string-array name="mode_entries"> <string-array name="mode_entries">
@@ -296,9 +300,16 @@
<string name="menu_item_add_url">افزودن لینک</string> <string name="menu_item_add_url">افزودن لینک</string>
<string-array name="ui_mode_night"> <string-array name="ui_mode_night">
<item>Follow system</item> <item>پیش فرض سیستم</item>
<item>Light</item> <item>روشن</item>
<item>Dark</item> <item>تاریک</item>
</string-array>
<string-array name="preset_rulesets">
<item>لیست سفید چین</item>
<item>لیست سیاه چین</item>
<item>جهانی(Global)</item>
<item>ایران</item>
</string-array> </string-array>
</resources> </resources>

View File

@@ -97,6 +97,7 @@
<string name="server_lab_content">Данные</string> <string name="server_lab_content">Данные</string>
<string name="toast_none_data_clipboard">В буфере обмена нет данных</string> <string name="toast_none_data_clipboard">В буфере обмена нет данных</string>
<string name="toast_invalid_url">Неправильный URL</string> <string name="toast_invalid_url">Неправильный URL</string>
<string name="toast_insecure_url_protocol">Не используйте небезопасный HTTP-протокол в адресе подписки</string>
<string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string> <string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string>
<string name="toast_malformed_josn">Профиль повреждён</string> <string name="toast_malformed_josn">Профиль повреждён</string>
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string> <string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
@@ -111,6 +112,9 @@
<string name="msg_remark_is_duplicate">Название уже существует</string> <string name="msg_remark_is_duplicate">Название уже существует</string>
<string name="toast_action_not_allowed">Это действие запрещено</string> <string name="toast_action_not_allowed">Это действие запрещено</string>
<string name="server_obfs_password">Пароль obfs</string> <string name="server_obfs_password">Пароль obfs</string>
<string name="server_lab_port_hop">Переключение портов</string>
<string name="server_lab_port_hop_interval">Интервал переключения портов</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Загрузка…</string> <string name="msg_dialog_progress">Загрузка…</string>
@@ -315,6 +319,7 @@
<item>Белый список Китая</item> <item>Белый список Китая</item>
<item>Чёрный список Китая</item> <item>Чёрный список Китая</item>
<item>Общие</item> <item>Общие</item>
<item>Белый список Ирана</item>
</string-array> </string-array>
</resources> </resources>

View File

@@ -97,6 +97,7 @@
<string name="server_lab_content">Nội dung</string> <string name="server_lab_content">Nội dung</string>
<string name="toast_none_data_clipboard">Không có dữ liệu nào trong Clipboard!</string> <string name="toast_none_data_clipboard">Không có dữ liệu nào trong Clipboard!</string>
<string name="toast_invalid_url">URL không hợp lệ hoặc trống!</string> <string name="toast_invalid_url">URL không hợp lệ hoặc trống!</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">Vui lòng đảm bảo cấu hình tùy chỉnh này không bị lỗi trước khi sử dụng!</string> <string name="server_lab_need_inbound">Vui lòng đảm bảo cấu hình tùy chỉnh này không bị lỗi trước khi sử dụng!</string>
<string name="toast_malformed_josn">Cấu hình không hợp lệ!</string> <string name="toast_malformed_josn">Cấu hình không hợp lệ!</string>
<string name="server_lab_request_host6">Host (SNI) (Không bắt buộc)</string> <string name="server_lab_request_host6">Host (SNI) (Không bắt buộc)</string>
@@ -106,6 +107,9 @@
<string name="menu_item_download_file">Tải xuống tệp tin</string> <string name="menu_item_download_file">Tải xuống tệp tin</string>
<string name="toast_action_not_allowed">Hành động này bị cấm!</string> <string name="toast_action_not_allowed">Hành động này bị cấm!</string>
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">Thêm URL nội dung</string> <string name="title_user_asset_add_url">Thêm URL nội dung</string>

View File

@@ -82,7 +82,7 @@
<string name="server_lab_short_id">ShortId</string> <string name="server_lab_short_id">ShortId</string>
<string name="server_lab_spider_x">SpiderX</string> <string name="server_lab_spider_x">SpiderX</string>
<string name="server_lab_secret_key">SecretKey</string> <string name="server_lab_secret_key">SecretKey</string>
<string name="server_lab_reserved">Reserved(可选)</string> <string name="server_lab_reserved">Reserved(可选,逗号隔开)</string>
<string name="server_lab_local_address">本地地址(可选IPv4/IPv6逗号隔开)</string> <string name="server_lab_local_address">本地地址(可选IPv4/IPv6逗号隔开)</string>
<string name="server_lab_local_mtu">Mtu(可选, 默认1420)</string> <string name="server_lab_local_mtu">Mtu(可选, 默认1420)</string>
<string name="toast_success">成功</string> <string name="toast_success">成功</string>
@@ -97,6 +97,7 @@
<string name="server_lab_content">内容</string> <string name="server_lab_content">内容</string>
<string name="toast_none_data_clipboard">剪贴板中没有数据</string> <string name="toast_none_data_clipboard">剪贴板中没有数据</string>
<string name="toast_invalid_url">无效的网址</string> <string name="toast_invalid_url">无效的网址</string>
<string name="toast_insecure_url_protocol">请不要使用不安全的HTTP协议订阅地址</string>
<string name="server_lab_need_inbound">确保inbounds port和设置中的一致</string> <string name="server_lab_need_inbound">确保inbounds port和设置中的一致</string>
<string name="toast_malformed_josn">配置格式错误</string> <string name="toast_malformed_josn">配置格式错误</string>
<string name="server_lab_request_host6">Host(SNI)(可选)</string> <string name="server_lab_request_host6">Host(SNI)(可选)</string>
@@ -106,6 +107,9 @@
<string name="menu_item_download_file">下载文件</string> <string name="menu_item_download_file">下载文件</string>
<string name="toast_action_not_allowed">禁止此项操作</string> <string name="toast_action_not_allowed">禁止此项操作</string>
<string name="server_obfs_password">混淆密码</string> <string name="server_obfs_password">混淆密码</string>
<string name="server_lab_port_hop">跳跃端口(会覆盖服务器端口)</string>
<string name="server_lab_port_hop_interval">端口跳跃间隔(秒)</string>
<string name="server_lab_stream_pinsha256">SHA256证书指纹</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">添加资产网址</string> <string name="title_user_asset_add_url">添加资产网址</string>
@@ -306,6 +310,7 @@
<item>绕过大陆(Whitelist)</item> <item>绕过大陆(Whitelist)</item>
<item>黑名单(Blacklist)</item> <item>黑名单(Blacklist)</item>
<item>全局(Global)</item> <item>全局(Global)</item>
<item>伊朗(Iran)</item>
</string-array> </string-array>
</resources> </resources>

View File

@@ -82,7 +82,7 @@
<string name="server_lab_short_id">ShortId</string> <string name="server_lab_short_id">ShortId</string>
<string name="server_lab_spider_x">SpiderX</string> <string name="server_lab_spider_x">SpiderX</string>
<string name="server_lab_secret_key">SecretKey</string> <string name="server_lab_secret_key">SecretKey</string>
<string name="server_lab_reserved">Reserved (可選)</string> <string name="server_lab_reserved">Reserved (可選,逗號隔開)</string>
<string name="server_lab_local_address">本機位址(可選IPv4/IPv6逗號隔開)</string> <string name="server_lab_local_address">本機位址(可選IPv4/IPv6逗號隔開)</string>
<string name="server_lab_local_mtu">MTU(可選, 預設1420)</string> <string name="server_lab_local_mtu">MTU(可選, 預設1420)</string>
<string name="toast_success">成功</string> <string name="toast_success">成功</string>
@@ -97,6 +97,7 @@
<string name="server_lab_content">內容</string> <string name="server_lab_content">內容</string>
<string name="toast_none_data_clipboard">剪貼簿內無資料</string> <string name="toast_none_data_clipboard">剪貼簿內無資料</string>
<string name="toast_invalid_url">URL 無效</string> <string name="toast_invalid_url">URL 無效</string>
<string name="toast_insecure_url_protocol">請不要使用不安全的HTTP協定訂閱位址</string>
<string name="server_lab_need_inbound">​​確保 inbounds port 和設定中的一致</string> <string name="server_lab_need_inbound">​​確保 inbounds port 和設定中的一致</string>
<string name="toast_malformed_josn">設定格式不正確</string> <string name="toast_malformed_josn">設定格式不正確</string>
<string name="server_lab_request_host6">Host(SNI)(可選)</string> <string name="server_lab_request_host6">Host(SNI)(可選)</string>
@@ -106,6 +107,9 @@
<string name="menu_item_download_file">下載檔案</string> <string name="menu_item_download_file">下載檔案</string>
<string name="toast_action_not_allowed">禁止此項操作</string> <string name="toast_action_not_allowed">禁止此項操作</string>
<string name="server_obfs_password">混淆密碼</string> <string name="server_obfs_password">混淆密碼</string>
<string name="server_lab_port_hop">跳躍連接埠(會覆蓋伺服器連接埠)</string>
<string name="server_lab_port_hop_interval">連接埠跳躍間隔(秒)</string>
<string name="server_lab_stream_pinsha256">SHA256憑證指紋</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">新增資產網址</string> <string name="title_user_asset_add_url">新增資產網址</string>
@@ -308,6 +312,7 @@
<item>繞過大陸(Whitelist)</item> <item>繞過大陸(Whitelist)</item>
<item>黑名單(Blacklist)</item> <item>黑名單(Blacklist)</item>
<item>全域(Global)</item> <item>全域(Global)</item>
<item>伊朗(Iran)</item>
</string-array> </string-array>
</resources> </resources>

View File

@@ -83,7 +83,7 @@
<string name="server_lab_short_id">ShortId</string> <string name="server_lab_short_id">ShortId</string>
<string name="server_lab_spider_x">SpiderX</string> <string name="server_lab_spider_x">SpiderX</string>
<string name="server_lab_secret_key">SecretKey</string> <string name="server_lab_secret_key">SecretKey</string>
<string name="server_lab_reserved">Reserved(Optional)</string> <string name="server_lab_reserved">Reserved(Optional, separated by commas)</string>
<string name="server_lab_local_address">Local address (optional IPv4/IPv6, separated by commas)</string> <string name="server_lab_local_address">Local address (optional IPv4/IPv6, separated by commas)</string>
<string name="server_lab_local_mtu">Mtu(optional, default 1420)</string> <string name="server_lab_local_mtu">Mtu(optional, default 1420)</string>
<string name="toast_success">Success</string> <string name="toast_success">Success</string>
@@ -98,6 +98,7 @@
<string name="server_lab_content">Content</string> <string name="server_lab_content">Content</string>
<string name="toast_none_data_clipboard">There is no data in the clipboard</string> <string name="toast_none_data_clipboard">There is no data in the clipboard</string>
<string name="toast_invalid_url">Invalid URL</string> <string name="toast_invalid_url">Invalid URL</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string>
<string name="server_lab_need_inbound">Ensure inbounds port is consistent with the settings</string> <string name="server_lab_need_inbound">Ensure inbounds port is consistent with the settings</string>
<string name="toast_malformed_josn">Config malformed</string> <string name="toast_malformed_josn">Config malformed</string>
<string name="server_lab_request_host6">Host(SNI)(Optional)</string> <string name="server_lab_request_host6">Host(SNI)(Optional)</string>
@@ -112,6 +113,9 @@
<string name="msg_remark_is_duplicate">The remarks already exists</string> <string name="msg_remark_is_duplicate">The remarks already exists</string>
<string name="toast_action_not_allowed">Action not allowed</string> <string name="toast_action_not_allowed">Action not allowed</string>
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping(will override the port)</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Loading</string> <string name="msg_dialog_progress">Loading</string>
@@ -318,6 +322,7 @@
<item>China Whitelist</item> <item>China Whitelist</item>
<item>China Blacklist</item> <item>China Blacklist</item>
<item>Global</item> <item>Global</item>
<item>Iran Whitelist</item>
</string-array> </string-array>
</resources> </resources>

View File

@@ -37,5 +37,102 @@ class UtilTest {
assertTrue(Utils.isIpAddress("240e:1234:abcd:12::6666")) assertTrue(Utils.isIpAddress("240e:1234:abcd:12::6666"))
assertTrue(Utils.isIpAddress("240e:1234:abcd:12::/64")) assertTrue(Utils.isIpAddress("240e:1234:abcd:12::/64"))
} }
// @Test
// fun test_fmtHysteria2Parse() {
// val url2 = "hysteria2://password2@127.0.0.1:443?obfs=salamander&obfs-password=obfs2&insecure=0#Hy22"
// var result2 = Hysteria2Fmt.parse(url2)
// assertTrue(result2 != null)
// assertTrue(result2?.server == "127.0.0.1")
// assertTrue(result2?.obfsPassword == "obfs2")
// assertTrue(result2?.security == "tls")
//
// var url22 = Hysteria2Fmt.toUri(result2!!)
// assertTrue(url22.contains("obfs2"))
// }
//
// @Test
// fun test_fmtSsParse() {
// val url2 = "ss://aa:bb@127.0.0.1:10000#sss"
// var result2 = ShadowsocksFmt.parse(url2)
// assertTrue(result2 != null)
// assertTrue(result2?.server == "127.0.0.1")
//
// var result = ShadowsocksFmt.parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQy@127.0.0.1:10000#sss")
// assertTrue(result != null)
// assertTrue(result?.server == "127.0.0.1")
// }
//
// @Test
// fun test_fmtSocksParse() {
// val url2 = "socks://Og%3D%3D@127.0.0.1:1000#socks2"
// var result2 = SocksFmt.parse(url2)
// assertTrue(result2 != null)
// assertTrue(result2?.server == "127.0.0.1")
// var url22 = SocksFmt.toUri(result2!!)
// assertTrue(url2.contains(url22))
//
// var result = SocksFmt.parse("socks://dXNlcjpwYXNz@127.0.0.1:1000#socks2")
// assertTrue(result != null)
// assertTrue(result?.server == "127.0.0.1")
// }
//
// @Test
// fun test_fmtTrojanParse() {
// val url2 = "trojan://password2@127.0.0.1:443?flow=xtls-rprx-vision&security=tls&type=tcp&headerType=none#Trojan"
// var result2 = TrojanFmt.parse(url2)
// assertTrue(result2 != null)
// assertTrue(result2?.server == "127.0.0.1")
// assertTrue(result2?.flow == "xtls-rprx-vision")
//
// val url = "trojan://password2@127.0.0.1:443#Trojan"
// var result = TrojanFmt.parse(url)
// assertTrue(result != null)
// assertTrue(result?.server == "127.0.0.1")
// assertTrue(result?.security == "tls")
//
//
// }
//
// @Test
// fun test_fmtVlessParse() {
// val url2 =
// "vless://cae1dc39-0547-4b1d-9e7a-01132c7ae3a7@127.0.0.1:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=sni2&fp=chrome&pbk=publickkey&sid=123456&spx=%2F&type=ws&host=host2&path=path2#VLESS"
// var result2 = VlessFmt.parse(url2)
// assertTrue(result2 != null)
// assertTrue(result2?.server == "127.0.0.1")
// assertTrue(result2?.flow == "xtls-rprx-vision")
//
//
// var url22 = VlessFmt.toUri(result2!!)
// assertTrue(url22.contains("xtls-rprx-vision"))
//
// }
//
// @Test
// fun test_fmtVmessParse() {
// val url2 =
// "vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogIlZtZXNzIiwNCiAgImFkZCI6ICIxMjcuMC4wLjEiLA0KICAicG9ydCI6ICIxMDAwMCIsDQogICJpZCI6ICJlYmI5MWM5OS1lZjA3LTRmZjUtOThhYS01OTAyYWI0ZDAyODYiLA0KICAiYWlkIjogIjEyMyIsDQogICJzY3kiOiAiYWVzLTEyOC1nY20iLA0KICAibmV0IjogInRjcCIsDQogICJ0eXBlIjogIm5vbmUiLA0KICAiaG9zdCI6ICJob3N0MiIsDQogICJwYXRoIjogInBhdGgyIiwNCiAgInRscyI6ICIiLA0KICAic25pIjogIiIsDQogICJhbHBuIjogIiINCn0="
// var result2 = VmessFmt.parse(url2)
// assertTrue(result2 != null)
// assertTrue(result2?.server == "127.0.0.1")
// assertTrue(result2?.method == "aes-128-gcm")
//
// }
//
//
// @Test
// fun test_fmtWireguardParse() {
// val url2 = "wireguard://privatekey2@127.0.0.1:2000?publickey=publickey2&reserved=2%2C2%2C3&address=127.0.0.127&mtu=1250#WGG"
// var result2 = WireguardFmt.parse(url2)
// assertTrue(result2 != null)
// assertTrue(result2?.server == "127.0.0.1")
// assertTrue(result2?.publicKey == "publickey2")
// assertTrue(result2?.localAddress == "127.0.0.127")
//
//
// var url22 = WireguardFmt.toUri(result2!!)
// assertTrue(url22.contains("publickey2"))
// }
} }

View File

@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "8.4.2" apply false alias(libs.plugins.android.application) apply false
id("com.android.library") version "8.4.2" apply false alias(libs.plugins.android.library) apply false
id("org.jetbrains.kotlin.android") version "1.9.23" apply false alias(libs.plugins.android.kotlin) apply false
} }

View File

@@ -1,18 +1,18 @@
[versions] [versions]
activityKtx = "1.9.2" activityKtx = "1.9.3"
appcompat = "1.7.0" appcompat = "1.7.0"
cardview = "1.0.0" cardview = "1.0.0"
constraintlayout = "2.1.4" constraintlayout = "2.1.4"
core = "3.5.3" core = "3.5.3"
editorkit = "2.9.0" editorkit = "2.9.0"
flexbox = "3.0.0" flexbox = "3.0.0"
fragmentKtx = "1.8.3" fragmentKtx = "1.8.4"
gson = "2.11.0" gson = "2.11.0"
junit = "4.13.2" junit = "4.13.2"
kotlinReflect = "2.0.20" kotlinReflect = "2.0.21"
kotlinxCoroutinesCore = "1.9.0" kotlinxCoroutinesCore = "1.9.0"
legacySupportV4 = "1.0.0" legacySupportV4 = "1.0.0"
lifecycleViewmodelKtx = "2.8.5" lifecycleViewmodelKtx = "2.8.6"
material = "1.12.0" material = "1.12.0"
mmkvStatic = "1.3.9" mmkvStatic = "1.3.9"
multidex = "2.0.1" multidex = "2.0.1"
@@ -25,6 +25,9 @@ rxpermissions = "0.12"
toastcompat = "1.1.0" toastcompat = "1.1.0"
viewpager2 = "1.1.0" viewpager2 = "1.1.0"
workRuntimeKtx = "2.9.1" workRuntimeKtx = "2.9.1"
androidGradlePlugin = "8.7.1"
androidKotlinPlugin = "2.0.21"
mockitoMockitoInline = "4.0.0"
[libraries] [libraries]
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
@@ -59,5 +62,10 @@ toastcompat = { module = "me.drakeet.support:toastcompat", version.ref = "toastc
viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
work-multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "workRuntimeKtx" } work-multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "workRuntimeKtx" }
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoMockitoInline" }
org-mockito-mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockitoMockitoInline" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
android-kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "androidKotlinPlugin" }