Compare commits

...

149 Commits

Author SHA1 Message Date
2dust f252d1395a up 1.9.19 2024-11-21 20:30:35 +08:00
2dust 2dc0472c69 When creating a new routing rule, add it to the top 2024-11-21 20:24:01 +08:00
hosêyň abāspanā 3e09adc4d1 Improved Luri Bakhtiari Translation (#4003) 2024-11-21 09:26:08 +08:00
phoenix6936 11750b9382 Improved Persian translation (#4001)
* Improved Persian translation

Improved Persian translation

* Improved Persian translation

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

* Import rulesets from qrcode

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

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

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

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

Update persian translate

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

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

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

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

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

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

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

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

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

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

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

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

Update persian translate

* Update persian translate 

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

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

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

* Refactor to use NetworkType enum in VmessFmt

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

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

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

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

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

* Update dependabot.yml

* Update libs.versions.toml

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

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

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

Add udp type":"base64",

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

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

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

Update some things

* Update V2rayConfigManager.kt

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

Update Persian translation

* Update Persian translation

Update Persian translation

* Update Persian translation

Update Persian translation

* Update Persian translation

Update Persian translation

* Update Persian translation

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

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

* Update AngConfigManager.kt

* Update AngConfigManager.kt

* Update AngConfigManager.kt
2024-10-29 13:37:34 +08:00
DecorativeFamily beceaba44d Update build.yml (#3804)
Fix restore cache failed
2024-10-29 13:36:00 +08:00
solokot f5987d9767 Update Russian translation (#3802) 2024-10-29 13:35:35 +08:00
2dust 616712b338 Rename ProfileItem to ProfileLiteItem 2024-10-28 15:01:15 +08:00
2dust 076a968476 targetSdk = 35 2024-10-28 14:33:30 +08:00
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
157 changed files with 4168 additions and 2694 deletions
+38 -8
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
+1 -1
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)
+63 -47
View File
@@ -1,19 +1,20 @@
plugins { plugins {
id("com.android.application") alias(libs.plugins.android.application)
id("org.jetbrains.kotlin.android") alias(libs.plugins.kotlin.android)
} }
android { android {
namespace = "com.v2ray.ang" namespace = "com.v2ray.ang"
compileSdk = 34 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 35
versionCode = 601 versionCode = 615
versionName = "1.9.7" versionName = "1.9.19"
multiDexEnabled = true multiDexEnabled = true
splits { splits {
abi { abi {
isEnable = true isEnable = true
@@ -27,20 +28,16 @@ android {
} }
} }
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles(
} getDefaultProguardFile("proguard-android-optimize.txt"),
debug { "proguard-rules.pro"
isMinifyEnabled = false )
} }
} }
@@ -50,8 +47,13 @@ android {
} }
} }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_11.toString()
} }
applicationVariants.all { applicationVariants.all {
@@ -69,7 +71,8 @@ android {
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk" output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if (versionCodes.containsKey(abi)) { if (versionCodes.containsKey(abi)) {
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode) output.versionCodeOverride =
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} else { } else {
return@forEach return@forEach
} }
@@ -86,47 +89,60 @@ android {
useLegacyPackaging = true useLegacyPackaging = true
} }
} }
} }
dependencies { dependencies {
// Core Libraries
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)
implementation(libs.flexbox) // AndroidX Core Libraries
// Androidx implementation(libs.androidx.core.ktx)
implementation(libs.constraintlayout) implementation(libs.androidx.appcompat)
implementation(libs.legacy.support.v4) implementation(libs.androidx.activity)
implementation(libs.appcompat) implementation(libs.androidx.constraintlayout)
implementation(libs.material)
implementation(libs.cardview)
implementation(libs.preference.ktx) implementation(libs.preference.ktx)
implementation(libs.recyclerview) implementation(libs.recyclerview)
implementation(libs.fragment.ktx)
implementation(libs.multidex)
implementation(libs.viewpager2)
// Androidx ktx // UI Libraries
implementation(libs.activity.ktx) implementation(libs.material)
implementation(libs.toastcompat)
implementation(libs.editorkit)
implementation(libs.flexbox)
// Data and Storage Libraries
implementation(libs.mmkv.static)
implementation(libs.gson)
// Reactive and Utility Libraries
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxpermissions)
// Language and Processing Libraries
implementation(libs.language.base)
implementation(libs.language.json)
// Intent and Utility Libraries
implementation(libs.quickie.bundled)
implementation(libs.core)
// AndroidX Lifecycle and Architecture Components
implementation(libs.lifecycle.viewmodel.ktx) implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.livedata.ktx) implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.runtime.ktx) implementation(libs.lifecycle.runtime.ktx)
//kotlin // Background Task Libraries
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.mmkv.static)
implementation(libs.gson)
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxpermissions)
implementation(libs.toastcompat)
implementation(libs.editorkit)
implementation(libs.language.base)
implementation(libs.language.json)
implementation(libs.quickie.bundled)
implementation(libs.core)
implementation(libs.work.runtime.ktx) implementation(libs.work.runtime.ktx)
implementation(libs.work.multiprocess) implementation(libs.work.multiprocess)
}
// Multidex Support
implementation(libs.multidex)
// Testing Libraries
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
testImplementation(libs.org.mockito.mockito.inline)
testImplementation(libs.mockito.kotlin)
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+3 -3
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>
@@ -42,21 +42,89 @@
] ]
}, },
{ {
"remarks": "代理GFW", "remarks": "代理海外公共DNSIP",
"outboundTag": "proxy", "outboundTag": "proxy",
"domain": [ "ip": [
"geosite:gfw", "1.1.1.1",
"geosite:greatfire" "1.0.0.1",
"2606:4700:4700::1111",
"2606:4700:4700::1001",
"1.1.1.2",
"1.0.0.2",
"2606:4700:4700::1112",
"2606:4700:4700::1002",
"1.1.1.3",
"1.0.0.3",
"2606:4700:4700::1113",
"2606:4700:4700::1003",
"8.8.8.8",
"8.8.4.4",
"2001:4860:4860::8888",
"2001:4860:4860::8844",
"94.140.14.14",
"94.140.15.15",
"2a10:50c0::ad1:ff",
"2a10:50c0::ad2:ff",
"94.140.14.15",
"94.140.15.16",
"2a10:50c0::bad1:ff",
"2a10:50c0::bad2:ff",
"94.140.14.140",
"94.140.14.141",
"2a10:50c0::1:ff",
"2a10:50c0::2:ff",
"208.67.222.222",
"208.67.220.220",
"2620:119:35::35",
"2620:119:53::53",
"208.67.222.123",
"208.67.220.123",
"2620:119:35::123",
"2620:119:53::123",
"9.9.9.9",
"149.112.112.112",
"2620:fe::9",
"2620:fe::fe",
"9.9.9.11",
"149.112.112.11",
"2620:fe::11",
"2620:fe::fe:11",
"9.9.9.10",
"149.112.112.10",
"2620:fe::10",
"2620:fe::fe:10",
"77.88.8.8",
"77.88.8.1",
"2a02:6b8::feed:0ff",
"2a02:6b8:0:1::feed:0ff",
"77.88.8.88",
"77.88.8.2",
"2a02:6b8::feed:bad",
"2a02:6b8:0:1::feed:bad",
"77.88.8.7",
"77.88.8.3",
"2a02:6b8::feed:a11",
"2a02:6b8:0:1::feed:a11"
] ]
}, },
{ {
"remarks": "代理Google等", "remarks": "代理海外公共DNS域名",
"outboundTag": "proxy",
"domain": [
"domain:cloudflare-dns.com",
"domain:one.one.one.one",
"domain:dns.google",
"domain:adguard-dns.com",
"domain:opendns.com",
"domain:umbrella.com",
"domain:quad9.net",
"domain:yandex.net"
]
},
{
"remarks": "代理IP",
"outboundTag": "proxy", "outboundTag": "proxy",
"ip": [ "ip": [
"1.0.0.1",
"1.1.1.1",
"8.8.8.8",
"8.8.4.4",
"geoip:facebook", "geoip:facebook",
"geoip:fastly", "geoip:fastly",
"geoip:google", "geoip:google",
@@ -65,6 +133,14 @@
"geoip:twitter" "geoip:twitter"
] ]
}, },
{
"remarks": "代理GFW",
"outboundTag": "proxy",
"domain": [
"geosite:gfw",
"geosite:greatfire"
]
},
{ {
"remarks": "最终直连", "remarks": "最终直连",
"port": "0-65535", "port": "0-65535",
@@ -35,48 +35,74 @@
] ]
}, },
{ {
"remarks": "绕过中国域名", "remarks": "绕过中国公共DNSIP",
"outboundTag": "direct",
"ip": [
"223.5.5.5",
"223.6.6.6",
"2400:3200::1",
"2400:3200:baba::1",
"119.29.29.29",
"1.12.12.12",
"120.53.53.53",
"2402:4e00::",
"2402:4e00:1::",
"180.76.76.76",
"2400:da00::6666",
"114.114.114.114",
"114.114.115.115",
"114.114.114.119",
"114.114.115.119",
"114.114.114.110",
"114.114.115.110",
"180.184.1.1",
"180.184.2.2",
"101.226.4.6",
"218.30.118.6",
"123.125.81.6",
"140.207.198.6",
"1.2.4.8",
"210.2.4.8",
"52.80.66.66",
"117.50.22.22",
"2400:7fc0:849e:200::4",
"2404:c2c0:85d8:901::4",
"117.50.10.10",
"52.80.52.52",
"2400:7fc0:849e:200::8",
"2404:c2c0:85d8:901::8",
"117.50.60.30",
"52.80.60.30"
]
},
{
"remarks": "绕过中国公共DNS域名",
"outboundTag": "direct", "outboundTag": "direct",
"domain": [ "domain": [
"domain:dns.alidns.com", "domain:alidns.com",
"domain:dns.pub",
"domain:doh.pub", "domain:doh.pub",
"domain:dot.pub", "domain:dot.pub",
"domain:doh.360.cn", "domain:360.cn",
"domain:dot.360.cn", "domain:onedns.net"
"geosite:cn",
"geosite:geolocation-cn"
] ]
}, },
{ {
"remarks": "绕过中国IP", "remarks": "绕过中国IP",
"outboundTag": "direct", "outboundTag": "direct",
"ip": [ "ip": [
"223.5.5.5/32",
"223.6.6.6/32",
"2400:3200::1/128",
"2400:3200:baba::1/128",
"119.29.29.29/32",
"1.12.12.12/32",
"120.53.53.53/32",
"2402:4e00::/128",
"2402:4e00:1::/128",
"180.76.76.76/32",
"2400:da00::6666/128",
"114.114.114.114/32",
"114.114.115.115/32",
"180.184.1.1/32",
"180.184.2.2/32",
"101.226.4.6/32",
"218.30.118.6/32",
"123.125.81.6/32",
"140.207.198.6/32",
"geoip:cn" "geoip:cn"
] ]
}, },
{
"remarks": "绕过中国域名",
"outboundTag": "direct",
"domain": [
"geosite:cn"
]
},
{ {
"remarks": "最终代理", "remarks": "最终代理",
"port": "0-65535", "port": "0-65535",
"outboundTag": "proxy" "outboundTag": "proxy"
} }
] ]
@@ -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"
}
]
@@ -7,7 +7,8 @@ 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.AppConfig.ANG_PACKAGE
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
class AngApplication : MultiDexApplication() { class AngApplication : MultiDexApplication() {
@@ -22,7 +23,7 @@ class AngApplication : MultiDexApplication() {
} }
private val workManagerConfiguration: Configuration = Configuration.Builder() private val workManagerConfiguration: Configuration = Configuration.Builder()
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg") .setDefaultProcessName("${ANG_PACKAGE}:bg")
.build() .build()
override fun onCreate() { override fun onCreate() {
@@ -37,7 +38,7 @@ class AngApplication : MultiDexApplication() {
MMKV.initialize(this) MMKV.initialize(this)
Utils.setNightMode(application) Utils.setNightMode()
// Initialize WorkManager with the custom configuration // Initialize WorkManager with the custom configuration
WorkManager.initialize(this, workManagerConfiguration) WorkManager.initialize(this, workManagerConfiguration)
@@ -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,32 @@ 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_DNSPOD_DOMAIN = "dot.pub"
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
const val DNS_GOOGLE_DOMAIN = "dns.google"
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
const val DEFAULT_PORT = 443
const val DEFAULT_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"
} }
@@ -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 = "",
@@ -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 }
} }
} }
@@ -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?,
)
}
} }
@@ -0,0 +1,19 @@
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"),
BAKHTIARI("bqi-rIR");
companion object {
fun fromCode(code: String): Language {
return entries.find { it.code == code } ?: AUTO
}
}
}
@@ -0,0 +1,18 @@
package com.v2ray.ang.dto
enum class NetworkType(val type: String) {
TCP("tcp"),
KCP("kcp"),
WS("ws"),
HTTP_UPGRADE("httpupgrade"),
SPLIT_HTTP("splithttp"),
XHTTP("xhttp"),
HTTP("http"),
H2("h2"),
//QUIC("quic"),
GRPC("grpc");
companion object {
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
}
}
@@ -0,0 +1,113 @@
package com.v2ray.ang.dto
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.util.Utils
data class ProfileItem(
val configVersion: Int = 4,
val configType: EConfigType,
var subscriptionId: String = "",
var addedTime: Long = System.currentTimeMillis(),
var remarks: String = "",
var server: String? = null,
var serverPort: String? = null,
var password: String? = null,
var method: String? = null,
var flow: String? = null,
var username: String? = null,
var network: String? = null,
var headerType: String? = null,
var host: String? = null,
var path: String? = null,
var seed: String? = null,
var quicSecurity: String? = null,
var quicKey: String? = null,
var mode: String? = null,
var serviceName: String? = null,
var authority: String? = null,
var xhttpMode: String? = null,
var xhttpExtra: String? = null,
var security: String? = null,
var sni: String? = null,
var alpn: String? = null,
var fingerPrint: String? = null,
var insecure: Boolean? = null,
var publicKey: String? = null,
var shortId: String? = null,
var spiderX: String? = null,
var secretKey: String? = null,
var preSharedKey: String? = null,
var localAddress: String? = null,
var reserved: String? = null,
var mtu: Int? = null,
var obfsPassword: String? = null,
var portHopping: String? = null,
var portHoppingInterval: String? = null,
var pinSHA256: String? = null,
) {
companion object {
fun create(configType: EConfigType): ProfileItem {
return ProfileItem(configType = configType)
}
}
fun getAllOutboundTags(): MutableList<String> {
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
}
fun getServerAddressAndPort(): String {
return Utils.getIpv6Address(server) + ":" + serverPort
}
override fun equals(other: Any?): Boolean {
if (other == null) return false
val obj = other as ProfileItem
return (this.server == obj.server
&& this.serverPort == obj.serverPort
&& this.password == obj.password
&& this.method == obj.method
&& this.flow == obj.flow
&& this.username == obj.username
&& this.network == obj.network
&& this.headerType == obj.headerType
&& this.host == obj.host
&& this.path == obj.path
&& this.seed == obj.seed
&& this.quicSecurity == obj.quicSecurity
&& this.quicKey == obj.quicKey
&& this.mode == obj.mode
&& this.serviceName == obj.serviceName
&& this.authority == obj.authority
&& this.xhttpMode == obj.xhttpMode
&& this.security == obj.security
&& this.sni == obj.sni
&& this.alpn == obj.alpn
&& this.fingerPrint == obj.fingerPrint
&& this.publicKey == obj.publicKey
&& this.shortId == obj.shortId
&& this.secretKey == obj.secretKey
&& this.localAddress == obj.localAddress
&& this.reserved == obj.reserved
&& this.mtu == obj.mtu
&& this.obfsPassword == obj.obfsPassword
&& this.portHopping == obj.portHopping
&& this.portHoppingInterval == obj.portHoppingInterval
&& this.pinSHA256 == obj.pinSHA256
)
}
}
@@ -1,6 +1,6 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
data class ProfileItem( data class ProfileLiteItem(
val configType: EConfigType, val configType: EConfigType,
var subscriptionId: String = "", var subscriptionId: String = "",
var remarks: String = "", var remarks: String = "",
@@ -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
}
}
}
}
@@ -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
) )
} }
@@ -159,18 +195,19 @@ data class V2rayConfig(
data class WireGuardBean( data class WireGuardBean(
var publicKey: String = "", var publicKey: String = "",
var preSharedKey: String = "",
var endpoint: String = "" var endpoint: String = ""
) )
} }
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,
var wsSettings: WsSettingsBean? = null, var wsSettings: WsSettingsBean? = null,
var httpupgradeSettings: HttpupgradeSettingsBean? = null, var httpupgradeSettings: HttpupgradeSettingsBean? = null,
var splithttpSettings: SplithttpSettingsBean? = null, var xhttpSettings: XhttpSettingsBean? = null,
var httpSettings: HttpSettingsBean? = null, var httpSettings: HttpSettingsBean? = null,
var tlsSettings: TlsSettingsBean? = null, var tlsSettings: TlsSettingsBean? = null,
var quicSettings: QuicSettingBean? = null, var quicSettings: QuicSettingBean? = null,
@@ -224,7 +261,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 +271,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 XhttpSettingsBean(
var path: String = "", var path: String? = null,
var host: String = "", var host: String? = null,
val maxUploadSize: Int? = null, var mode: String? = null,
val maxConcurrentUploads: Int? = null var extra: Any? = 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 +299,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,
@@ -308,21 +345,28 @@ data class V2rayConfig(
} }
fun populateTransportSettings( fun populateTransportSettings(
transport: String, headerType: String?, host: String?, path: String?, seed: String?, transport: String,
quicSecurity: String?, key: String?, mode: String?, serviceName: String?, headerType: String?,
host: String?,
path: String?,
seed: 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" -> { NetworkType.TCP.type -> {
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
} }
@@ -333,7 +377,7 @@ data class V2rayConfig(
tcpSettings = tcpSetting tcpSettings = tcpSetting
} }
"kcp" -> { NetworkType.KCP.type -> {
val kcpsetting = KcpSettingsBean() val kcpsetting = KcpSettingsBean()
kcpsetting.header.type = headerType ?: "none" kcpsetting.header.type = headerType ?: "none"
if (seed.isNullOrEmpty()) { if (seed.isNullOrEmpty()) {
@@ -344,7 +388,7 @@ data class V2rayConfig(
kcpSettings = kcpsetting kcpSettings = kcpsetting
} }
"ws" -> { NetworkType.WS.type -> {
val wssetting = WsSettingsBean() val wssetting = WsSettingsBean()
wssetting.headers.Host = host.orEmpty() wssetting.headers.Host = host.orEmpty()
sni = wssetting.headers.Host sni = wssetting.headers.Host
@@ -352,7 +396,7 @@ data class V2rayConfig(
wsSettings = wssetting wsSettings = wssetting
} }
"httpupgrade" -> { NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = HttpupgradeSettingsBean() val httpupgradeSetting = HttpupgradeSettingsBean()
httpupgradeSetting.host = host.orEmpty() httpupgradeSetting.host = host.orEmpty()
sni = httpupgradeSetting.host sni = httpupgradeSetting.host
@@ -360,32 +404,32 @@ data class V2rayConfig(
httpupgradeSettings = httpupgradeSetting httpupgradeSettings = httpupgradeSetting
} }
"splithttp" -> { NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
val splithttpSetting = SplithttpSettingsBean() val xhttpSetting = XhttpSettingsBean()
splithttpSetting.host = host.orEmpty() xhttpSetting.host = host.orEmpty()
sni = splithttpSetting.host sni = xhttpSetting.host
splithttpSetting.path = path ?: "/" xhttpSetting.path = path ?: "/"
splithttpSettings = splithttpSetting xhttpSettings = xhttpSetting
} }
"h2", "http" -> { NetworkType.H2.type, NetworkType.HTTP.type -> {
network = "h2" network = NetworkType.H2.type
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
} }
"quic" -> { // "quic" -> {
val quicsetting = QuicSettingBean() // val quicsetting = QuicSettingBean()
quicsetting.security = quicSecurity ?: "none" // quicsetting.security = quicSecurity ?: "none"
quicsetting.key = key.orEmpty() // quicsetting.key = key.orEmpty()
quicsetting.header.type = headerType ?: "none" // quicsetting.header.type = headerType ?: "none"
quicSettings = quicsetting // quicSettings = quicsetting
} // }
"grpc" -> { NetworkType.GRPC.type -> {
val grpcSetting = GrpcSettingsBean() val grpcSetting = GrpcSettingsBean()
grpcSetting.multiMode = mode == "multi" grpcSetting.multiMode = mode == "multi"
grpcSetting.serviceName = serviceName.orEmpty() grpcSetting.serviceName = serviceName.orEmpty()
@@ -400,8 +444,14 @@ data class V2rayConfig(
} }
fun populateTlsSettings( fun populateTlsSettings(
streamSecurity: String, allowInsecure: Boolean, sni: String, fingerprint: String?, alpns: String?, streamSecurity: String,
publicKey: String?, shortId: String?, spiderX: String? allowInsecure: Boolean,
sni: String?,
fingerprint: String?,
alpns: String?,
publicKey: String?,
shortId: String?,
spiderX: String?
) { ) {
security = streamSecurity security = streamSecurity
val tlsSetting = TlsSettingsBean( val tlsSetting = TlsSettingsBean(
@@ -413,10 +463,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
} }
@@ -434,16 +484,16 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true) || protocol.equals(EConfigType.VLESS.name, true)
) { ) {
return settings?.vnext?.get(0)?.address return settings?.vnext?.first()?.address
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true) || protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true) || protocol.equals(EConfigType.HTTP.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true) || protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true) || protocol.equals(EConfigType.HYSTERIA2.name, true)
) { ) {
return settings?.servers?.get(0)?.address return settings?.servers?.first()?.address
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.get(0)?.endpoint?.substringBeforeLast(":") return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
} }
return null return null
} }
@@ -452,16 +502,16 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true) || protocol.equals(EConfigType.VLESS.name, true)
) { ) {
return settings?.vnext?.get(0)?.port return settings?.vnext?.first()?.port
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true) || protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true) || protocol.equals(EConfigType.HTTP.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true) || protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true) || protocol.equals(EConfigType.HYSTERIA2.name, true)
) { ) {
return settings?.servers?.get(0)?.port return settings?.servers?.first()?.port
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.get(0)?.endpoint?.substringAfterLast(":")?.toInt() return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
} }
return null return null
} }
@@ -476,16 +526,16 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true) || protocol.equals(EConfigType.VLESS.name, true)
) { ) {
return settings?.vnext?.get(0)?.users?.get(0)?.id return settings?.vnext?.first()?.users?.first()?.id
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true) } else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true) || protocol.equals(EConfigType.TROJAN.name, true)
|| protocol.equals(EConfigType.HYSTERIA2.name, true) || protocol.equals(EConfigType.HYSTERIA2.name, true)
) { ) {
return settings?.servers?.get(0)?.password return settings?.servers?.first()?.password
} else if (protocol.equals(EConfigType.SOCKS.name, true) } else if (protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true) || protocol.equals(EConfigType.HTTP.name, true)
) { ) {
return settings?.servers?.get(0)?.users?.get(0)?.pass return settings?.servers?.first()?.users?.first()?.pass
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) { } else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.secretKey return settings?.secretKey
} }
@@ -494,14 +544,14 @@ data class V2rayConfig(
fun getSecurityEncryption(): String? { fun getSecurityEncryption(): String? {
return when { return when {
protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.security protocol.equals(EConfigType.VMESS.name, true) -> settings?.vnext?.first()?.users?.first()?.security
protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.get(0)?.users?.get(0)?.encryption protocol.equals(EConfigType.VLESS.name, true) -> settings?.vnext?.first()?.users?.first()?.encryption
protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.get(0)?.method protocol.equals(EConfigType.SHADOWSOCKS.name, true) -> settings?.servers?.first()?.method
else -> null else -> null
} }
} }
fun getTransportSettingDetails(): List<String>? { fun getTransportSettingDetails(): List<String?>? {
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true) || protocol.equals(EConfigType.VLESS.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true) || protocol.equals(EConfigType.TROJAN.name, true)
@@ -509,7 +559,7 @@ data class V2rayConfig(
) { ) {
val transport = streamSettings?.network ?: return null val transport = streamSettings?.network ?: return null
return when (transport) { return when (transport) {
"tcp" -> { NetworkType.TCP.type -> {
val tcpSetting = streamSettings?.tcpSettings ?: return null val tcpSetting = streamSettings?.tcpSettings ?: return null
listOf( listOf(
tcpSetting.header.type, tcpSetting.header.type,
@@ -518,7 +568,7 @@ data class V2rayConfig(
) )
} }
"kcp" -> { NetworkType.KCP.type -> {
val kcpSetting = streamSettings?.kcpSettings ?: return null val kcpSetting = streamSettings?.kcpSettings ?: return null
listOf( listOf(
kcpSetting.header.type, kcpSetting.header.type,
@@ -527,7 +577,7 @@ data class V2rayConfig(
) )
} }
"ws" -> { NetworkType.WS.type -> {
val wsSetting = streamSettings?.wsSettings ?: return null val wsSetting = streamSettings?.wsSettings ?: return null
listOf( listOf(
"", "",
@@ -536,7 +586,7 @@ data class V2rayConfig(
) )
} }
"httpupgrade" -> { NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
listOf( listOf(
"", "",
@@ -545,16 +595,16 @@ data class V2rayConfig(
) )
} }
"splithttp" -> { NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
val splithttpSetting = streamSettings?.splithttpSettings ?: return null val xhttpSettings = streamSettings?.xhttpSettings ?: return null
listOf( listOf(
"", "",
splithttpSetting.host, xhttpSettings.host,
splithttpSetting.path xhttpSettings.path
) )
} }
"h2" -> { NetworkType.H2.type -> {
val h2Setting = streamSettings?.httpSettings ?: return null val h2Setting = streamSettings?.httpSettings ?: return null
listOf( listOf(
"", "",
@@ -563,16 +613,16 @@ data class V2rayConfig(
) )
} }
"quic" -> { // "quic" -> {
val quicSetting = streamSettings?.quicSettings ?: return null // val quicSetting = streamSettings?.quicSettings ?: return null
listOf( // listOf(
quicSetting.header.type, // quicSetting.header.type,
quicSetting.security, // quicSetting.security,
quicSetting.key // quicSetting.key
) // )
} // }
"grpc" -> { NetworkType.GRPC.type -> {
val grpcSetting = streamSettings?.grpcSettings ?: return null val grpcSetting = streamSettings?.grpcSettings ?: return null
listOf( listOf(
if (grpcSetting.multiMode == true) "multi" else "gun", if (grpcSetting.multiMode == true) "multi" else "gun",
@@ -601,7 +651,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,
) )
} }
@@ -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())
@@ -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
}
}
@@ -0,0 +1,123 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.Utils
import java.net.URI
open class FmtBase {
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
val query = if (dicQuery != null)
("?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
else ""
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(userInfo ?: ""),
Utils.getIpv6Address(config.server),
config.serverPort
)
return "${url}${query}#${Utils.urlEncode(config.remarks)}"
}
fun getQueryParam(uri: URI): Map<String, String> {
return uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
}
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
config.network = queryParam["type"] ?: NetworkType.TCP.type
//TODO
if (config.network == NetworkType.SPLIT_HTTP.type) config.network = NetworkType.XHTTP.type
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.xhttpMode = queryParam["mode"]
config.xhttpExtra = queryParam["extra"]
config.security = queryParam["security"]
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["allowInsecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
config.publicKey = queryParam["pbk"]
config.shortId = queryParam["sid"]
config.spiderX = queryParam["spx"]
config.flow = queryParam["flow"]
}
fun getQueryDic(config: ProfileItem): HashMap<String, String> {
val dicQuery = HashMap<String, String>()
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
config.fingerPrint.let { if (it.isNotNullEmpty()) dicQuery["fp"] = it.orEmpty() }
config.publicKey.let { if (it.isNotNullEmpty()) dicQuery["pbk"] = it.orEmpty() }
config.shortId.let { if (it.isNotNullEmpty()) dicQuery["sid"] = it.orEmpty() }
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
val networkType = NetworkType.fromString(config.network)
dicQuery["type"] = networkType.type
when (networkType) {
NetworkType.TCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
}
NetworkType.KCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
}
NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
NetworkType.SPLIT_HTTP, NetworkType.XHTTP -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() }
}
NetworkType.HTTP, NetworkType.H2 -> {
dicQuery["type"] = "http"
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
}
// NetworkType.QUIC -> {
// dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
// config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
// config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
// }
NetworkType.GRPC -> {
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }
}
}
return dicQuery
}
}
@@ -0,0 +1,28 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.isNotNullEmpty
import kotlin.text.orEmpty
object HttpFmt : FmtBase() {
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HTTP)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = profileItem.username.orEmpty()
socksUsersBean.pass = profileItem.password.orEmpty()
server.users = listOf(socksUsersBean)
}
}
return outboundBean
}
}
@@ -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
}
}
@@ -0,0 +1,109 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
return parseSip002(str) ?: parseLegacy(str)
}
fun parseSip002(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
if (uri.userInfo.isNullOrEmpty()) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
val result = if (uri.userInfo.contains(":")) {
uri.userInfo.split(":", limit = 2)
} else {
Utils.decode(uri.userInfo).split(":", limit = 2)
}
if (result.count() == 2) {
config.method = result.first()
config.password = result.last()
}
if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri)
if (queryParam["plugin"] == "obfs-local" && queryParam["obfs"] == "http") {
config.network = NetworkType.TCP.type
config.headerType = "http"
config.host = queryParam["obfs-host"]
config.path = queryParam["path"]
}
}
return config
}
fun parseLegacy(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result) ?: return null
config.server = match.groupValues[3].removeSurrounding("[", "]")
config.serverPort = match.groupValues[4]
config.password = match.groupValues[2]
config.method = match.groupValues[1].lowercase()
return config
}
fun toUri(config: ProfileItem): String {
val pw = "${config.method}:${config.password}"
return toUri(config, Utils.encode(pw), null)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.method = profileItem.method
}
return outboundBean
}
}
@@ -0,0 +1,62 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object SocksFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SOCKS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
if (uri.userInfo?.isEmpty() == false) {
val result = Utils.decode(uri.userInfo).split(":", limit = 2)
if (result.count() == 2) {
config.username = result.first()
config.password = result.last()
}
}
return config
}
fun toUri(config: ProfileItem): String {
val pw =
if (config.username.isNotNullEmpty())
"${config.username}:${config.password}"
else
":"
return toUri(config, Utils.encode(pw), null)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = profileItem.username.orEmpty()
socksUsersBean.pass = profileItem.password.orEmpty()
server.users = listOf(socksUsersBean)
}
}
return outboundBean
}
}
@@ -0,0 +1,81 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object TrojanFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
if (uri.rawQuery.isNullOrEmpty()) {
config.network = NetworkType.TCP.type
config.security = AppConfig.TLS
config.insecure = allowInsecure
} else {
val queryParam = getQueryParam(uri)
getItemFormQuery(config, queryParam, allowInsecure)
config.security = queryParam["security"] ?: AppConfig.TLS
}
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
return toUri(config, config.password, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.flow = profileItem.flow
}
outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}
@@ -0,0 +1,83 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = queryParam["encryption"] ?: "none"
getItemFormQuery(config, queryParam, allowInsecure)
return config
}
fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config)
dicQuery["encryption"] = config.method ?: "none"
return toUri(config, config.password, dicQuery)
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VLESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].encryption = profileItem.method
vnext.users[0].flow = profileItem.flow
}
outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}
@@ -0,0 +1,183 @@
package com.v2ray.ang.fmt
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object VmessFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str)
}
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
return null
}
config.remarks = vmessQRCode.ps
config.server = vmessQRCode.add
config.serverPort = vmessQRCode.port
config.password = vmessQRCode.id
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
config.network = vmessQRCode.net ?: NetworkType.TCP.type
config.headerType = vmessQRCode.type
config.host = vmessQRCode.host
config.path = vmessQRCode.path
when (NetworkType.fromString(config.network)) {
NetworkType.KCP -> {
config.seed = vmessQRCode.path
}
// NetworkType.QUIC -> {
// config.quicSecurity = vmessQRCode.host
// config.quicKey = vmessQRCode.path
// }
NetworkType.GRPC -> {
config.mode = vmessQRCode.type
config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host
}
else -> {}
}
config.security = vmessQRCode.tls
config.insecure = allowInsecure
config.sni = vmessQRCode.sni
config.fingerPrint = vmessQRCode.fp
config.alpn = vmessQRCode.alpn
return config
}
fun toUri(config: ProfileItem): String {
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = config.server.orEmpty()
vmessQRCode.port = config.serverPort.orEmpty()
vmessQRCode.id = config.password.orEmpty()
vmessQRCode.scy = config.method.orEmpty()
vmessQRCode.aid = "0"
vmessQRCode.net = config.network.orEmpty()
vmessQRCode.type = config.headerType.orEmpty()
when (NetworkType.fromString(config.network)) {
NetworkType.KCP -> {
vmessQRCode.path = config.seed.orEmpty()
}
// NetworkType.QUIC -> {
// vmessQRCode.host = config.quicSecurity.orEmpty()
// vmessQRCode.path = config.quicKey.orEmpty()
// }
NetworkType.GRPC -> {
vmessQRCode.type = config.mode.orEmpty()
vmessQRCode.path = config.serviceName.orEmpty()
vmessQRCode.host = config.authority.orEmpty()
}
else -> {}
}
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
vmessQRCode.tls = config.security.orEmpty()
vmessQRCode.sni = config.sni.orEmpty()
vmessQRCode.fp = config.fingerPrint.orEmpty()
vmessQRCode.alpn = config.alpn.orEmpty()
val json = JsonUtil.toJson(vmessQRCode)
return Utils.encode(json)
}
fun parseVmessStd(str: String): ProfileItem? {
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
config.method = AppConfig.DEFAULT_SECURITY
getItemFormQuery(config, queryParam, allowInsecure)
return config
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VMESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].security = profileItem.method
}
outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
null,
null,
null
)
return outboundBean
}
}
@@ -0,0 +1,124 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
import kotlin.text.orEmpty
object WireguardFmt : FmtBase() {
fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo.orEmpty()
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
config.publicKey = queryParam["publickey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"].orEmpty()
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = (queryParam["reserved"] ?: "0,0,0")
return config
}
fun parseWireguardConfFile(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val interfaceParams: MutableMap<String, String> = mutableMapOf()
val peerParams: MutableMap<String, String> = mutableMapOf()
var currentSection: String? = null
str.lines().forEach { line ->
val trimmedLine = line.trim()
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
return@forEach
}
when {
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
else -> {
if (currentSection != null) {
val parts = trimmedLine.split("=", limit = 2).map { it.trim() }
if (parts.size == 2) {
val key = parts[0].lowercase()
val value = parts[1]
when (currentSection) {
"Interface" -> interfaceParams[key] = value
"Peer" -> peerParams[key] = value
}
}
}
}
}
}
config.secretKey = interfaceParams["privatekey"].orEmpty()
config.remarks = System.currentTimeMillis().toString()
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.publicKey = peerParams["publickey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"].orEmpty()
val endpoint = peerParams["endpoint"].orEmpty()
val endpointParts = endpoint.split(":", limit = 2)
if (endpointParts.size == 2) {
config.server = endpointParts[0]
config.serverPort = endpointParts[1]
} else {
config.server = endpoint
config.serverPort = ""
}
config.reserved = peerParams["reserved"] ?: "0,0,0"
return config
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
}
wireguard.mtu = profileItem.mtu
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
}
return outboundBean
}
fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] = config.publicKey.orEmpty()
if (config.reserved != null) {
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty()
}
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty()
if (config.mtu != null) {
dicQuery["mtu"] = config.mtu.toString()
}
if (config.preSharedKey != null) {
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty()
}
return toUri(config, config.secretKey, dicQuery)
}
}
@@ -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,15 +277,10 @@ 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(JsonUtil.toJson(srv)) ?: 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)?:"")
count += 1 count += 1
} }
return count return count
@@ -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()) {
@@ -0,0 +1,191 @@
package com.v2ray.ang.handler
import android.util.Log
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object MigrateManager {
private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
fun migrateServerConfig2Profile(): Boolean {
if (serverStorage.count().toInt() == 0) {
return false
}
val serverList = serverStorage.allKeys() ?: return false
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
for (guid in serverList) {
var configOld = decodeServerConfigOld(guid) ?: continue
var config = decodeServerConfig(guid)
if (config != null) {
serverStorage.remove(guid)
continue
}
config = migrateServerConfig2ProfileSub(configOld) ?: continue
config.subscriptionId = configOld.subscriptionId
MmkvManager.encodeServerConfig(guid, config)
//check and remove old
decodeServerConfig(guid) ?: continue
serverStorage.remove(guid)
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
}
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
return true
}
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
return when (configOld.getProxyOutbound()?.protocol) {
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.VLESS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.TROJAN.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.SHADOWSOCKS.name.lowercase() -> migrate2ProfileCommon(configOld)
EConfigType.SOCKS.name.lowercase() -> migrate2ProfileSocks(configOld)
EConfigType.HTTP.name.lowercase() -> migrate2ProfileHttp(configOld)
EConfigType.WIREGUARD.name.lowercase() -> migrate2ProfileWireguard(configOld)
EConfigType.HYSTERIA2.name.lowercase() -> migrate2ProfileHysteria2(configOld)
EConfigType.CUSTOM.name.lowercase() -> migrate2ProfileCustom(configOld)
else -> null
}
}
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(configOld.configType)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.method = outbound.getSecurityEncryption()
config.password = outbound.getPassword()
config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type
outbound.getTransportSettingDetails()?.let { transportDetails ->
config.headerType = transportDetails[0].orEmpty()
config.host = transportDetails[1].orEmpty()
config.path = transportDetails[2].orEmpty()
}
config.seed = outbound?.streamSettings?.kcpSettings?.seed
config.quicSecurity = outbound?.streamSettings?.quicSettings?.security
config.quicKey = outbound?.streamSettings?.quicSettings?.key
config.mode = if (outbound?.streamSettings?.grpcSettings?.multiMode == true) "multi" else "gun"
config.serviceName = outbound?.streamSettings?.grpcSettings?.serviceName
config.authority = outbound?.streamSettings?.grpcSettings?.authority
config.security = outbound.streamSettings?.security
val tlsSettings = outbound?.streamSettings?.realitySettings ?: outbound?.streamSettings?.tlsSettings
config.insecure = tlsSettings?.allowInsecure
config.sni = tlsSettings?.serverName
config.fingerPrint = tlsSettings?.fingerprint
config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString()
config.publicKey = tlsSettings?.publicKey
config.shortId = tlsSettings?.shortId
config.spiderX = tlsSettings?.spiderX
return config
}
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.SOCKS)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
config.password = outbound.getPassword()
return config
}
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.HTTP)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.username = outbound.settings?.servers?.first()?.users?.first()?.user
config.password = outbound.getPassword()
return config
}
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
outbound.settings?.let { wireguard ->
config.secretKey = wireguard.secretKey
config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString()
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
config.mtu = wireguard.mtu
config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString()
}
return config
}
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.HYSTERIA2)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
config.password = outbound.getPassword()
config.security = AppConfig.TLS
outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
config.insecure = tlsSetting.allowInsecure
config.sni = tlsSetting.serverName
config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty()
}
config.obfsPassword = outbound.settings?.obfsPassword
return config
}
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.CUSTOM)
val outbound = configOld.getProxyOutbound() ?: return null
config.remarks = configOld.remarks
config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString()
return config
}
private fun decodeServerConfigOld(guid: String): ServerConfig? {
if (guid.isBlank()) {
return null
}
val json = serverStorage.decodeString(guid)
if (json.isNullOrBlank()) {
return null
}
return JsonUtil.fromJson(json, ServerConfig::class.java)
}
}
@@ -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,false)
}
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
return settingsStorage.decodeBool(key, defaultValue)
}
fun decodeSettingsInt(key: String, defaultValue: Int): Int {
return settingsStorage.decodeInt(key, defaultValue)
}
fun decodeSettingsStringSet(key: String): MutableSet<String>? {
return settingsStorage.decodeStringSet(key)
} }
//endregion //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
@@ -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,12 +42,13 @@ 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 resetRoutingRulesetsFromPresets(context: Context, index: Int) {
val rulesetList = getPresetRoutingRulesets(context, index) ?: return val rulesetList = getPresetRoutingRulesets(context, index) ?: return
resetRoutingRulesetsCommon(rulesetList) resetRoutingRulesetsCommon(rulesetList)
} }
fun resetRoutingRulesetsFromClipboard(content: String?): Boolean { fun resetRoutingRulesets(content: String?): Boolean {
if (content.isNullOrEmpty()) { if (content.isNullOrEmpty()) {
return false return false
} }
@@ -93,7 +95,7 @@ object SettingsManager {
if (rulesetList.isNullOrEmpty()) return if (rulesetList.isNullOrEmpty()) return
if (index < 0 || index >= rulesetList.count()) { if (index < 0 || index >= rulesetList.count()) {
rulesetList.add(ruleset) rulesetList.add(0, ruleset)
} else { } else {
rulesetList[index] = ruleset rulesetList[index] = ruleset
} }
@@ -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)
}
}
} }
@@ -1,15 +1,30 @@
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_CLOUDFLARE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_DNSPOD_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_DNSPOD_DOMAIN
import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN
import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_YANDEX_DOMAIN
import com.v2ray.ang.AppConfig.GEOIP_CN import com.v2ray.ang.AppConfig.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,28 +34,30 @@ import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6 import com.v2ray.ang.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)
@@ -53,11 +70,10 @@ object V2rayConfigUtil {
} }
} }
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): ConfigResult { private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult {
val result = ConfigResult(false) val 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 +87,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 +103,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 +123,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 +157,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 +180,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 +191,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 +205,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 +256,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 +280,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 +310,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 +337,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 +351,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 +377,16 @@ 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_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
hosts["dns.alidns.com"] = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_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_DNSPOD_DOMAIN] = DNS_DNSPOD_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
hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
// DNS dns对象 // DNS dns对象
v2rayConfig.dns = V2rayConfig.DnsBean( v2rayConfig.dns = V2rayConfig.DnsBean(
@@ -379,7 +397,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 +414,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)
@@ -407,18 +425,18 @@ object V2rayConfigUtil {
) { ) {
muxEnabled = false muxEnabled = false
} else if (protocol.equals(EConfigType.VLESS.name, true) } else if (protocol.equals(EConfigType.VLESS.name, true)
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true
) { ) {
muxEnabled = false muxEnabled = false
} }
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 +448,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"],"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 +486,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 +503,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 +517,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 +550,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 +562,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 +578,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 +587,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 +616,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)
}
}
}
@@ -13,46 +13,41 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.v2ray.ang.helper
package com.v2ray.ang.helper;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
/** /**
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}. * Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
* *
* @author Paul Burke (ipaulpro) * @author Paul Burke (ipaulpro)
*/ */
public interface ItemTouchHelperAdapter { interface ItemTouchHelperAdapter {
/** /**
* Called when an item has been dragged far enough to trigger a move. This is called every time * Called when an item has been dragged far enough to trigger a move. This is called every time
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/> * an item is shifted, and **not** at the end of a "drop" event.<br></br>
* <br/> * <br></br>
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after * Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
* adjusting the underlying data to reflect this move. * adjusting the underlying data to reflect this move.
* *
* @param fromPosition The start position of the moved item. * @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item. * @param toPosition Then resolved position of the moved item.
* @return True if the item was moved to the new adapter position. * @return True if the item was moved to the new adapter position.
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) * @see RecyclerView.getAdapterPositionFor
* @see RecyclerView.ViewHolder#getAdapterPosition() * @see RecyclerView.ViewHolder.getAdapterPosition
*/ */
boolean onItemMove(int fromPosition, int toPosition); fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
void onItemMoveCompleted(); fun onItemMoveCompleted()
/** /**
* Called when an item has been dismissed by a swipe.<br/> * Called when an item has been dismissed by a swipe.<br></br>
* <br/> * <br></br>
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after * Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
* adjusting the underlying data to reflect this removal. * adjusting the underlying data to reflect this removal.
* *
* @param position The position of the item dismissed. * @param position The position of the item dismissed.
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) * @see RecyclerView.getAdapterPositionFor
* @see RecyclerView.ViewHolder#getAdapterPosition() * @see RecyclerView.ViewHolder.getAdapterPosition
*/ */
void onItemDismiss(int position); fun onItemDismiss(position: Int)
} }
@@ -13,29 +13,26 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.v2ray.ang.helper
package com.v2ray.ang.helper; import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper;
/** /**
* Interface to notify an item ViewHolder of relevant callbacks from {@link * Interface to notify an item ViewHolder of relevant callbacks from [ ].
* ItemTouchHelper.Callback}.
* *
* @author Paul Burke (ipaulpro) * @author Paul Burke (ipaulpro)
*/ */
public interface ItemTouchHelperViewHolder { interface ItemTouchHelperViewHolder {
/** /**
* Called when the {@link ItemTouchHelper} first registers an item as being moved or swiped. * Called when the [ItemTouchHelper] first registers an item as being moved or swiped.
* Implementations should update the item view to indicate it's active state. * Implementations should update the item view to indicate it's active state.
*/ */
void onItemSelected(); fun onItemSelected()
/** /**
* Called when the {@link ItemTouchHelper} has completed the move or swipe, and the active item * Called when the [ItemTouchHelper] has completed the move or swipe, and the active item
* state should be cleared. * state should be cleared.
*/ */
void onItemClear(); fun onItemClear()
} }
@@ -1,129 +0,0 @@
/*
* Copyright (C) 2015 Paul Burke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper;
import android.graphics.Canvas;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.jetbrains.annotations.NotNull;
/**
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
* </br/>
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement
* {@link ItemTouchHelperViewHolder}.
*
* @author Paul Burke (ipaulpro)
*/
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
public static final float ALPHA_FULL = 1.0f;
private final ItemTouchHelperAdapter mAdapter;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
mAdapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
// Set movement flags based on the layout manager
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
final int swipeFlags = 0;
return makeMovementFlags(dragFlags, swipeFlags);
} else {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
}
@Override
public boolean onMove(@NotNull RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
// Notify the adapter of the move
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
// Notify the adapter of the dismissal
mAdapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
}
@Override
public void onChildDraw(@NotNull Canvas c, @NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder, float dX,
float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
// Fade out the view as it is swiped out of the parent's bounds
final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX);
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
// We only want the active item to change
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof ItemTouchHelperViewHolder) {
// Let the view holder know that this item is being moved or dragged
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected();
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(@NotNull RecyclerView recyclerView, @NotNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
mAdapter.onItemMoveCompleted();
viewHolder.itemView.setAlpha(ALPHA_FULL);
if (viewHolder instanceof ItemTouchHelperViewHolder) {
// Tell the view holder it's time to restore the idle state
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear();
}
}
}
@@ -0,0 +1,148 @@
/*
* Copyright (C) 2015 Paul Burke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper
import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.graphics.Canvas
import android.view.animation.DecelerateInterpolator
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.sign
/**
* An implementation of [ItemTouchHelper.Callback] that enables basic drag & drop and
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br></br>
*
* Expects the `RecyclerView.Adapter` to listen for [ ] callbacks and the `RecyclerView.ViewHolder` to implement
* [ItemTouchHelperViewHolder].
*
* @author Paul Burke (ipaulpro)
*/
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
private var mReturnAnimator: ValueAnimator? = null
override fun isLongPressDragEnabled(): Boolean = true
override fun isItemViewSwipeEnabled(): Boolean = true
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags: Int
val swipeFlags: Int
if (recyclerView.layoutManager is GridLayoutManager) {
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
} else {
dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
}
return makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(
recyclerView: RecyclerView,
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return if (source.itemViewType != target.itemViewType) {
false
} else {
mAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
true
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// Do not delete; simply return item to original position
returnViewToOriginalPosition(viewHolder)
}
override fun onChildDraw(
c: Canvas, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val maxSwipeDistance = viewHolder.itemView.width * SWIPE_THRESHOLD
val swipeAmount = abs(dX)
val direction = sign(dX)
// Limit maximum swipe distance
val translationX = min(swipeAmount, maxSwipeDistance) * direction
val alpha = ALPHA_FULL - min(swipeAmount, maxSwipeDistance) / maxSwipeDistance
viewHolder.itemView.translationX = translationX
viewHolder.itemView.alpha = alpha
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
returnViewToOriginalPosition(viewHolder)
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
private fun returnViewToOriginalPosition(viewHolder: RecyclerView.ViewHolder) {
mReturnAnimator?.takeIf { it.isRunning }?.cancel()
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.translationX, 0f).apply {
addUpdateListener { animation ->
val value = animation.animatedValue as Float
viewHolder.itemView.translationX = value
viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD))
}
interpolator = DecelerateInterpolator()
duration = ANIMATION_DURATION
start()
}
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && viewHolder is ItemTouchHelperViewHolder) {
viewHolder.onItemSelected()
}
super.onSelectedChanged(viewHolder, actionState)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = ALPHA_FULL
if (viewHolder is ItemTouchHelperViewHolder) {
viewHolder.onItemClear()
}
mAdapter.onItemMoveCompleted()
}
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
return 1.1f // Set a value greater than 1 to prevent default swipe delete
}
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
return defaultValue * 10 // Increase swipe escape velocity to make swipe harder to trigger
}
companion object {
private const val ALPHA_FULL = 1.0f
private const val SWIPE_THRESHOLD = 0.25f
private const val ANIMATION_DURATION: Long = 200
}
}
@@ -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))
} }
@@ -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}")
} }
@@ -0,0 +1,18 @@
package com.v2ray.ang.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//Check if context is not null and action is the one we want
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) return
//Check if flag is true and a server is selected
if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
//Start v2ray
V2RayServiceManager.startV2Ray(context)
}
}
@@ -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() {
@@ -16,9 +16,9 @@ class TaskerReceiver : BroadcastReceiver() {
try { try {
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE) val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false) val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "") val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty()
if (switch == null || guid == null || TextUtils.isEmpty(guid)) { if (switch == null || TextUtils.isEmpty(guid)) {
return return
} else if (switch) { } else if (switch) {
if (guid == AppConfig.TASKER_DEFAULT_GUID) { if (guid == AppConfig.TASKER_DEFAULT_GUID) {
@@ -8,11 +8,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ProcessService { class ProcessService {
private val TAG = ANG_PACKAGE private var process: Process? = null
private lateinit var process: Process
fun runProcess(context: Context, cmd: MutableList<String>) { fun runProcess(context: Context, cmd: MutableList<String>) {
Log.d(TAG, cmd.toString()) Log.d(ANG_PACKAGE, cmd.toString())
try { try {
val proBuilder = ProcessBuilder(cmd) val proBuilder = ProcessBuilder(cmd)
@@ -23,23 +22,23 @@ class ProcessService {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(50L) Thread.sleep(50L)
Log.d(TAG, "runProcess check") Log.d(ANG_PACKAGE, "runProcess check")
process.waitFor() process?.waitFor()
Log.d(TAG, "runProcess exited") Log.d(ANG_PACKAGE, "runProcess exited")
} }
Log.d(TAG, process.toString()) Log.d(ANG_PACKAGE, process.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
} }
fun stopProcess() { fun stopProcess() {
try { try {
Log.d(TAG, "runProcess destroy") Log.d(ANG_PACKAGE, "runProcess destroy")
process?.destroy() process?.destroy()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
} }
} }
@@ -9,6 +9,7 @@ import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
@@ -32,24 +33,31 @@ class QSTileService : TileService() {
qsTile?.updateTile() qsTile?.updateTile()
} }
/**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
*/
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
setState(Tile.STATE_INACTIVE) setState(Tile.STATE_INACTIVE)
mMsgReceive = ReceiveMessageHandler(this) mMsgReceive = ReceiveMessageHandler(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY), Context.RECEIVER_EXPORTED) ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
} else {
registerReceiver(mMsgReceive, IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY))
}
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
} }
override fun onStopListening() { override fun onStopListening() {
super.onStopListening() super.onStopListening()
unregisterReceiver(mMsgReceive) try {
mMsgReceive = null applicationContext.unregisterReceiver(mMsgReceive)
mMsgReceive = null
} catch (e: Exception) {
e.printStackTrace()
}
} }
override fun onClick() { override fun onClick() {
@@ -14,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 {
@@ -13,21 +13,21 @@ import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.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 +55,7 @@ object V2RayServiceManager {
Seq.setContext(value?.get()?.getService()?.applicationContext) Seq.setContext(value?.get()?.getService()?.applicationContext)
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
} }
var currentConfig: ServerConfig? = null var currentConfig: ProfileItem? = null
private var lastQueryTime = 0L private var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null private var mBuilder: NotificationCompat.Builder? = null
@@ -65,15 +65,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)
@@ -125,6 +127,11 @@ object V2RayServiceManager {
} }
} }
/**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
*/
fun startV2rayPoint() { fun startV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
val guid = MmkvManager.getSelectServer() ?: return val guid = MmkvManager.getSelectServer() ?: return
@@ -132,7 +139,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
@@ -141,11 +148,7 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_SCREEN_ON) mFilter.addAction(Intent.ACTION_SCREEN_ON)
mFilter.addAction(Intent.ACTION_SCREEN_OFF) mFilter.addAction(Intent.ACTION_SCREEN_OFF)
mFilter.addAction(Intent.ACTION_USER_PRESENT) mFilter.addAction(Intent.ACTION_USER_PRESENT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
} else {
service.registerReceiver(mMsgReceive, mFilter)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
@@ -155,7 +158,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))
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
@@ -378,7 +381,7 @@ object V2RayServiceManager {
private fun startSpeedNotification() { private fun startSpeedNotification() {
if (mDisposable == null && if (mDisposable == null &&
v2rayPoint.isRunning && v2rayPoint.isRunning &&
settingsStorage?.decodeBool(AppConfig.PREF_SPEED_ENABLED) == true MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
) { ) {
var lastZeroSpeed = false var lastZeroSpeed = false
val outboundTags = currentConfig?.getAllOutboundTags() val outboundTags = currentConfig?.getAllOutboundTags()
@@ -428,10 +431,11 @@ object V2RayServiceManager {
} }
private fun stopSpeedNotification() { private fun stopSpeedNotification() {
if (mDisposable != null) { mDisposable?.let {
mDisposable?.dispose() //stop queryStats it.dispose() //stop queryStats
mDisposable = null mDisposable = null
updateNotification(currentConfig?.remarks, 0, 0) updateNotification(currentConfig?.remarks, 0, 0)
} }
} }
} }
@@ -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
@@ -24,7 +24,7 @@ import libv2ray.Libv2ray
import java.util.concurrent.Executors import java.util.concurrent.Executors
class V2RayTestService : Service() { class V2RayTestService : Service() {
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(10).asCoroutineDispatcher()) } private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@@ -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
} }
@@ -17,12 +17,13 @@ import android.os.StrictMode
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.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 +65,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
.build() .build()
} }
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } private val connectivity by lazy { getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager }
@delegate:RequiresApi(Build.VERSION_CODES.P) @delegate:RequiresApi(Build.VERSION_CODES.P)
private val defaultNetworkCallback by lazy { private val defaultNetworkCallback by lazy {
@@ -130,7 +131,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 +140,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 +154,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)) {
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)
//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 {
@@ -165,6 +166,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
else else
builder.addAllowedApplication(it) builder.addAllowedApplication(it)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
} }
} }
} else { } else {
@@ -215,12 +217,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
"--loglevel", "notice" "--loglevel", "notice"
) )
if (settingsStorage?.decodeBool(AppConfig.PREF_PREFER_IPV6) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
cmd.add("--netif-ip6addr") cmd.add("--netif-ip6addr")
cmd.add(PRIVATE_VLAN6_ROUTER) cmd.add(PRIVATE_VLAN6_ROUTER)
} }
if (settingsStorage?.decodeBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
val localDnsPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt()) val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
cmd.add("--dnsgw") cmd.add("--dnsgw")
cmd.add("$LOOPBACK:${localDnsPort}") cmd.add("$LOOPBACK:${localDnsPort}")
} }
@@ -232,7 +234,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
process = proBuilder process = proBuilder
.directory(applicationContext.filesDir) .directory(applicationContext.filesDir)
.start() .start()
Thread(Runnable { Thread {
Log.d(packageName, "$TUN2SOCKS check") Log.d(packageName, "$TUN2SOCKS check")
process.waitFor() process.waitFor()
Log.d(packageName, "$TUN2SOCKS exited") Log.d(packageName, "$TUN2SOCKS exited")
@@ -240,7 +242,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
Log.d(packageName, "$TUN2SOCKS restart") Log.d(packageName, "$TUN2SOCKS restart")
runTun2socks() runTun2socks()
} }
}).start() }.start()
Log.d(packageName, process.toString()) Log.d(packageName, process.toString())
sendFd() sendFd()
@@ -4,6 +4,7 @@ import android.Manifest
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.tbruyelle.rxpermissions3.RxPermissions import com.tbruyelle.rxpermissions3.RxPermissions
@@ -133,13 +134,15 @@ class AboutActivity : BaseActivity() {
} }
private fun showFileChooser() { private fun showFileChooser() {
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
intent.type = "*/*" type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
}
try { try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: android.content.ActivityNotFoundException) { } catch (ex: android.content.ActivityNotFoundException) {
Log.e(AppConfig.ANG_PACKAGE, "File chooser activity not found: ${ex.message}", ex)
toast(R.string.toast_require_file_manager) toast(R.string.toast_require_file_manager)
} }
} }
@@ -149,27 +152,23 @@ class AboutActivity : BaseActivity() {
val uri = it.data?.data val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) { if (it.resultCode == RESULT_OK && uri != null) {
try { try {
try { val targetFile =
val targetFile = File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip") contentResolver.openInputStream(uri).use { input ->
contentResolver.openInputStream(uri).use { input -> targetFile.outputStream().use { fileOut ->
targetFile.outputStream().use { fileOut -> input?.copyTo(fileOut)
input?.copyTo(fileOut)
}
} }
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} catch (e: Exception) {
e.printStackTrace()
} }
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
toast(e.message.toString()) toast(R.string.toast_failure)
} }
} }
} }
} }
@@ -34,9 +34,6 @@ abstract class BaseActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let { super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, Utils.getLocale()))
MyContextWrapper.wrap(newBase, Utils.getLocale())
}
super.attachBaseContext(context)
} }
} }
@@ -33,46 +33,42 @@ class LogcatActivity : BaseActivity() {
} }
private fun logcat(shouldFlushLog: Boolean) { private fun logcat(shouldFlushLog: Boolean) {
binding.pbWaiting.visibility = View.VISIBLE
try { lifecycleScope.launch(Dispatchers.Default) {
binding.pbWaiting.visibility = View.VISIBLE try {
lifecycleScope.launch(Dispatchers.Default) {
if (shouldFlushLog) { if (shouldFlushLog) {
val lst = LinkedHashSet<String>() val lst = linkedSetOf("logcat", "-c")
lst.add("logcat")
lst.add("-c")
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val process = Runtime.getRuntime().exec(lst.toTypedArray()) val process = Runtime.getRuntime().exec(lst.toTypedArray())
process.waitFor() process.waitFor()
} }
} }
val lst = LinkedHashSet<String>() val lst = linkedSetOf(
lst.add("logcat") "logcat", "-d", "-v", "time", "-s",
lst.add("-d") "GoLog,tun2socks,$ANG_PACKAGE,AndroidRuntime,System.err"
lst.add("-v") )
lst.add("time")
lst.add("-s")
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
val process = withContext(Dispatchers.IO) { val process = withContext(Dispatchers.IO) {
Runtime.getRuntime().exec(lst.toTypedArray()) Runtime.getRuntime().exec(lst.toTypedArray())
} }
// val bufferedReader = BufferedReader(
// InputStreamReader(process.inputStream))
// val allText = bufferedReader.use(BufferedReader::readText)
val allText = process.inputStream.bufferedReader().use { it.readText() } val allText = process.inputStream.bufferedReader().use { it.readText() }
launch(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.tvLogcat.text = allText binding.tvLogcat.text = allText
binding.tvLogcat.movementMethod = ScrollingMovementMethod() binding.tvLogcat.movementMethod = ScrollingMovementMethod()
binding.pbWaiting.visibility = View.GONE binding.pbWaiting.visibility = View.GONE
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) } Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
} }
} catch (e: IOException) {
withContext(Dispatchers.Main) {
binding.pbWaiting.visibility = View.GONE
toast(R.string.toast_failure)
}
e.printStackTrace()
} }
} catch (e: IOException) {
e.printStackTrace()
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_logcat, menu) menuInflater.inflate(R.menu.menu_logcat, menu)
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
@@ -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,13 +126,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
initGroupTab() initGroupTab()
setupViewModel() setupViewModel()
migrateLegacy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RxPermissions(this) RxPermissions(this)
.request(Manifest.permission.POST_NOTIFICATIONS) .request(Manifest.permission.POST_NOTIFICATIONS)
.subscribe { .subscribe {
if (!it) if (!it)
toast(R.string.toast_permission_denied) toast(R.string.toast_permission_denied_notification)
} }
} }
@@ -171,7 +173,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
mainViewModel.startListenBroadcast() mainViewModel.startListenBroadcast()
mainViewModel.copyAssets(assets) mainViewModel.initAssets(assets)
}
private fun migrateLegacy() {
lifecycleScope.launch(Dispatchers.IO) {
val result = MigrateManager.migrateServerConfig2Profile()
launch(Dispatchers.Main) {
if (result) {
toast(getString(R.string.migration_success))
mainViewModel.reloadServerList()
} else {
//toast(getString(R.string.migration_fail))
}
}
}
} }
private fun initGroupTab() { private fun initGroupTab() {
@@ -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 {
@@ -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)
@@ -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()
@@ -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)
@@ -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)
@@ -1,29 +1,31 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.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 +53,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])
} }
} }
} }
@@ -84,13 +86,13 @@ class RoutingSettingActivity : BaseActivity() {
true true
} }
R.id.import_rulesets -> { R.id.import_predefined_rulesets -> {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i -> AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
try { try {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
SettingsManager.resetRoutingRulesets(this@RoutingSettingActivity, i) SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
refreshData() refreshData()
toast(R.string.toast_success) toast(R.string.toast_success)
@@ -113,30 +115,45 @@ 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.resetRoutingRulesets(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.import_rulesets_from_qrcode -> {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
}
true
}
R.id.export_rulesets_to_clipboard -> { R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets() val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) { if (rulesetList.isNullOrEmpty()) {
@@ -151,8 +168,38 @@ class RoutingSettingActivity : BaseActivity() {
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importRulesetsFromQRcode(it.data?.getStringExtra("SCAN_RESULT"))
}
}
private fun importRulesetsFromQRcode(qrcode: String?): Boolean {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
val result = SettingsManager.resetRoutingRulesets(qrcode)
withContext(Dispatchers.Main) {
if (result) {
refreshData()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do nothing
}
.show()
return true
}
fun refreshData() { fun refreshData() {
rulesets = MmkvManager.decodeRoutingRulesets() ?: mutableListOf() rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
} }
@@ -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)
} }
@@ -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()
} }
} }
@@ -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)
} }
} }
} }
} }
@@ -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,23 @@ 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.NetworkType
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_PORT import com.v2ray.ang.extension.isNotNullEmpty
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() {
@@ -79,6 +81,10 @@ class ServerActivity : BaseActivity() {
private val alpns: Array<out String> by lazy { private val alpns: Array<out String> by lazy {
resources.getStringArray(R.array.streamsecurity_alpn) resources.getStringArray(R.array.streamsecurity_alpn)
} }
private val xhttpMode: Array<out String> by lazy {
resources.getStringArray(R.array.xhttp_mode)
}
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach. // Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
// We don't use AndroidViewBinding because, it is better to share similar logics for different // We don't use AndroidViewBinding because, it is better to share similar logics for different
@@ -87,7 +93,6 @@ class ServerActivity : BaseActivity() {
private val et_address: EditText by lazy { findViewById(R.id.et_address) } private val et_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) }
@@ -108,17 +113,21 @@ class ServerActivity : BaseActivity() {
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) } private val container_alpn: LinearLayout? by lazy { findViewById(R.id.lay_stream_alpn) }
private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) } private val et_public_key: EditText? by lazy { findViewById(R.id.et_public_key) }
private val et_preshared_key: EditText? by lazy { findViewById(R.id.et_preshared_key) }
private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) } private val container_public_key: LinearLayout? by lazy { findViewById(R.id.lay_public_key) }
private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) } private val et_short_id: EditText? by lazy { findViewById(R.id.et_short_id) }
private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) } private val container_short_id: LinearLayout? by lazy { findViewById(R.id.lay_short_id) }
private val et_spider_x: EditText? by lazy { findViewById(R.id.et_spider_x) } private val 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) }
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -142,7 +151,7 @@ class ServerActivity : BaseActivity() {
parent: AdapterView<*>?, parent: AdapterView<*>?,
view: View?, view: View?,
position: Int, position: Int,
id: Long id: Long,
) { ) {
val types = transportTypes(networks[position]) val types = transportTypes(networks[position])
sp_header_type?.isEnabled = types.size > 1 sp_header_type?.isEnabled = types.size > 1
@@ -150,25 +159,49 @@ class ServerActivity : BaseActivity() {
ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types) ArrayAdapter(this@ServerActivity, android.R.layout.simple_spinner_item, types)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
sp_header_type?.adapter = adapter sp_header_type?.adapter = adapter
sp_header_type_title?.text = if (networks[position] == "grpc") sp_header_type_title?.text =
getString(R.string.server_lab_mode_type) else when (networks[position]) {
getString(R.string.server_lab_head_type) NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type)
config?.getProxyOutbound()?.getTransportSettingDetails()?.let { transportDetails -> NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode)
sp_header_type?.setSelection(Utils.arrayFind(types, transportDetails[0])) else -> getString(R.string.server_lab_head_type)
et_request_host?.text = Utils.getEditable(transportDetails[1]) }.orEmpty()
et_path?.text = Utils.getEditable(transportDetails[2]) sp_header_type?.setSelection(
} Utils.arrayFind(
types,
when (networks[position]) {
NetworkType.GRPC.type -> config?.mode
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> config?.xhttpMode
else -> config?.headerType
}.orEmpty()
)
)
et_request_host?.text = Utils.getEditable(
when (networks[position]) {
//"quic" -> config?.quicSecurity
NetworkType.GRPC.type -> config?.authority
else -> config?.host
}.orEmpty()
)
et_path?.text = Utils.getEditable(
when (networks[position]) {
NetworkType.KCP.type -> config?.seed
//"quic" -> config?.quicKey
NetworkType.GRPC.type -> config?.serviceName
else -> config?.path
}.orEmpty()
)
tv_request_host?.text = Utils.getEditable( tv_request_host?.text = Utils.getEditable(
getString( getString(
when (networks[position]) { when (networks[position]) {
"tcp" -> R.string.server_lab_request_host_http NetworkType.TCP.type -> R.string.server_lab_request_host_http
"ws" -> R.string.server_lab_request_host_ws NetworkType.WS.type -> R.string.server_lab_request_host_ws
"httpupgrade" -> R.string.server_lab_request_host_httpupgrade NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade
"splithttp" -> R.string.server_lab_request_host_splithttp NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp
"h2" -> R.string.server_lab_request_host_h2 NetworkType.H2.type -> R.string.server_lab_request_host_h2
"quic" -> R.string.server_lab_request_host_quic //"quic" -> R.string.server_lab_request_host_quic
"grpc" -> R.string.server_lab_request_host_grpc NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc
else -> R.string.server_lab_request_host else -> R.string.server_lab_request_host
} }
) )
@@ -177,17 +210,29 @@ class ServerActivity : BaseActivity() {
tv_path?.text = Utils.getEditable( tv_path?.text = Utils.getEditable(
getString( getString(
when (networks[position]) { when (networks[position]) {
"kcp" -> R.string.server_lab_path_kcp NetworkType.KCP.type -> R.string.server_lab_path_kcp
"ws" -> R.string.server_lab_path_ws NetworkType.WS.type -> R.string.server_lab_path_ws
"httpupgrade" -> R.string.server_lab_path_httpupgrade NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
"splithttp" -> R.string.server_lab_path_splithttp NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
"h2" -> R.string.server_lab_path_h2 NetworkType.H2.type -> R.string.server_lab_path_h2
"quic" -> R.string.server_lab_path_quic //"quic" -> R.string.server_lab_path_quic
"grpc" -> R.string.server_lab_path_grpc NetworkType.GRPC.type -> R.string.server_lab_path_grpc
else -> R.string.server_lab_path else -> R.string.server_lab_path
} }
) )
) )
et_extra?.text = Utils.getEditable(
when (networks[position]) {
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> config?.xhttpExtra
else -> null
}.orEmpty()
)
layout_extra?.visibility =
when (networks[position]) {
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> View.VISIBLE
else -> View.GONE
}
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@@ -199,31 +244,48 @@ class ServerActivity : BaseActivity() {
parent: AdapterView<*>?, parent: AdapterView<*>?,
view: View?, view: View?,
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 +304,90 @@ 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) { et_preshared_key?.visibility = View.VISIBLE
et_reserved1?.text = Utils.getEditable("0") et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
et_reserved2?.text = Utils.getEditable("0") et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
et_reserved3?.text = Utils.getEditable("0") et_local_address?.text = Utils.getEditable(
} else { config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6"
et_reserved1?.text = )
Utils.getEditable(outbound.settings?.reserved?.get(0).toString()) et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
et_reserved2?.text =
Utils.getEditable(outbound.settings?.reserved?.get(1).toString())
et_reserved3?.text =
Utils.getEditable(outbound.settings?.reserved?.get(2).toString())
}
if (outbound.settings?.address == null) {
et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
} else {
val list = outbound.settings?.address as List<*>
et_local_address?.text = Utils.getEditable(list.joinToString(","))
}
if (outbound.settings?.mtu == null) {
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
} else {
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString())
}
} 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 = val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
Utils.arrayFind(securityEncryptions, outbound.getSecurityEncryption().orEmpty())
if (security >= 0) { if (security >= 0) {
sp_security?.setSelection(security) sp_security?.setSelection(security)
} }
val streamSetting = config.outboundBean?.streamSettings ?: return true val streamSecurity = Utils.arrayFind(streamSecuritys, config.security.orEmpty())
val streamSecurity = Utils.arrayFind(streamSecuritys, streamSetting.security)
if (streamSecurity >= 0) { if (streamSecurity >= 0) {
sp_stream_security?.setSelection(streamSecurity) sp_stream_security?.setSelection(streamSecurity)
(streamSetting.tlsSettings ?: streamSetting.realitySettings)?.let { tlsSetting -> container_sni?.visibility = View.VISIBLE
container_sni?.visibility = View.VISIBLE container_fingerprint?.visibility = View.VISIBLE
container_fingerprint?.visibility = View.VISIBLE container_alpn?.visibility = View.VISIBLE
container_alpn?.visibility = View.VISIBLE
et_sni?.text = Utils.getEditable(tlsSetting.serverName) et_sni?.text = Utils.getEditable(config.sni)
tlsSetting.fingerprint?.let { config.fingerPrint?.let {
val utlsIndex = Utils.arrayFind(uTlsItems, tlsSetting.fingerprint) val utlsIndex = Utils.arrayFind(uTlsItems, it)
sp_stream_fingerprint?.setSelection(utlsIndex) sp_stream_fingerprint?.setSelection(utlsIndex)
}
tlsSetting.alpn?.let {
val alpnIndex = Utils.arrayFind(
alpns,
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
)
sp_stream_alpn?.setSelection(alpnIndex)
}
if (streamSetting.tlsSettings != null) {
container_allow_insecure?.visibility = View.VISIBLE
val allowinsecure =
Utils.arrayFind(allowinsecures, tlsSetting.allowInsecure.toString())
if (allowinsecure >= 0) {
sp_allow_insecure?.setSelection(allowinsecure)
}
container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE
} else { // reality settings
container_public_key?.visibility = View.VISIBLE
et_public_key?.text = Utils.getEditable(tlsSetting.publicKey.orEmpty())
container_short_id?.visibility = View.VISIBLE
et_short_id?.text = Utils.getEditable(tlsSetting.shortId.orEmpty())
container_spider_x?.visibility = View.VISIBLE
et_spider_x?.text = Utils.getEditable(tlsSetting.spiderX.orEmpty())
container_allow_insecure?.visibility = View.GONE
}
} }
if (streamSetting.tlsSettings == null && streamSetting.realitySettings == null) { config.alpn?.let {
container_sni?.visibility = View.GONE val alpnIndex = Utils.arrayFind(alpns, it)
container_fingerprint?.visibility = View.GONE sp_stream_alpn?.setSelection(alpnIndex)
container_alpn?.visibility = View.GONE }
container_allow_insecure?.visibility = View.GONE if (config.security == TLS) {
container_allow_insecure?.visibility = View.VISIBLE
val allowinsecure = Utils.arrayFind(allowinsecures, config.insecure.toString())
if (allowinsecure >= 0) {
sp_allow_insecure?.setSelection(allowinsecure)
}
container_public_key?.visibility = View.GONE container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE container_spider_x?.visibility = View.GONE
} else if (config.security == REALITY) {
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 +402,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 +415,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 +434,14 @@ 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) ?: ServerConfig.create(createConfigType) MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.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 +462,82 @@ class ServerActivity : BaseActivity() {
return false return false
} }
} }
et_alterId?.let { if (et_extra?.text?.toString().isNotNullEmpty()) {
val alterId = Utils.parseInt(it.text.toString()) if (JsonUtil.parseString(et_extra?.text?.toString()) == null) {
if (alterId < 0) { toast(R.string.server_lab_xhttp_extra)
toast(R.string.server_lab_alterid)
return false 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.preSharedKey = et_preshared_key?.text.toString().trim()
config.reserved = et_reserved1?.text.toString().trim()
config.localAddress = et_local_address?.text.toString().trim()
config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
} else if (config.configType == EConfigType.HYSTERIA2) {
config.obfsPassword = et_obfs_password?.text?.toString()
config.portHopping = et_port_hop?.text?.toString()
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
config.pinSHA256 = et_pinsha256?.text?.toString()
} }
} }
private fun savePeer(wireguard: V2rayConfig.OutboundBean.OutSettingsBean, port: Int) {
wireguard.secretKey = et_id.text.toString().trim() 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
profileItem.xhttpMode = transportTypes(networks[network])[type]
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
} }
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,38 +547,41 @@ 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)
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> {
return when (network) { return when (network) {
"tcp" -> { NetworkType.TCP.type -> {
tcpTypes tcpTypes
} }
"kcp", "quic" -> { NetworkType.KCP.type -> {
kcpAndQuicTypes kcpAndQuicTypes
} }
"grpc" -> { NetworkType.GRPC.type -> {
grpcModes grpcModes
} }
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> {
xhttpMode
}
else -> { else -> {
arrayOf("---") arrayOf("---")
} }
@@ -599,12 +589,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)
@@ -8,15 +8,13 @@ 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.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.fmt.CustomFmt
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 me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
@@ -48,16 +46,14 @@ class ServerCustomConfigActivity : BaseActivity() {
} }
/** /**
* bingding seleced server config * Binding selected server config
*/ */
private fun bindingServer(config: ServerConfig): Boolean { private fun bindingServer(config: ProfileItem): Boolean {
binding.etRemarks.text = Utils.getEditable(config.remarks) binding.etRemarks.text = Utils.getEditable(config.remarks)
val raw = 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
} }
@@ -78,17 +74,20 @@ class ServerCustomConfigActivity : BaseActivity() {
return false return false
} }
val v2rayConfig = try { val profileItem = try {
JsonUtil.fromJson(binding.editor.text.toString(), V2rayConfig::class.java) CustomFmt.parse(binding.editor.text.toString())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show() ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
return false return false
} }
val config = MmkvManager.decodeServerConfig(editGuid) ?: ServerConfig.create(EConfigType.CUSTOM) val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(EConfigType.CUSTOM)
config.remarks = if (binding.etRemarks.text.isNullOrEmpty()) v2rayConfig.remarks.orEmpty() else binding.etRemarks.text.toString() binding.etRemarks.text.let {
config.fullConfig = v2rayConfig config.remarks = if (it.isNullOrEmpty()) profileItem?.remarks.orEmpty() else it.toString()
}
config.server = profileItem?.server
config.serverPort = profileItem?.serverPort
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
@@ -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"))
} }
} }
@@ -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)
@@ -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
@@ -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)
@@ -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) }
@@ -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() {
@@ -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,12 @@ 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.add_qrcode -> importAssetFromQRcode().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 +109,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())
@@ -170,6 +157,40 @@ class UserAssetActivity : BaseActivity() {
null null
} }
private fun importAssetFromQRcode(): Boolean {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
}
return true
}
private val scanQRCodeForAssetURL = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importAsset(it.data?.getStringExtra("SCAN_RESULT"))
}
}
private fun importAsset(url: String?): Boolean {
try {
if (!Utils.isValidUrl(url)) {
toast(R.string.toast_invalid_url)
return false
}
// Send URL to UserAssetUrlActivity for Processing
startActivity(Intent(this, UserAssetUrlActivity::class.java)
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url))
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
private fun downloadGeoFiles() { private fun downloadGeoFiles() {
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root) .setView(LayoutProgressBinding.inflate(layoutInflater).root)
@@ -254,6 +275,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 +315,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 +327,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()
} }
} }
@@ -9,11 +9,16 @@ 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
class UserAssetUrlActivity : BaseActivity() { class UserAssetUrlActivity : BaseActivity() {
// Receive QRcode URL from UserAssetActivity
companion object {
const val ASSET_URL_QRCODE = "ASSET_URL_QRCODE"
}
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) } private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null var del_config: MenuItem? = null
@@ -28,10 +33,15 @@ class UserAssetUrlActivity : BaseActivity() {
title = getString(R.string.title_user_asset_add_url) title = getString(R.string.title_user_asset_add_url)
val assetItem = MmkvManager.decodeAsset(editAssetId) val assetItem = MmkvManager.decodeAsset(editAssetId)
if (assetItem != null) { val assetUrlQrcode = intent.getStringExtra(ASSET_URL_QRCODE)
bindingAsset(assetItem) val assetNameQrcode = File(assetUrlQrcode.toString()).name
} else { when {
clearAsset() assetItem != null -> bindingAsset(assetItem)
assetUrlQrcode != null -> {
binding.etRemarks.setText(assetNameQrcode)
binding.etUrl.setText(assetUrlQrcode)
}
else -> clearAsset()
} }
} }
@@ -13,13 +13,11 @@ object AppManagerUtil {
val apps = ArrayList<AppInfo>() val apps = ArrayList<AppInfo>()
for (pkg in packages) { for (pkg in packages) {
//if (!pkg.hasInternetPermission && pkg.packageName != "android") continue val applicationInfo = pkg.applicationInfo ?: continue
val applicationInfo = pkg.applicationInfo
val appName = applicationInfo.loadLabel(packageManager).toString() val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager) val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0 val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo) apps.add(appInfo)
@@ -2,6 +2,8 @@ package com.v2ray.ang.util
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer import com.google.gson.JsonSerializer
@@ -15,15 +17,17 @@ object JsonUtil {
return gson.toJson(src) return gson.toJson(src)
} }
fun <T> fromJson(json: String, cls: Class<T>): T { fun <T> fromJson(src: String, cls: Class<T>): T {
return gson.fromJson(json, cls) return gson.fromJson(src, cls)
} }
fun toJsonPretty(src: Any?): String { fun toJsonPretty(src: Any?): String? {
if (src == null)
return null
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(
@@ -34,4 +38,15 @@ object JsonUtil {
.create() .create()
return gsonPre.toJson(src) return gsonPre.toJson(src)
} }
fun parseString(src: String?): JsonObject? {
if (src == null)
return null
try {
return JsonParser.parseString(src).getAsJsonObject()
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
} }
@@ -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())
} }
} }
} }
@@ -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
} }
} }
@@ -17,14 +17,16 @@ import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.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 +131,8 @@ 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 +141,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 +150,8 @@ 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 +162,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
@@ -288,7 +295,7 @@ object Utils {
fun urlEncode(url: String): String { fun urlEncode(url: String): String {
return try { return try {
URLEncoder.encode(url, Charsets.UTF_8.toString()) URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
url url
@@ -352,7 +359,11 @@ object Utils {
} }
@Throws(IOException::class) @Throws(IOException::class)
fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String { fun getUrlContentWithCustomUserAgent(
urlStr: String?,
timeout: Int = 30000,
httpPort: Int = 0
): String {
val url = URL(urlStr) val url = URL(urlStr)
val conn = if (httpPort == 0) { val conn = if (httpPort == 0) {
url.openConnection() url.openConnection()
@@ -385,8 +396,8 @@ object Utils {
} }
fun setNightMode(context: Context) { fun setNightMode() {
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 +416,20 @@ object Utils {
} }
fun getLocale(): Locale { fun getLocale(): Locale {
val lang = settingsStorage?.decodeString(AppConfig.PREF_LANGUAGE) ?: "auto" val langCode =
return when (lang) { MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
"auto" -> getSysLocale() val language = Language.fromCode(langCode)
"en" -> Locale.ENGLISH
"zh-rCN" -> Locale.CHINA return when (language) {
"zh-rTW" -> Locale.TRADITIONAL_CHINESE Language.AUTO -> getSysLocale()
"vi" -> Locale("vi") Language.ENGLISH -> Locale.ENGLISH
"ru" -> Locale("ru") Language.CHINA -> Locale.CHINA
"fa" -> Locale("fa") Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
"bn" -> Locale("bn") Language.VIETNAMESE -> Locale("vi")
else -> getSysLocale() Language.RUSSIAN -> Locale("ru")
Language.PERSIAN -> Locale("fa")
Language.BANGLA -> Locale("bn")
Language.BAKHTIARI -> Locale("bqi", "IR")
} }
} }
@@ -449,7 +463,8 @@ 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 +480,23 @@ 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
}
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.RECEIVER_EXPORTED
} else {
ContextCompat.RECEIVER_NOT_EXPORTED
}
} }

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