Compare commits

...

116 Commits

Author SHA1 Message Date
2dust 496a0483d2 up 1.9.27 2024-12-24 17:32:06 +08:00
2dust e11dca00bb Add release function to build 2024-12-24 17:31:32 +08:00
2dust fde39bf34e Bug fix for isXray() 2024-12-24 15:03:10 +08:00
kore kas nadar 4f11bae238 Update Luri Bakhtiari translation (#4178) 2024-12-23 17:15:06 +08:00
2dust f6282ba71f Ignore libhysteria2.so 2024-12-23 09:59:57 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ 3edf1f4e1b Refine cache key (#4172) 2024-12-22 13:36:18 +08:00
886963226 41bc064083 Optimization logcat after change quickie-foss (#4171)
After changed quickie-foss, scanning cause lot of log. throttle log com.google.zxing.NotFoundException.
2024-12-22 10:21:55 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ eb8562e6b0 Ignore libtun2socks.so (#4169) 2024-12-22 10:04:23 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ 68fbdd92c3 s/versionNameSuffix/applicationIdSuffix (#4168) 2024-12-21 21:08:21 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ 02038a5d93 Build and cache libtun2socks and libv2ray (#4167)
* build libtun2socks

* Clean up binaries

* test cache

altrepo for testing

* switch to original repo
2024-12-21 20:35:17 +08:00
2dust 4fb8c2f4b2 org.gradle.jvmargs=-Xmx4096m 2024-12-21 17:54:03 +08:00
2dust 7afffa60c3 Code clean 2024-12-21 17:18:35 +08:00
2dust 0e6c860360 JavaVersion.VERSION_17 2024-12-21 16:43:26 +08:00
2dust ebfbbfa08b Revert "Update build.yml (#4166)"
This reverts commit b5f182dfec.
2024-12-21 16:08:00 +08:00
886963226 b5f182dfec Update build.yml (#4166) 2024-12-21 14:47:28 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ 4cf28d0ad0 Fix split apk (#4165) 2024-12-21 14:21:58 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ 149bb049a5 Fix flavor build (#4164)
* Fix license report

* Upload both flavors
2024-12-21 14:09:52 +08:00
2dust 124702f0a2 Fix libs 2024-12-21 11:48:48 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ c1cebe578b fdroid flavor and split abi (#4162) 2024-12-21 11:18:41 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ e46c1ee849 switch to quickie-foss for QR code (#4161)
Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2024-12-21 11:18:16 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ 70f1743114 Switch to gradle-license-plugin (#4160)
* switch to gradle-license-plugin

* generate licenseReleaseReport
2024-12-21 10:40:20 +08:00
2dust 54d520727e Add signature for build 2024-12-20 15:26:55 +08:00
alphax-hue3682 1f1e4db486 Update persian translate (#4158) 2024-12-20 15:08:29 +08:00
solokot e536236634 Update Russian translation (#4156) 2024-12-20 11:16:55 +08:00
2dust 140c236da5 Add DNS hosts (Format: domain:address,…)
https://github.com/2dust/v2rayNG/issues/4147
2024-12-19 20:57:24 +08:00
2dust 69ede34274 up 1.9.26 2024-12-19 17:34:55 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ fcf6e22132 Add fastlane metadata for F-Droid (#4121)
* Add fastlane metadata

* fixup! Add fastlane metadata

* hoedown full_description.txt

* Remove title heading 1
2024-12-19 13:03:12 +08:00
2dust 7438ee8308 up 1.9.25 2024-12-18 20:58:31 +08:00
2dust f01cf7fcb5 Disable mux when using xhttp 2024-12-16 21:15:51 +08:00
Tamim Hossain 7a852f78e4 Update package info logic for better encapsulation (#4110)
Update package info logic for better encapsulation
2024-12-08 14:27:20 +08:00
kore kas nadar 0923659a49 Update Luri Bakhtiari translation (#4105) 2024-12-07 19:22:49 +08:00
phoenix6936 f9feb08607 Update translation (#4104)
* Update strings.xml

* Update persian translation
2024-12-07 19:22:40 +08:00
deepsm0ke 3b43fe39e5 Prevent showing the location of the USA during WebRTC Leak (#4103)
Changed tun2socks config private vlan4s and vlan6s to 10.10.10.1>10.10.10.2 and fc00::10:10:10:1>fc00::10:10:10:2 .

When visiting certain websites or using apps that could identify the real public IP, if the IP 26.26.26.1 was shown and it indicated a location in the USA, it unfortunately led to permanent account bans! . This issue is most likely resolved with this PR.
2024-12-06 10:50:45 +08:00
phoenix6936 de30fa15b3 Update persian translation (#4102) 2024-12-05 17:31:43 +08:00
solokot 25a4d7c14d Update Russian translation (#4101) 2024-12-05 17:31:32 +08:00
2dust c8d3607efe Fix
https://github.com/2dust/v2rayNG/issues/4098
2024-12-05 09:25:57 +08:00
2dust a0e73a9aa9 Fix typos 2024-12-05 09:25:11 +08:00
kore kas nadar eaccf237a4 Update Luri Bakhtiari translation (#4097) 2024-12-05 09:13:13 +08:00
2dust 5124266346 up 1.9.24 2024-12-04 19:26:30 +08:00
phoenix6936 5cf2ea5a1e Update translation persian (#4096) 2024-12-04 19:10:05 +08:00
2dust 7a1af5914e Shows how many configurations have been test 2024-12-04 17:20:06 +08:00
2dust e61f5eeb76 Shows how many configurations have been export/import/Update 2024-12-04 17:02:52 +08:00
2dust 6d92106f9d Shows how many configurations have been deleted
https://github.com/2dust/v2rayNG/issues/4078
2024-12-04 16:07:43 +08:00
2dust 8b06745e86 Using mixed local listening ports
Remove inbound http
2024-12-03 20:05:58 +08:00
2dust 85ad999975 Export complete configuration for hysteria2
https://github.com/2dust/v2rayNG/issues/4080
2024-12-02 14:05:41 +08:00
2dust 4c0f2d84cc Add stream-one
https://github.com/2dust/v2rayNG/issues/4083
2024-12-02 13:51:40 +08:00
2dust 6fc9803431 Delete splithttp 2024-11-30 19:29:41 +08:00
2dust 59a710bae5 up 1.9.23 2024-11-30 16:20:57 +08:00
2dust 98c642e1a8 Bug fix 2024-11-30 14:46:38 +08:00
phoenix6936 e91f4470fb Update persian translate (#4064)
* Update persian translate 

Update persian translate

* Update strings.xml
2024-11-30 14:40:01 +08:00
2dust b33cc5284f Add restart button in notification
https://github.com/2dust/v2rayNG/issues/4069
2024-11-29 17:06:56 +08:00
2dust eea6db6814 up 1.9.22 2024-11-28 16:54:27 +08:00
hosêyň abāspanā ba622c7edf Update Luri Bakhtiari translation (#4063) 2024-11-28 16:51:24 +08:00
phoenix6936 b52e89d614 Update persian translate (#4062)
* Update strings.xml

* Update strings.xml

* Update strings.xml
2024-11-28 16:51:15 +08:00
2dust 49cd3a0494 Bug fix 2024-11-28 16:49:06 +08:00
solokot 75cc16c5e0 Update Russian translation (#4060) 2024-11-28 16:03:27 +08:00
phoenix6936 e674d22ecd Update persian translate (#4059)
* Update persian translate 

Update persian translate

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml
2024-11-28 16:03:15 +08:00
2dust dca5011eb1 Append HTTP Proxy to VPN setting
https://github.com/2dust/v2rayNG/issues/4017
https://github.com/2dust/v2rayNG/issues/4045
2024-11-28 14:29:06 +08:00
phoenix6936 4d8e38b704 Update kotlin version to 2.1.0 (#4050)
* Update kotlin version to 2.1.0

Update kotlin version to 2.1.0

* Update kotlin version to 2.1.0

Update kotlin version to 2.1.0
2024-11-28 09:52:50 +08:00
phoenix6936 5ba4352641 Update Persian Translation (#4041)
* Update Persian Translation

Fixing some problems in translations and updating according to the latest strings added to the English language file

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

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

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

Improved Persian translation

* Improved Persian translation

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

* Import rulesets from qrcode

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

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

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

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

Update persian translate

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

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

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

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

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

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

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

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

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

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

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

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

Update persian translate

* Update persian translate 

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

* Segment and update DNS lists
2024-11-09 14:40:24 +08:00
2dust 0880313659 Bug fix
https://github.com/2dust/v2rayNG/issues/3885
2024-11-08 15:06:52 +08:00
2dust 875ca02126 Bug fix
https://github.com/2dust/v2rayNG/issues/3900
2024-11-08 10:43:13 +08:00
2dust c2d5925053 up 1.9.14 2024-11-07 20:54:09 +08:00
2dust 547bbf8e95 Bug fix 2024-11-07 20:37:17 +08:00
2dust 2218251b03 Bug fix
https://github.com/2dust/v2rayNG/issues/3895
2024-11-07 20:32:08 +08:00
2dust 4da3a23162 up 1.9.13 2024-11-06 14:31:10 +08:00
2dust 28a90baf88 Bug fix
https://github.com/2dust/v2rayNG/issues/3883
2024-11-06 11:22:01 +08:00
2dust 7bbdda2f2f This is a temporary solution
https://github.com/2dust/v2rayNG/issues/3883
2024-11-05 21:32:50 +08:00
160 changed files with 3876 additions and 1354 deletions
+108 -32
View File
@@ -1,13 +1,14 @@
name: Build APK name: Build APK
on: on:
push:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
XRAY_CORE_VERSION: release_tag:
description: 'Xray core version or commit hash'
required: false required: false
type: string
push:
branches:
- master
jobs: jobs:
build: build:
@@ -17,77 +18,152 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Java - name: Prepare build dir
uses: actions/setup-java@v4 run: |
mkdir ${{ github.workspace }}/build
- name: Fetch AndroidLibV2rayLite
run: |
cd ${{ github.workspace }}/build
git clone --depth=1 -b master https://github.com/2dust/AndroidLibV2rayLite.git
cd AndroidLibV2rayLite
git submodule update --init
- name: Restore cached libtun2socks
id: cache-libtun2socks-restore
uses: actions/cache/restore@v4
with: with:
distribution: 'temurin' path: ${{ github.workspace }}/build/AndroidLibV2rayLite/libs
java-version: '21' key: libtun2socks-${{ runner.os }}-${{ hashFiles('build/AndroidLibV2rayLite/.git/refs/heads/master') }}-${{ hashFiles('build/AndroidLibV2rayLite/.git/modules/badvpn/HEAD') }}-${{ hashFiles('build/AndroidLibV2rayLite/.git/modules/libancillary/HEAD') }}
- name: Setup Android NDK
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
uses: nttld/setup-ndk@v1
id: setup-ndk
# Same version as https://gitlab.com/fdroid/fdroiddata/metadata/com.v2ray.ang.yml
with:
ndk-version: r27
- name: Build libtun2socks
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
run: |
cd ${{ github.workspace }}/build/AndroidLibV2rayLite
bash compile-tun2socks.sh
tar -xvzf libtun2socks.so.tgz
cp -r libs/* ${{ github.workspace }}/V2rayNG/app/libs/
env:
NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Save libtun2socks
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ${{ github.workspace }}/build/AndroidLibV2rayLite/libs
key: libtun2socks-${{ runner.os }}-${{ hashFiles('build/AndroidLibV2rayLite/.git/refs/heads/master') }}-${{ hashFiles('build/AndroidLibV2rayLite/.git/modules/badvpn/HEAD') }}-${{ hashFiles('build/AndroidLibV2rayLite/.git/modules/libancillary/HEAD') }}
- name: Copy libtun2socks
run: |
cp -r ${{ github.workspace }}/build/AndroidLibV2rayLite/libs/* ${{ github.workspace }}/V2rayNG/app/libs/
- name: Fetch AndroidLibXrayLite
run: |
cd ${{ github.workspace }}/build
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
- name: Restore cached libv2ray
id: cache-libv2ray-restore
uses: actions/cache/restore@v4
with:
path: ${{ github.workspace }}/build/AndroidLibXrayLite/
key: libv2ray-${{ runner.os }}-${{ hashFiles('build/AndroidLibXrayLite/.git/refs/heads/main') }}
- name: Setup Golang - name: Setup Golang
if: steps.cache-libv2ray-restore.outputs.cache-hit != 'true'
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23.2' go-version: '1.23.4'
cache: false cache: false
- name: Patch Go use 600296 - name: Patch Go use 600296
if: steps.cache-libv2ray-restore.outputs.cache-hit != 'true'
#https://go-review.googlesource.com/c/go/+/600296 #https://go-review.googlesource.com/c/go/+/600296
run: | run: |
cd "$(go env GOROOT)" cd "$(go env GOROOT)"
curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1 curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1
- name: Install gomobile - name: Install gomobile
if: steps.cache-libv2ray-restore.outputs.cache-hit != 'true'
run: | run: |
go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240806205939-81131f6468ab 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: Build libv2ray
uses: android-actions/setup-android@v3 if: steps.cache-libv2ray-restore.outputs.cache-hit != 'true'
- name: Build dependencies
run: | run: |
mkdir ${{ github.workspace }}/build cd ${{ github.workspace }}/build/AndroidLibXrayLite
cd ${{ github.workspace }}/build
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
cd AndroidLibXrayLite
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
gomobile init gomobile init
go mod tidy -v go mod tidy -v
gomobile bind -v -androidapi 21 -ldflags='-s -w' ./ gomobile bind -v -androidapi 21 -ldflags='-s -w' ./
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
- name: Save libv2ray
if: steps.cache-libv2ray-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ${{ github.workspace }}/build/AndroidLibXrayLite/
key: libv2ray-${{ runner.os }}-${{ hashFiles('build/AndroidLibXrayLite/.git/refs/heads/main') }}
- name: Copy libv2ray
run: |
cp -r ${{ github.workspace }}/build/AndroidLibXrayLite/*.aar ${{ github.workspace }}/V2rayNG/app/libs/
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Android environment
uses: android-actions/setup-android@v3
- name: Decode Keystore
uses: timheuer/base64-to-file@v1
id: android_keystore
with:
fileName: "android_keystore.jks"
encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }}
- name: Build APK - name: Build APK
run: | run: |
cd ${{ github.workspace }}/V2rayNG cd ${{ github.workspace }}/V2rayNG
chmod 755 gradlew chmod 755 gradlew
./gradlew assembleDebug ./gradlew licenseFdroidReleaseReport
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
- name: Upload arm64-v8a APK - name: Upload arm64-v8a APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ success() }} if: ${{ success() }}
with: with:
name: arm64-v8a name: arm64-v8a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*arm64-v8a*.apk path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
- name: Upload armeabi-v7a APK - name: Upload armeabi-v7a APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ success() }} if: ${{ success() }}
with: with:
name: armeabi-v7a name: armeabi-v7a
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
- name: Upload x86 APK - name: Upload x86 APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ success() }} if: ${{ success() }}
with: with:
name: x86-apk name: x86-apk
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*x86*.apk
- name: Upload Other APKs - name: Upload AndroidLibXrayLite to release
uses: actions/upload-artifact@v4 uses: svenstaro/upload-release-action@v2
if: github.event.inputs.release_tag != ''
with: with:
name: others-apk file: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*playstore*/release/*.apk
path: | tag: ${{ github.event.inputs.release_tag }}
${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug file_glob: true
!${{ 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
+16
View File
@@ -0,0 +1,16 @@
name: Validate Fastlane metadata
on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate Fastlane Supply Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0
+2
View File
@@ -3,3 +3,5 @@
V2rayNG/app/release/output.json V2rayNG/app/release/output.json
.idea/ .idea/
.gradle/ .gradle/
libtun2socks.so
libhysteria2.so
+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-2.0.21-blue.svg)](https://kotlinlang.org) [![Kotlin Version](https://img.shields.io/badge/Kotlin-2.1.0-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)
+124 -68
View File
@@ -1,46 +1,63 @@
plugins { plugins {
id("com.android.application") alias(libs.plugins.android.application)
id("org.jetbrains.kotlin.android") alias(libs.plugins.kotlin.android)
id("com.jaredsburrows.license")
} }
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 = 35 targetSdk = 35
versionCode = 607 versionCode = 623
versionName = "1.9.12" versionName = "1.9.27"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
splits { splits {
abi { abi {
isEnable = true isEnable = true
include( reset()
"arm64-v8a", if (abiFilterList != null && abiFilterList.isNotEmpty()) {
"armeabi-v7a", include(*abiFilterList.toTypedArray())
"x86_64", } else {
"x86" include(
) "arm64-v8a",
isUniversalApk = true "armeabi-v7a",
"x86_64",
"x86"
)
}
isUniversalApk = abiFilterList.isNullOrEmpty()
} }
} }
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"),
"proguard-rules.pro"
)
} }
debug { }
isMinifyEnabled = false
flavorDimensions.add("distribution")
productFlavors {
create("fdroid") {
dimension = "distribution"
applicationIdSuffix = ".fdroid"
buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"")
}
create("playstore") {
dimension = "distribution"
buildConfigField("String", "DISTRIBUTION", "\"Play Store\"")
} }
} }
@@ -50,30 +67,57 @@ android {
} }
} }
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_17.toString()
} }
applicationVariants.all { applicationVariants.all {
val variant = this val variant = this
val versionCodes = val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4) if (isFdroid) {
val versionCodes =
mapOf("armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
)
variant.outputs variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output -> .forEach { output ->
val abi = if (output.getFilter("ABI") != null) val abi = output.getFilter("ABI") ?: "universal"
output.getFilter("ABI") output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
else if (versionCodes.containsKey(abi)) {
"universal" output.versionCodeOverride =
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk" } else {
if (versionCodes.containsKey(abi)) { return@forEach
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode) }
} else {
return@forEach
} }
} } else {
val versionCodes =
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
val abi = if (output.getFilter("ABI") != null)
output.getFilter("ABI")
else
"universal"
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if (versionCodes.containsKey(abi)) {
output.versionCodeOverride =
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} else {
return@forEach
}
}
}
} }
buildFeatures { buildFeatures {
@@ -86,49 +130,61 @@ 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)
testImplementation(libs.org.mockito.mockito.inline)
testImplementation(libs.mockito.kotlin)
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.foss)
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)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
@@ -41,14 +41,90 @@
"geosite:private" "geosite:private"
] ]
}, },
{
"remarks": "代理海外公共DNSIP",
"outboundTag": "proxy",
"ip": [
"1.1.1.1",
"1.0.0.1",
"2606:4700:4700::1111",
"2606:4700:4700::1001",
"1.1.1.2",
"1.0.0.2",
"2606:4700:4700::1112",
"2606:4700:4700::1002",
"1.1.1.3",
"1.0.0.3",
"2606:4700:4700::1113",
"2606:4700:4700::1003",
"8.8.8.8",
"8.8.4.4",
"2001:4860:4860::8888",
"2001:4860:4860::8844",
"94.140.14.14",
"94.140.15.15",
"2a10:50c0::ad1:ff",
"2a10:50c0::ad2:ff",
"94.140.14.15",
"94.140.15.16",
"2a10:50c0::bad1:ff",
"2a10:50c0::bad2:ff",
"94.140.14.140",
"94.140.14.141",
"2a10:50c0::1:ff",
"2a10:50c0::2:ff",
"208.67.222.222",
"208.67.220.220",
"2620:119:35::35",
"2620:119:53::53",
"208.67.222.123",
"208.67.220.123",
"2620:119:35::123",
"2620:119:53::123",
"9.9.9.9",
"149.112.112.112",
"2620:fe::9",
"2620:fe::fe",
"9.9.9.11",
"149.112.112.11",
"2620:fe::11",
"2620:fe::fe:11",
"9.9.9.10",
"149.112.112.10",
"2620:fe::10",
"2620:fe::fe:10",
"77.88.8.8",
"77.88.8.1",
"2a02:6b8::feed:0ff",
"2a02:6b8:0:1::feed:0ff",
"77.88.8.88",
"77.88.8.2",
"2a02:6b8::feed:bad",
"2a02:6b8:0:1::feed:bad",
"77.88.8.7",
"77.88.8.3",
"2a02:6b8::feed:a11",
"2a02:6b8:0:1::feed:a11"
]
},
{
"remarks": "代理海外公共DNS域名",
"outboundTag": "proxy",
"domain": [
"domain:cloudflare-dns.com",
"domain:one.one.one.one",
"domain:dns.google",
"domain:adguard-dns.com",
"domain:opendns.com",
"domain:umbrella.com",
"domain:quad9.net",
"domain:yandex.net"
]
},
{ {
"remarks": "代理IP", "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",
@@ -34,29 +34,62 @@
"geosite:private" "geosite:private"
] ]
}, },
{
"remarks": "绕过中国公共DNSIP",
"outboundTag": "direct",
"ip": [
"223.5.5.5",
"223.6.6.6",
"2400:3200::1",
"2400:3200:baba::1",
"119.29.29.29",
"1.12.12.12",
"120.53.53.53",
"2402:4e00::",
"2402:4e00:1::",
"180.76.76.76",
"2400:da00::6666",
"114.114.114.114",
"114.114.115.115",
"114.114.114.119",
"114.114.115.119",
"114.114.114.110",
"114.114.115.110",
"180.184.1.1",
"180.184.2.2",
"101.226.4.6",
"218.30.118.6",
"123.125.81.6",
"140.207.198.6",
"1.2.4.8",
"210.2.4.8",
"52.80.66.66",
"117.50.22.22",
"2400:7fc0:849e:200::4",
"2404:c2c0:85d8:901::4",
"117.50.10.10",
"52.80.52.52",
"2400:7fc0:849e:200::8",
"2404:c2c0:85d8:901::8",
"117.50.60.30",
"52.80.60.30"
]
},
{
"remarks": "绕过中国公共DNS域名",
"outboundTag": "direct",
"domain": [
"domain:alidns.com",
"domain:doh.pub",
"domain:dot.pub",
"domain:360.cn",
"domain:onedns.net"
]
},
{ {
"remarks": "绕过中国IP", "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"
] ]
}, },
@@ -64,13 +97,7 @@
"remarks": "绕过中国域名", "remarks": "绕过中国域名",
"outboundTag": "direct", "outboundTag": "direct",
"domain": [ "domain": [
"domain:dns.alidns.com", "geosite:cn"
"domain:doh.pub",
"domain:dot.pub",
"domain:doh.360.cn",
"domain:dot.360.cn",
"geosite:cn",
"geosite:geolocation-cn"
] ]
}, },
{ {
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,7 @@ import androidx.multidex.MultiDexApplication
import androidx.work.Configuration import androidx.work.Configuration
import androidx.work.WorkManager import androidx.work.WorkManager
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -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,16 +38,11 @@ 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)
SettingsManager.initRoutingRulesets(this) SettingsManager.initRoutingRulesets(this)
} }
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
)!!
} }
@@ -22,6 +22,7 @@ object AppConfig {
const val PREF_BYPASS_APPS = "pref_bypass_apps" const val PREF_BYPASS_APPS = "pref_bypass_apps"
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled" const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled" const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy"
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port" const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
const val PREF_VPN_DNS = "pref_vpn_dns" const val PREF_VPN_DNS = "pref_vpn_dns"
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
@@ -47,9 +48,9 @@ object AppConfig {
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled" const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_ALLOW_INSECURE = "pref_allow_insecure" const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
const val PREF_SOCKS_PORT = "pref_socks_port" const val PREF_SOCKS_PORT = "pref_socks_port"
const val PREF_HTTP_PORT = "pref_http_port"
const val PREF_REMOTE_DNS = "pref_remote_dns" const val PREF_REMOTE_DNS = "pref_remote_dns"
const val PREF_DOMESTIC_DNS = "pref_domestic_dns" const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_DNS_HOSTS = "pref_dns_hosts"
const val PREF_DELAY_TEST_URL = "pref_delay_test_url" const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
const val PREF_LOGLEVEL = "pref_core_loglevel" const val PREF_LOGLEVEL = "pref_core_loglevel"
const val PREF_MODE = "pref_mode" const val PREF_MODE = "pref_mode"
@@ -111,7 +112,6 @@ object AppConfig {
/** Ports and addresses for various services. */ /** Ports and addresses for various services. */
const val PORT_LOCAL_DNS = "10853" const val PORT_LOCAL_DNS = "10853"
const val PORT_SOCKS = "10808" const val PORT_SOCKS = "10808"
const val PORT_HTTP = "10809"
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32" const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128" const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
const val WIREGUARD_LOCAL_MTU = "1420" const val WIREGUARD_LOCAL_MTU = "1420"
@@ -161,15 +161,20 @@ object AppConfig {
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com" const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
// Android Private DNS constants // Android Private DNS constants
const val DNS_PUB_DOMAIN = "dns.pub" const val DNS_DNSPOD_DOMAIN = "dot.pub"
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com" const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
const val DNS_ONE_ONE_DOMAIN = "one.one.one.one" const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
const val DNS_GOOGLE_DOMAIN = "dns.google" const val DNS_GOOGLE_DOMAIN = "dns.google"
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
val DNS_PUB_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_ONE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001") val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844") val DNS_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_PORT = 443
const val DEFAULT_SECURITY = "auto" const val DEFAULT_SECURITY = "auto"
@@ -8,7 +8,8 @@ enum class Language(val code: String) {
VIETNAMESE("vi"), VIETNAMESE("vi"),
RUSSIAN("ru"), RUSSIAN("ru"),
PERSIAN("fa"), PERSIAN("fa"),
BANGLA("bn"); BANGLA("bn"),
BAKHTIARI("bqi-rIR");
companion object { companion object {
fun fromCode(code: String): Language { fun fromCode(code: String): Language {
@@ -5,10 +5,10 @@ enum class NetworkType(val type: String) {
KCP("kcp"), KCP("kcp"),
WS("ws"), WS("ws"),
HTTP_UPGRADE("httpupgrade"), HTTP_UPGRADE("httpupgrade"),
SPLIT_HTTP("splithttp"), XHTTP("xhttp"),
HTTP("http"), HTTP("http"),
H2("h2"), H2("h2"),
QUIC("quic"), //QUIC("quic"),
GRPC("grpc"); GRPC("grpc");
companion object { companion object {
@@ -30,6 +30,8 @@ data class ProfileItem(
var mode: String? = null, var mode: String? = null,
var serviceName: String? = null, var serviceName: String? = null,
var authority: String? = null, var authority: String? = null,
var xhttpMode: String? = null,
var xhttpExtra: String? = null,
var security: String? = null, var security: String? = null,
var sni: String? = null, var sni: String? = null,
@@ -42,6 +44,7 @@ data class ProfileItem(
var spiderX: String? = null, var spiderX: String? = null,
var secretKey: String? = null, var secretKey: String? = null,
var preSharedKey: String? = null,
var localAddress: String? = null, var localAddress: String? = null,
var reserved: String? = null, var reserved: String? = null,
var mtu: Int? = null, var mtu: Int? = null,
@@ -66,10 +69,45 @@ data class ProfileItem(
return Utils.getIpv6Address(server) + ":" + serverPort return Utils.getIpv6Address(server) + ":" + serverPort
} }
fun getKeyProperty(): ProfileItem { override fun equals(other: Any?): Boolean {
val copy = this.copy() if (other == null) return false
copy.subscriptionId = "" val obj = other as ProfileItem
copy.addedTime = 0L
return copy 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
)
} }
} }
@@ -9,5 +9,5 @@ data class RulesetItem(
var network: String? = null, var network: String? = null,
var protocol: List<String>? = null, var protocol: List<String>? = null,
var enabled: Boolean = true, var enabled: Boolean = true,
var looked: Boolean? = false, var locked: Boolean? = false,
) )
@@ -195,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 = AppConfig.DEFAULT_NETWORK, var network: String = AppConfig.DEFAULT_NETWORK,
var security: String = "", var security: String? = null,
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,
@@ -275,11 +276,11 @@ data class V2rayConfig(
val acceptProxyProtocol: Boolean? = null val acceptProxyProtocol: Boolean? = null
) )
data class SplithttpSettingsBean( data class XhttpSettingsBean(
var path: String? = null, var path: String? = null,
var host: String? = null, 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(
@@ -344,14 +345,21 @@ 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: String? = null var sni: String? = null
network = transport network = if (transport.isEmpty()) NetworkType.TCP.type else transport
when (network) { when (network) {
"tcp" -> { NetworkType.TCP.type -> {
val tcpSetting = TcpSettingsBean() val tcpSetting = TcpSettingsBean()
if (headerType == AppConfig.HEADER_TYPE_HTTP) { if (headerType == AppConfig.HEADER_TYPE_HTTP) {
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
@@ -360,16 +368,16 @@ data class V2rayConfig(
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)
} }
} else { } else {
tcpSetting.header.type = "none" tcpSetting.header.type = "none"
sni = host.orEmpty() sni = host
} }
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()) {
@@ -380,55 +388,55 @@ 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 = host
wssetting.path = path ?: "/" wssetting.path = path ?: "/"
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 = host
httpupgradeSetting.path = path ?: "/" httpupgradeSetting.path = path ?: "/"
httpupgradeSettings = httpupgradeSetting httpupgradeSettings = httpupgradeSetting
} }
"splithttp" -> { NetworkType.XHTTP.type -> {
val splithttpSetting = SplithttpSettingsBean() val xhttpSetting = XhttpSettingsBean()
splithttpSetting.host = host.orEmpty() xhttpSetting.host = host.orEmpty()
sni = splithttpSetting.host sni = 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)
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()
grpcSetting.authority = authority.orEmpty() grpcSetting.authority = authority.orEmpty()
grpcSetting.idle_timeout = 60 grpcSetting.idle_timeout = 60
grpcSetting.health_check_timeout = 20 grpcSetting.health_check_timeout = 20
sni = authority.orEmpty() sni = authority
grpcSettings = grpcSetting grpcSettings = grpcSetting
} }
} }
@@ -436,18 +444,25 @@ 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 = if (streamSecurity.isEmpty()) null else streamSecurity
if (security == null) return
val tlsSetting = TlsSettingsBean( val tlsSetting = TlsSettingsBean(
allowInsecure = allowInsecure, allowInsecure = allowInsecure,
serverName = sni, serverName = if (sni.isNullOrEmpty()) null else sni,
fingerprint = fingerprint, fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() }, alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
publicKey = publicKey, publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
shortId = shortId, shortId = if (shortId.isNullOrEmpty()) null else shortId,
spiderX = spiderX spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
) )
if (security == AppConfig.TLS) { if (security == AppConfig.TLS) {
tlsSettings = tlsSetting tlsSettings = tlsSetting
@@ -470,16 +485,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
} }
@@ -488,16 +503,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
} }
@@ -512,16 +527,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
} }
@@ -530,9 +545,9 @@ 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
} }
} }
@@ -545,7 +560,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,
@@ -554,7 +569,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,
@@ -563,7 +578,7 @@ data class V2rayConfig(
) )
} }
"ws" -> { NetworkType.WS.type -> {
val wsSetting = streamSettings?.wsSettings ?: return null val wsSetting = streamSettings?.wsSettings ?: return null
listOf( listOf(
"", "",
@@ -572,7 +587,7 @@ data class V2rayConfig(
) )
} }
"httpupgrade" -> { NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null val httpupgradeSetting = streamSettings?.httpupgradeSettings ?: return null
listOf( listOf(
"", "",
@@ -581,16 +596,16 @@ data class V2rayConfig(
) )
} }
"splithttp" -> { 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(
"", "",
@@ -599,16 +614,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",
@@ -95,4 +95,4 @@ inline fun <reified T : Serializable> Intent.serializable(key: String): T? = whe
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
} }
inline fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty()) fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
@@ -30,6 +30,39 @@ open class FmtBase {
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } .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
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"]
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
config.security = null
}
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> { fun getQueryDic(config: ProfileItem): HashMap<String, String> {
val dicQuery = HashMap<String, String>() val dicQuery = HashMap<String, String>()
dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty() dicQuery["security"] = config.security?.ifEmpty { "none" }.orEmpty()
@@ -55,22 +88,29 @@ open class FmtBase {
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() } config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
} }
NetworkType.WS, NetworkType.HTTP_UPGRADE, NetworkType.SPLIT_HTTP -> { NetworkType.WS, NetworkType.HTTP_UPGRADE -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() } config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() } config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
} }
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 -> { NetworkType.HTTP, NetworkType.H2 -> {
dicQuery["type"] = "http" dicQuery["type"] = "http"
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() } config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() } config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
} }
NetworkType.QUIC -> { // NetworkType.QUIC -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty() // dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() } // config.quicSecurity.let { if (it.isNotNullEmpty()) dicQuery["quicSecurity"] = it.orEmpty() }
config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() } // config.quicKey.let { if (it.isNotNullEmpty()) dicQuery["key"] = it.orEmpty() }
} // }
NetworkType.GRPC -> { NetworkType.GRPC -> {
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() } config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
@@ -10,7 +10,7 @@ object HttpFmt : FmtBase() {
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HTTP) val outboundBean = OutboundBean.create(EConfigType.HTTP)
outboundBean?.settings?.servers?.get(0)?.let { server -> outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty() server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt() server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) { if (profileItem.username.isNotNullEmpty()) {
@@ -0,0 +1,139 @@
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"]?.contains("obfs=http") == true) {
val queryPairs = HashMap<String, String>()
for (pair in queryParam["plugin"]?.split(";") ?: listOf()) {
val idx = pair.split("=")
if (idx.count() == 2) {
queryPairs.put(idx.first(), idx.last())
}
}
config.network = NetworkType.TCP.type
config.headerType = "http"
config.host = queryPairs["obfs-host"]
config.path = queryPairs["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
}
val sni = 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,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
return outboundBean
}
}
@@ -15,6 +15,7 @@ object SocksFmt : FmtBase() {
val uri = URI(Utils.fixIllegalUrl(str)) val uri = URI(Utils.fixIllegalUrl(str))
if (uri.idnHost.isEmpty()) return null if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.server = uri.idnHost config.server = uri.idnHost
@@ -38,13 +39,13 @@ object SocksFmt : FmtBase() {
else else
":" ":"
return toUri(config, pw, null) return toUri(config, Utils.encode(pw), null)
} }
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SOCKS) val outboundBean = OutboundBean.create(EConfigType.SOCKS)
outboundBean?.settings?.servers?.get(0)?.let { server -> outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty() server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt() server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) { if (profileItem.username.isNotNullEmpty()) {
@@ -2,6 +2,7 @@ package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
@@ -22,37 +23,14 @@ object TrojanFmt : FmtBase() {
config.password = uri.userInfo config.password = uri.userInfo
if (uri.rawQuery.isNullOrEmpty()) { if (uri.rawQuery.isNullOrEmpty()) {
config.network = NetworkType.TCP.type
config.security = AppConfig.TLS config.security = AppConfig.TLS
config.insecure = allowInsecure config.insecure = allowInsecure
} else { } else {
val queryParam = getQueryParam(uri) val queryParam = getQueryParam(uri)
config.network = queryParam["type"] ?: "tcp" getItemFormQuery(config, queryParam, allowInsecure)
config.headerType = queryParam["headerType"] config.security = queryParam["security"] ?: AppConfig.TLS
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.security = queryParam["security"]
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["allowInsecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
config.publicKey = queryParam["pbk"]
config.shortId = queryParam["sid"]
config.spiderX = queryParam["spx"]
config.flow = queryParam["flow"]
} }
return config return config
@@ -67,14 +45,14 @@ object TrojanFmt : FmtBase() {
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.TROJAN) val outboundBean = OutboundBean.create(EConfigType.TROJAN)
outboundBean?.settings?.servers?.get(0)?.let { server -> outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty() server.address = profileItem.server.orEmpty()
server.port = profileItem.serverPort.orEmpty().toInt() server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password server.password = profileItem.password
server.flow = profileItem.flow server.flow = profileItem.flow
} }
outboundBean?.streamSettings?.populateTransportSettings( val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(), profileItem.network.orEmpty(),
profileItem.headerType, profileItem.headerType,
profileItem.host, profileItem.host,
@@ -90,7 +68,7 @@ object TrojanFmt : FmtBase() {
outboundBean?.streamSettings?.populateTlsSettings( outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(), profileItem.security.orEmpty(),
profileItem.insecure == true, profileItem.insecure == true,
profileItem.sni, if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint, profileItem.fingerPrint,
profileItem.alpn, profileItem.alpn,
profileItem.publicKey, profileItem.publicKey,
@@ -6,6 +6,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -25,31 +26,7 @@ object VlessFmt : FmtBase() {
config.password = uri.userInfo config.password = uri.userInfo
config.method = queryParam["encryption"] ?: "none" config.method = queryParam["encryption"] ?: "none"
config.network = queryParam["type"] ?: "tcp" getItemFormQuery(config, queryParam, allowInsecure)
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.security = queryParam["security"]
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
allowInsecure
} else {
queryParam["allowInsecure"].orEmpty() == "1"
}
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
config.publicKey = queryParam["pbk"]
config.shortId = queryParam["sid"]
config.spiderX = queryParam["spx"]
config.flow = queryParam["flow"]
return config return config
} }
@@ -65,7 +42,7 @@ object VlessFmt : FmtBase() {
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VLESS) val outboundBean = OutboundBean.create(EConfigType.VLESS)
outboundBean?.settings?.vnext?.get(0)?.let { vnext -> outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty() vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt() vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty() vnext.users[0].id = profileItem.password.orEmpty()
@@ -73,7 +50,7 @@ object VlessFmt : FmtBase() {
vnext.users[0].flow = profileItem.flow vnext.users[0].flow = profileItem.flow
} }
outboundBean?.streamSettings?.populateTransportSettings( val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(), profileItem.network.orEmpty(),
profileItem.headerType, profileItem.headerType,
profileItem.host, profileItem.host,
@@ -85,11 +62,13 @@ object VlessFmt : FmtBase() {
profileItem.serviceName, profileItem.serviceName,
profileItem.authority, profileItem.authority,
) )
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
outboundBean?.streamSettings?.populateTlsSettings( outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(), profileItem.security.orEmpty(),
profileItem.insecure == true, profileItem.insecure == true,
profileItem.sni, if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint, profileItem.fingerPrint,
profileItem.alpn, profileItem.alpn,
profileItem.publicKey, profileItem.publicKey,
@@ -48,7 +48,7 @@ object VmessFmt : FmtBase() {
config.password = vmessQRCode.id config.password = vmessQRCode.id
config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy config.method = if (TextUtils.isEmpty(vmessQRCode.scy)) AppConfig.DEFAULT_SECURITY else vmessQRCode.scy
config.network = vmessQRCode.net ?: "tcp" config.network = vmessQRCode.net ?: NetworkType.TCP.type
config.headerType = vmessQRCode.type config.headerType = vmessQRCode.type
config.host = vmessQRCode.host config.host = vmessQRCode.host
config.path = vmessQRCode.path config.path = vmessQRCode.path
@@ -58,10 +58,10 @@ object VmessFmt : FmtBase() {
config.seed = vmessQRCode.path config.seed = vmessQRCode.path
} }
NetworkType.QUIC -> { // NetworkType.QUIC -> {
config.quicSecurity = vmessQRCode.host // config.quicSecurity = vmessQRCode.host
config.quicKey = vmessQRCode.path // config.quicKey = vmessQRCode.path
} // }
NetworkType.GRPC -> { NetworkType.GRPC -> {
config.mode = vmessQRCode.type config.mode = vmessQRCode.type
@@ -98,10 +98,10 @@ object VmessFmt : FmtBase() {
vmessQRCode.path = config.seed.orEmpty() vmessQRCode.path = config.seed.orEmpty()
} }
NetworkType.QUIC -> { // NetworkType.QUIC -> {
vmessQRCode.host = config.quicSecurity.orEmpty() // vmessQRCode.host = config.quicSecurity.orEmpty()
vmessQRCode.path = config.quicKey.orEmpty() // vmessQRCode.path = config.quicKey.orEmpty()
} // }
NetworkType.GRPC -> { NetworkType.GRPC -> {
vmessQRCode.type = config.mode.orEmpty() vmessQRCode.type = config.mode.orEmpty()
@@ -137,23 +137,7 @@ object VmessFmt : FmtBase() {
config.password = uri.userInfo config.password = uri.userInfo
config.method = AppConfig.DEFAULT_SECURITY config.method = AppConfig.DEFAULT_SECURITY
config.network = NetworkType.fromString(queryParam["type"]).name getItemFormQuery(config, queryParam, allowInsecure)
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
config.path = queryParam["path"]
config.seed = queryParam["seed"]
config.quicSecurity = queryParam["quicSecurity"]
config.quicKey = queryParam["key"]
config.mode = queryParam["mode"]
config.serviceName = queryParam["serviceName"]
config.authority = queryParam["authority"]
config.security = queryParam["security"]
config.insecure = if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
return config return config
} }
@@ -162,14 +146,14 @@ object VmessFmt : FmtBase() {
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VMESS) val outboundBean = OutboundBean.create(EConfigType.VMESS)
outboundBean?.settings?.vnext?.get(0)?.let { vnext -> outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty() vnext.address = profileItem.server.orEmpty()
vnext.port = profileItem.serverPort.orEmpty().toInt() vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty() vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].security = profileItem.method vnext.users[0].security = profileItem.method
} }
outboundBean?.streamSettings?.populateTransportSettings( val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(), profileItem.network.orEmpty(),
profileItem.headerType, profileItem.headerType,
profileItem.host, profileItem.host,
@@ -185,7 +169,7 @@ object VmessFmt : FmtBase() {
outboundBean?.streamSettings?.populateTlsSettings( outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(), profileItem.security.orEmpty(),
profileItem.insecure == true, profileItem.insecure == true,
profileItem.sni, if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint, profileItem.fingerPrint,
profileItem.alpn, profileItem.alpn,
null, null,
@@ -22,9 +22,10 @@ object WireguardFmt : FmtBase() {
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo config.secretKey = uri.userInfo.orEmpty()
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4) config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
config.publicKey = queryParam["publickey"].orEmpty() config.publicKey = queryParam["publickey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"].orEmpty()
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = (queryParam["reserved"] ?: "0,0,0") config.reserved = (queryParam["reserved"] ?: "0,0,0")
@@ -33,33 +34,75 @@ object WireguardFmt : FmtBase() {
fun parseWireguardConfFile(str: String): ProfileItem? { fun parseWireguardConfFile(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD) val config = ProfileItem.create(EConfigType.WIREGUARD)
val queryParam: MutableMap<String, String> = mutableMapOf()
val interfaceParams: MutableMap<String, String> = mutableMapOf()
val peerParams: MutableMap<String, String> = mutableMapOf()
var currentSection: String? = null var currentSection: String? = null
str.lines().forEach { line -> str.lines().forEach { line ->
val trimmedLine = line.trim() val trimmedLine = line.trim()
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
return@forEach
}
when { when {
trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface" trimmedLine.startsWith("[Interface]", ignoreCase = true) -> currentSection = "Interface"
trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer" trimmedLine.startsWith("[Peer]", ignoreCase = true) -> currentSection = "Peer"
trimmedLine.isBlank() || trimmedLine.startsWith("#") -> Unit // Skip blank lines or comments else -> {
currentSection != null -> { if (currentSection != null) {
val (key, value) = trimmedLine.split("=").map { it.trim() } val parts = trimmedLine.split("=", limit = 2).map { it.trim() }
queryParam[key.lowercase()] = value // Store the key in lowercase for case-insensitivity 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 = queryParam["privatekey"].orEmpty() config.secretKey = interfaceParams["privatekey"].orEmpty()
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4) config.remarks = System.currentTimeMillis().toString()
config.publicKey = queryParam["publickey"].orEmpty() config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = (queryParam["reserved"] ?: "0,0,0") 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 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 { fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>() val dicQuery = HashMap<String, String>()
@@ -72,24 +115,10 @@ object WireguardFmt : FmtBase() {
if (config.mtu != null) { if (config.mtu != null) {
dicQuery["mtu"] = config.mtu.toString() dicQuery["mtu"] = config.mtu.toString()
} }
if (config.preSharedKey != null) {
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty()
}
return toUri(config, config.secretKey, dicQuery) return toUri(config, config.secretKey, dicQuery)
} }
}
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.get(0)?.publicKey = profileItem.publicKey.orEmpty()
wireguard.peers?.get(0)?.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
wireguard.mtu = profileItem.mtu?.toInt()
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
}
return outboundBean
}
}
@@ -138,11 +138,11 @@ object AngConfigManager {
if (sb.count() > 0) { if (sb.count() > 0) {
Utils.setClipboard(context, sb.toString()) Utils.setClipboard(context, sb.toString())
} }
return sb.lines().count()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return -1 return -1
} }
return 0
} }
/** /**
@@ -170,6 +170,13 @@ object AngConfigManager {
if (guid == null) return -1 if (guid == null) return -1
val result = V2rayConfigManager.getV2rayConfig(context, guid) val result = V2rayConfigManager.getV2rayConfig(context, guid)
if (result.status) { if (result.status) {
val config = MmkvManager.decodeServerConfig(guid)
if (config?.configType == EConfigType.HYSTERIA2) {
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort)
Utils.setClipboard(context, JsonUtil.toJsonPretty(hy2Config) + "\n" + result.content)
return 0
}
Utils.setClipboard(context, result.content) Utils.setClipboard(context, result.content)
} else { } else {
return -1 return -1
@@ -280,7 +287,7 @@ object AngConfigManager {
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
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
@@ -5,6 +5,7 @@ import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
@@ -36,7 +37,7 @@ object MigrateManager {
//check and remove old //check and remove old
decodeServerConfig(guid) ?: continue decodeServerConfig(guid) ?: continue
//serverStorage.remove(guid) serverStorage.remove(guid)
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks) Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + config.remarks)
} }
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end") Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end")
@@ -70,9 +71,9 @@ object MigrateManager {
config.serverPort = outbound.getServerPort().toString() config.serverPort = outbound.getServerPort().toString()
config.method = outbound.getSecurityEncryption() config.method = outbound.getSecurityEncryption()
config.password = outbound.getPassword() config.password = outbound.getPassword()
config.flow = outbound?.settings?.vnext?.get(0)?.users?.get(0)?.flow ?: outbound?.settings?.servers?.get(0)?.flow config.flow = outbound?.settings?.vnext?.first()?.users?.first()?.flow ?: outbound?.settings?.servers?.first()?.flow
config.network = outbound?.streamSettings?.network ?: "tcp" config.network = outbound?.streamSettings?.network ?: NetworkType.TCP.type
outbound.getTransportSettingDetails()?.let { transportDetails -> outbound.getTransportSettingDetails()?.let { transportDetails ->
config.headerType = transportDetails[0].orEmpty() config.headerType = transportDetails[0].orEmpty()
config.host = transportDetails[1].orEmpty() config.host = transportDetails[1].orEmpty()
@@ -107,7 +108,7 @@ object MigrateManager {
config.remarks = configOld.remarks config.remarks = configOld.remarks
config.server = outbound.getServerAddress() config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString() config.serverPort = outbound.getServerPort().toString()
config.username = outbound.settings?.servers?.get(0)?.users?.get(0)?.user config.username = outbound.settings?.servers?.first()?.users?.first()?.user
config.password = outbound.getPassword() config.password = outbound.getPassword()
return config return config
@@ -120,7 +121,7 @@ object MigrateManager {
config.remarks = configOld.remarks config.remarks = configOld.remarks
config.server = outbound.getServerAddress() config.server = outbound.getServerAddress()
config.serverPort = outbound.getServerPort().toString() config.serverPort = outbound.getServerPort().toString()
config.username = outbound.settings?.servers?.get(0)?.users?.get(0)?.user config.username = outbound.settings?.servers?.first()?.users?.first()?.user
config.password = outbound.getPassword() config.password = outbound.getPassword()
return config return config
@@ -164,18 +164,22 @@ object MmkvManager {
} }
} }
fun removeAllServer() { fun removeAllServer(): Int {
val count = profileFullStorage.allKeys()?.count() ?: 0
mainStorage.clearAll() mainStorage.clearAll()
profileFullStorage.clearAll() profileFullStorage.clearAll()
//profileStorage.clearAll() //profileStorage.clearAll()
serverAffStorage.clearAll() serverAffStorage.clearAll()
return count
} }
fun removeInvalidServer(guid: String) { fun removeInvalidServer(guid: String): Int {
var count = 0
if (guid.isNotEmpty()) { if (guid.isNotEmpty()) {
decodeServerAffiliationInfo(guid)?.let { aff -> decodeServerAffiliationInfo(guid)?.let { aff ->
if (aff.testDelayMillis < 0L) { if (aff.testDelayMillis < 0L) {
removeServer(guid) removeServer(guid)
count++
} }
} }
} else { } else {
@@ -183,10 +187,12 @@ object MmkvManager {
decodeServerAffiliationInfo(key)?.let { aff -> decodeServerAffiliationInfo(key)?.let { aff ->
if (aff.testDelayMillis < 0L) { if (aff.testDelayMillis < 0L) {
removeServer(key) removeServer(key)
count++
} }
} }
} }
} }
return count
} }
fun encodeServerRaw(guid: String, config: String) { fun encodeServerRaw(guid: String, config: String) {
@@ -336,7 +342,7 @@ object MmkvManager {
} }
fun decodeSettingsBool(key: String): Boolean { fun decodeSettingsBool(key: String): Boolean {
return settingsStorage.decodeBool(key,false) return settingsStorage.decodeBool(key, false)
} }
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean { fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
@@ -43,12 +43,12 @@ object SettingsManager {
} }
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
} }
@@ -70,7 +70,7 @@ object SettingsManager {
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) { private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
val rulesetNew: MutableList<RulesetItem> = mutableListOf() val rulesetNew: MutableList<RulesetItem> = mutableListOf()
MmkvManager.decodeRoutingRulesets()?.forEach { key -> MmkvManager.decodeRoutingRulesets()?.forEach { key ->
if (key.looked == true) { if (key.locked == true) {
rulesetNew.add(key) rulesetNew.add(key)
} }
} }
@@ -95,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
} }
@@ -155,7 +155,7 @@ object SettingsManager {
} }
fun getHttpPort(): Int { fun getHttpPort(): Int {
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt()) return getSocksPort() + (if (Utils.isXray()) 0 else 1)
} }
fun initAssets(context: Context, assets: AssetManager) { fun initAssets(context: Context, assets: AssetManager) {
@@ -8,12 +8,16 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_NETWORK import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN 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_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_ADDRESSES import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_ONE_ONE_DOMAIN import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN
import com.v2ray.ang.AppConfig.DNS_PUB_ADDRESSES import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES
import com.v2ray.ang.AppConfig.DNS_PUB_DOMAIN 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.GEOSITE_PRIVATE import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
@@ -30,10 +34,12 @@ 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.NetworkType
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.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.fmt.HttpFmt import com.v2ray.ang.fmt.HttpFmt
import com.v2ray.ang.fmt.Hysteria2Fmt import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.fmt.ShadowsocksFmt import com.v2ray.ang.fmt.ShadowsocksFmt
@@ -57,7 +63,7 @@ object V2rayConfigManager {
} }
val result = getV2rayNonCustomConfig(context, config) val result = getV2rayNonCustomConfig(context, config)
Log.d(ANG_PACKAGE, result.content) //Log.d(ANG_PACKAGE, result.content)
result.guid = guid result.guid = guid
return result return result
} catch (e: Exception) { } catch (e: Exception) {
@@ -116,7 +122,6 @@ object V2rayConfigManager {
private fun inbounds(v2rayConfig: V2rayConfig): Boolean { private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
try { try {
val socksPort = SettingsManager.getSocksPort() val socksPort = SettingsManager.getSocksPort()
val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds.forEach { curInbound -> v2rayConfig.inbounds.forEach { curInbound ->
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
@@ -138,14 +143,13 @@ object V2rayConfigManager {
v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns") v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
} }
v2rayConfig.inbounds[1].port = httpPort if (Utils.isXray()) {
v2rayConfig.inbounds.removeAt(1)
} else {
val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds[1].port = httpPort
}
// if (httpPort > 0) {
// val httpCopy = v2rayConfig.inbounds[0].copy()
// httpCopy.port = httpPort
// httpCopy.protocol = "http"
// v2rayConfig.inbounds.add(httpCopy)
// }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return false return false
@@ -236,7 +240,7 @@ object V2rayConfigManager {
val rulesetItems = MmkvManager.decodeRoutingRulesets() val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key -> rulesetItems?.forEach { key ->
if (key != null && key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) { if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
key.domain?.forEach { key.domain?.forEach {
if (it != GEOSITE_PRIVATE if (it != GEOSITE_PRIVATE
&& (it.startsWith("geosite:") || it.startsWith("domain:")) && (it.startsWith("geosite:") || it.startsWith("domain:"))
@@ -330,11 +334,11 @@ object V2rayConfigManager {
remoteDns.forEach { remoteDns.forEach {
servers.add(it) servers.add(it)
} }
if (proxyDomain.size > 0) { if (proxyDomain.isNotEmpty()) {
servers.add( servers.add(
V2rayConfig.DnsBean.ServersBean( V2rayConfig.DnsBean.ServersBean(
address = remoteDns.first(), address = remoteDns.first(),
domains = proxyDomain, domains = proxyDomain,
) )
) )
} }
@@ -344,7 +348,7 @@ object V2rayConfigManager {
val directDomain = userRule2Domain(TAG_DIRECT) val directDomain = userRule2Domain(TAG_DIRECT)
val isCnRoutingMode = directDomain.contains(GEOSITE_CN) val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
val geoipCn = arrayListOf(GEOIP_CN) val geoipCn = arrayListOf(GEOIP_CN)
if (directDomain.size > 0) { if (directDomain.isNotEmpty()) {
servers.add( servers.add(
V2rayConfig.DnsBean.ServersBean( V2rayConfig.DnsBean.ServersBean(
address = domesticDns.first(), address = domesticDns.first(),
@@ -366,9 +370,23 @@ object V2rayConfigManager {
) )
} }
//User DNS hosts
try {
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
if (userHosts.isNotNullEmpty()) {
var userHostsMap = userHosts?.split(",")
?.filter { it.isNotEmpty() }
?.filter { it.contains(":") }
?.associate { it.split(":").let { (k, v) -> k to v } }
if (userHostsMap != null) hosts.putAll(userHostsMap)
}
} catch (e: Exception) {
e.printStackTrace()
}
//block dns //block dns
val blkDomain = userRule2Domain(TAG_BLOCKED) val blkDomain = userRule2Domain(TAG_BLOCKED)
if (blkDomain.size > 0) { if (blkDomain.isNotEmpty()) {
hosts.putAll(blkDomain.map { it to LOOPBACK }) hosts.putAll(blkDomain.map { it to LOOPBACK })
} }
@@ -376,10 +394,12 @@ object V2rayConfigManager {
hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN 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_DOMAIN] = DNS_PUB_ADDRESSES
hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
hosts[DNS_ONE_ONE_DOMAIN] = DNS_ONE_ONE_ADDRESSES hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_ADDRESSES
hosts[DNS_DNSPOD_DOMAIN] = DNS_DNSPOD_ADDRESSES
hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
// DNS dns对象 // DNS dns对象
@@ -419,9 +439,11 @@ object V2rayConfigManager {
) { ) {
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
} else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
muxEnabled = false
} }
if (muxEnabled == true) { if (muxEnabled == true) {
outbound.mux?.enabled = true outbound.mux?.enabled = true
@@ -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,160 +0,0 @@
/*
* Copyright (C) 2015 Paul Burke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.v2ray.ang.helper;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.view.animation.DecelerateInterpolator;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
/**
* An implementation of {@link ItemTouchHelper.Callback} that enables basic drag & drop and
* swipe-to-dismiss. Drag events are automatically started by an item long-press.<br/>
* </br/>
* Expects the <code>RecyclerView.Adapter</code> to listen for {@link
* ItemTouchHelperAdapter} callbacks and the <code>RecyclerView.ViewHolder</code> to implement
* {@link ItemTouchHelperViewHolder}.
*
* @author Paul Burke (ipaulpro)
*/
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private static final float ALPHA_FULL = 1.0f;
private static final float SWIPE_THRESHOLD = 0.25f;
private static final long ANIMATION_DURATION = 200;
private final ItemTouchHelperAdapter mAdapter;
private ValueAnimator mReturnAnimator;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
mAdapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
} else {
final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder source, @NonNull RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
mAdapter.onItemMove(source.getBindingAdapterPosition(), target.getBindingAdapterPosition());
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
// 不执行删除操作,仅返回项目到原位
returnViewToOriginalPosition(viewHolder);
}
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float maxSwipeDistance = viewHolder.itemView.getWidth() * SWIPE_THRESHOLD;
float swipeAmount = Math.abs(dX);
float direction = Math.signum(dX);
// 限制最大滑动距离
float translationX = Math.min(swipeAmount, maxSwipeDistance) * direction;
float alpha = ALPHA_FULL - Math.min(swipeAmount, maxSwipeDistance) / maxSwipeDistance;
viewHolder.itemView.setTranslationX(translationX);
viewHolder.itemView.setAlpha(alpha);
if (swipeAmount >= maxSwipeDistance && isCurrentlyActive) {
returnViewToOriginalPosition(viewHolder);
}
} else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
private void returnViewToOriginalPosition(RecyclerView.ViewHolder viewHolder) {
if (mReturnAnimator != null && mReturnAnimator.isRunning()) {
mReturnAnimator.cancel();
}
mReturnAnimator = ValueAnimator.ofFloat(viewHolder.itemView.getTranslationX(), 0f);
mReturnAnimator.addUpdateListener(animation -> {
float value = (float) animation.getAnimatedValue();
viewHolder.itemView.setTranslationX(value);
viewHolder.itemView.setAlpha(1f - Math.abs(value) / (viewHolder.itemView.getWidth() * SWIPE_THRESHOLD));
});
mReturnAnimator.setInterpolator(new DecelerateInterpolator());
mReturnAnimator.setDuration(ANIMATION_DURATION);
mReturnAnimator.start();
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected();
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setAlpha(ALPHA_FULL);
if (viewHolder instanceof ItemTouchHelperViewHolder) {
ItemTouchHelperViewHolder itemViewHolder = (ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear();
}
mAdapter.onItemMoveCompleted();
}
@Override
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return 1.1f; // 设置一个大于1的值,确保不会触发默认的滑动删除操作
}
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
return defaultValue * 10; // 增加滑动逃逸速度,使得更难触发滑动
}
}
@@ -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
}
}
@@ -222,7 +222,7 @@ object PluginManager {
return File(pluginDir, pluginId).absolutePath return File(pluginDir, pluginId).absolutePath
} }
fun ComponentInfo.loadString(key: String) = when (val value = metaData.get(key)) { fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
is String -> value is String -> value
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo) is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
.getString(value) .getString(value)
@@ -22,6 +22,7 @@
package com.v2ray.ang.plugin package com.v2ray.ang.plugin
import android.content.pm.ComponentInfo import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
@@ -33,13 +34,18 @@ abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin()
override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! } override val id by lazy { componentInfo.loadString(PluginContract.METADATA_KEY_ID)!! }
override val version by lazy { override val version by lazy {
AngApplication.application.getPackageInfo(componentInfo.packageName).versionCode getPackageInfo(componentInfo.packageName).versionCode
} }
override val versionName: String by lazy { override val versionName: String by lazy {
AngApplication.application.getPackageInfo(componentInfo.packageName).versionName!! getPackageInfo(componentInfo.packageName).versionName!!
} }
override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager) override val label: CharSequence get() = resolveInfo.loadLabel(AngApplication.application.packageManager)
override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager) override val icon: Drawable get() = resolveInfo.loadIcon(AngApplication.application.packageManager)
override val packageName: String get() = componentInfo.packageName override val packageName: String get() = componentInfo.packageName
override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware override val directBootAware get() = Build.VERSION.SDK_INT < 24 || componentInfo.directBootAware
fun getPackageInfo(packageName: String) = AngApplication.application.packageManager.getPackageInfo(
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
)!!
} }
@@ -12,7 +12,6 @@ import android.service.quicksettings.TileService
import androidx.core.content.ContextCompat 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.extension.v2RayApplication
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
@@ -41,23 +40,24 @@ class QSTileService : TileService() {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
setState(Tile.STATE_INACTIVE) setState(Tile.STATE_INACTIVE)
mMsgReceive = ReceiveMessageHandler(this) mMsgReceive = ReceiveMessageHandler(this)
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY) val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
ContextCompat.registerReceiver(applicationContext,mMsgReceive,mFilter,ContextCompat.RECEIVER_EXPORTED)
} else {
ContextCompat.registerReceiver(applicationContext,mMsgReceive,mFilter,ContextCompat.RECEIVER_NOT_EXPORTED)
}
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() {
@@ -4,6 +4,7 @@ import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -44,6 +45,7 @@ object V2RayServiceManager {
private const val NOTIFICATION_ID = 1 private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0 private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1 private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
private const val NOTIFICATION_PENDING_INTENT_RESTART_V2RAY = 2
private const val NOTIFICATION_ICON_THRESHOLD = 3000 private const val NOTIFICATION_ICON_THRESHOLD = 3000
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
@@ -148,17 +150,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())
ContextCompat.registerReceiver(
service, mMsgReceive, mFilter,
ContextCompat.RECEIVER_EXPORTED
)
} else {
ContextCompat.registerReceiver(
service, mMsgReceive, mFilter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
@@ -229,11 +221,15 @@ object V2RayServiceManager {
} }
AppConfig.MSG_STATE_STOP -> { AppConfig.MSG_STATE_STOP -> {
Log.d(ANG_PACKAGE, "Stop Service")
serviceControl.stopService() serviceControl.stopService()
} }
AppConfig.MSG_STATE_RESTART -> { AppConfig.MSG_STATE_RESTART -> {
startV2rayPoint() Log.d(ANG_PACKAGE, "Restart Service")
serviceControl.stopService()
Thread.sleep(500L)
startV2Ray(serviceControl.getService())
} }
AppConfig.MSG_MEASURE_DELAY -> { AppConfig.MSG_MEASURE_DELAY -> {
@@ -288,30 +284,24 @@ object V2RayServiceManager {
private fun showNotification() { private fun showNotification() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val startMainIntent = Intent(service, MainActivity::class.java) val startMainIntent = Intent(service, MainActivity::class.java)
val contentPendingIntent = PendingIntent.getActivity( val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
service,
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE) val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
stopV2RayIntent.`package` = ANG_PACKAGE stopV2RayIntent.`package` = ANG_PACKAGE
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP) stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
val stopV2RayPendingIntent = PendingIntent.getBroadcast( val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
service, restartV2RayIntent.`package` = ANG_PACKAGE
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val channelId = val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -335,6 +325,11 @@ object V2RayServiceManager {
service.getString(R.string.notification_action_stop_v2ray), service.getString(R.string.notification_action_stop_v2ray),
stopV2RayPendingIntent stopV2RayPendingIntent
) )
.addAction(
R.drawable.ic_delete_24dp,
service.getString(R.string.title_service_restart),
restartV2RayPendingIntent
)
//.build() //.build()
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使 //mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
@@ -359,7 +354,12 @@ object V2RayServiceManager {
fun cancelNotification() { fun cancelNotification() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
service.stopForeground(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
} else {
service.stopForeground(true)
}
mBuilder = null mBuilder = null
mDisposable?.dispose() mDisposable?.dispose()
mDisposable = null mDisposable = null
@@ -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()
@@ -10,6 +10,7 @@ import android.net.LocalSocketAddress
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.net.ProxyInfo
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
@@ -34,10 +35,10 @@ import java.lang.ref.SoftReference
class V2RayVpnService : VpnService(), ServiceControl { class V2RayVpnService : VpnService(), ServiceControl {
companion object { companion object {
private const val VPN_MTU = 1500 private const val VPN_MTU = 1500
private const val PRIVATE_VLAN4_CLIENT = "26.26.26.1" private const val PRIVATE_VLAN4_CLIENT = "10.10.10.1"
private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2" private const val PRIVATE_VLAN4_ROUTER = "10.10.10.2"
private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1" private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:10:1"
private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2" private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:10:2"
private const val TUN2SOCKS = "libtun2socks.so" private const val TUN2SOCKS = "libtun2socks.so"
} }
@@ -190,6 +191,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false) builder.setMetered(false)
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY)) {
builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOOPBACK, SettingsManager.getHttpPort()))
}
} }
// Create a new interface using the builder and save the parameters. // Create a new interface using the builder and save the parameters.
@@ -21,6 +21,7 @@ import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class AboutActivity : BaseActivity() { class AboutActivity : BaseActivity() {
private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) } private val binding by lazy { ActivityAboutBinding.inflate(layoutInflater) }
private val extDir by lazy { File(Utils.backupPath(this)) } private val extDir by lazy { File(Utils.backupPath(this)) }
@@ -87,6 +88,14 @@ class AboutActivity : BaseActivity() {
binding.layoutFeedback.setOnClickListener { binding.layoutFeedback.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGIssues) Utils.openUri(this, AppConfig.v2rayNGIssues)
} }
binding.layoutOssLicenses.setOnClickListener{
val webView = android.webkit.WebView(this);
webView.loadUrl("file:///android_asset/open_source_licenses.html");
android.app.AlertDialog.Builder(this)
.setTitle("Open source licenses")
.setView(webView)
.setPositiveButton("OK", android.content.DialogInterface.OnClickListener { dialog, whichButton -> dialog.dismiss() }).show()
}
binding.layoutTgChannel.setOnClickListener { binding.layoutTgChannel.setOnClickListener {
Utils.openUri(this, AppConfig.TgChannelUrl) Utils.openUri(this, AppConfig.TgChannelUrl)
@@ -19,19 +19,42 @@ import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
class LogcatActivity : BaseActivity() { class LogcatActivity : BaseActivity() {
private val binding by lazy { private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
ActivityLogcatBinding.inflate(layoutInflater) private val throttleManager = ThrottleManager()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
title = getString(R.string.title_logcat) title = getString(R.string.title_logcat)
logcat(false) logcat(false)
} }
class ThrottleManager {
private val throttleMap = mutableMapOf<String, Long>()
companion object {
private const val THROTTLE_DURATION = 1000L
}
@Synchronized
fun shouldProcess(key: String): Boolean {
val currentTime = System.currentTimeMillis()
val lastProcessTime = throttleMap[key] ?: 0L
return if (currentTime - lastProcessTime > THROTTLE_DURATION) {
throttleMap[key] = currentTime
true
} else {
false
}
}
@Synchronized
fun reset(key: String) {
throttleMap.remove(key)
}
}
private fun logcat(shouldFlushLog: Boolean) { private fun logcat(shouldFlushLog: Boolean) {
binding.pbWaiting.visibility = View.VISIBLE binding.pbWaiting.visibility = View.VISIBLE
@@ -44,20 +67,23 @@ class LogcatActivity : BaseActivity() {
process.waitFor() process.waitFor()
} }
} }
val lst = linkedSetOf( val lst = linkedSetOf(
"logcat", "-d", "-v", "time", "-s", "logcat", "-d", "-v", "time", "-s",
"GoLog,tun2socks,$ANG_PACKAGE,AndroidRuntime,System.err" "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 allText = process.inputStream.bufferedReader().use { it.readText() }
val allLogs = process.inputStream.bufferedReader().use { it.readLines() }
val filteredLogs = processLogs(allLogs)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.tvLogcat.text = allText updateLogDisplay(filteredLogs)
binding.tvLogcat.movementMethod = ScrollingMovementMethod()
binding.pbWaiting.visibility = View.GONE
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) }
} }
} catch (e: IOException) { } catch (e: IOException) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.pbWaiting.visibility = View.GONE binding.pbWaiting.visibility = View.GONE
@@ -68,6 +94,36 @@ class LogcatActivity : BaseActivity() {
} }
} }
private fun processLogs(logs: List<String>): List<String> {
val processedLogs = mutableListOf<String>()
var isNotMatch = false
for (line in logs) {
when {
line.contains("zxing.NotFoundException", ignoreCase = true) -> {
if (!isNotMatch) {
if (throttleManager.shouldProcess("NotFoundException")) {
processedLogs.add(line)
isNotMatch = true
}
}
}
else -> processedLogs.add(line)
}
}
return processedLogs.take(500)
}
private fun updateLogDisplay(logs: List<String>) {
binding.tvLogcat.text = logs.joinToString("\n")
binding.tvLogcat.movementMethod = ScrollingMovementMethod()
binding.pbWaiting.visibility = View.GONE
Handler(Looper.getMainLooper()).post {
binding.svLogcat.fullScroll(View.FOCUS_DOWN)
}
}
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)
@@ -80,12 +136,11 @@ class LogcatActivity : BaseActivity() {
toast(R.string.toast_success) toast(R.string.toast_success)
true true
} }
R.id.clear_all -> { R.id.clear_all -> {
throttleManager.reset("zxing.NotFoundException")
logcat(true) logcat(true)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
@@ -346,8 +346,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.exportAllServer() val ret = mainViewModel.exportAllServer()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (ret == 0) if (ret > 0)
toast(R.string.toast_success) toast(getString(R.string.title_export_config_count, ret))
else else
toast(R.string.toast_failure) toast(R.string.toast_failure)
binding.pbWaiting.hide() binding.pbWaiting.hide()
@@ -358,13 +358,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
R.id.ping_all -> { R.id.ping_all -> {
toast(R.string.connection_test_testing) toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
mainViewModel.testAllTcping() mainViewModel.testAllTcping()
true true
} }
R.id.real_ping_all -> { R.id.real_ping_all -> {
toast(R.string.connection_test_testing) toast(getString(R.string.connection_test_testing_count, mainViewModel.serversCache.count()))
mainViewModel.testAllRealPing() mainViewModel.testAllRealPing()
true true
} }
@@ -379,14 +379,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show() binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.removeAllServer() val ret = mainViewModel.removeAllServer()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
toast(getString(R.string.title_del_config_count, ret))
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting //do noting
} }
.show() .show()
@@ -406,7 +407,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting //do noting
} }
.show() .show()
@@ -418,14 +419,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show() binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.removeInvalidServer() val ret = mainViewModel.removeInvalidServer()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
toast(getString(R.string.title_del_config_count, ret))
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting //do noting
} }
.show() .show()
@@ -517,7 +519,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
when { when {
count > 0 -> { count > 0 -> {
toast(R.string.toast_success) toast(getString(R.string.title_import_config_count, count))
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
} }
@@ -623,7 +625,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
delay(500L) delay(500L)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (count > 0) { if (count > 0) {
toast(R.string.toast_success) toast(getString(R.string.title_update_config_count, count))
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
} else { } else {
toast(R.string.toast_failure) toast(R.string.toast_failure)
@@ -143,7 +143,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
removeServer(guid, position) removeServer(guid, position)
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting //do noting
} }
.show() .show()
@@ -37,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 == true binding.chkLocked.isChecked = rulesetItem.locked == 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)
@@ -60,7 +60,7 @@ class RoutingEditActivity : BaseActivity() {
rulesetItem.apply { rulesetItem.apply {
remarks = binding.etRemarks.text.toString() remarks = binding.etRemarks.text.toString()
looked = binding.chkLocked.isChecked locked = binding.chkLocked.isChecked
domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() } domain = binding.etDomain.text.toString().takeIf { it.isNotEmpty() }
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } ?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() } ip = binding.etIp.text.toString().takeIf { it.isNotEmpty() }
@@ -95,7 +95,7 @@ class RoutingEditActivity : BaseActivity() {
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -1,15 +1,18 @@
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
@@ -83,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)
@@ -102,7 +105,7 @@ class RoutingSettingActivity : BaseActivity() {
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting //do noting
} }
.show() .show()
@@ -120,7 +123,7 @@ class RoutingSettingActivity : BaseActivity() {
return@setPositiveButton return@setPositiveButton
} }
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val result = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard) val result = SettingsManager.resetRoutingRulesets(clipboard)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (result) { if (result) {
refreshData() refreshData()
@@ -131,13 +134,25 @@ class RoutingSettingActivity : BaseActivity() {
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do nothing //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()
@@ -153,6 +168,34 @@ 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.cancel) { _, _ ->
//do nothing
}
.show()
return true
}
fun refreshData() { fun refreshData() {
rulesets.clear() rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf()) rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
@@ -25,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 == true holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true
holder.itemView.setBackgroundColor(Color.TRANSPARENT) holder.itemView.setBackgroundColor(Color.TRANSPARENT)
holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener { holder.itemRoutingSettingBinding.layoutEdit.setOnClickListener {
@@ -24,7 +24,9 @@ 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.NetworkType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
@@ -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
@@ -107,6 +113,7 @@ 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) }
@@ -119,6 +126,8 @@ class ServerActivity : BaseActivity() {
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) } 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_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_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,28 +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?.headerType?.let { it -> NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode)
sp_header_type?.setSelection(Utils.arrayFind(types, it)) else -> getString(R.string.server_lab_head_type)
} }.orEmpty()
config?.host?.let { it -> sp_header_type?.setSelection(
et_request_host?.text = Utils.getEditable(it) Utils.arrayFind(
} types,
config?.path?.let { it -> when (networks[position]) {
et_path?.text = Utils.getEditable(it) NetworkType.GRPC.type -> config?.mode
} 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.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
} }
) )
@@ -180,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.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.XHTTP.type -> config?.xhttpExtra
else -> null
}.orEmpty()
)
layout_extra?.visibility =
when (networks[position]) {
NetworkType.XHTTP.type -> View.VISIBLE
else -> View.GONE
}
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@@ -202,7 +244,7 @@ class ServerActivity : BaseActivity() {
parent: AdapterView<*>?, parent: AdapterView<*>?,
view: View?, view: View?,
position: Int, position: Int,
id: Long id: Long,
) { ) {
val isBlank = streamSecuritys[position].isBlank() val isBlank = streamSecuritys[position].isBlank()
val isTLS = streamSecuritys[position] == TLS val isTLS = streamSecuritys[position] == TLS
@@ -280,29 +322,21 @@ class ServerActivity : BaseActivity() {
} else if (config.configType == EConfigType.WIREGUARD) { } else if (config.configType == EConfigType.WIREGUARD) {
et_id.text = Utils.getEditable(config.secretKey.orEmpty()) et_id.text = Utils.getEditable(config.secretKey.orEmpty())
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty()) et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
if (config.reserved == null) { et_preshared_key?.visibility = View.VISIBLE
et_reserved1?.text = Utils.getEditable("0,0,0") et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
} else { et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
et_reserved1?.text = Utils.getEditable(config.reserved?.toString()) et_local_address?.text = Utils.getEditable(
} config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6"
if (config.localAddress == null) { )
et_local_address?.text = Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}") et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
} else {
et_local_address?.text = Utils.getEditable(config.localAddress)
}
if (config.mtu == null) {
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
} else {
et_local_mtu?.text = Utils.getEditable(config.mtu.toString())
}
} else if (config.configType == EConfigType.HYSTERIA2) { } else if (config.configType == EConfigType.HYSTERIA2) {
et_obfs_password?.text = Utils.getEditable(config.obfsPassword) et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
et_port_hop?.text = Utils.getEditable(config.portHopping) et_port_hop?.text = Utils.getEditable(config.portHopping)
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval) et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
et_pinsha256?.text = Utils.getEditable(config.pinSHA256) et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
} }
val securityEncryptions =
val securityEncryptions = if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty()) val security = Utils.arrayFind(securityEncryptions, config.method.orEmpty())
if (security >= 0) { if (security >= 0) {
sp_security?.setSelection(security) sp_security?.setSelection(security)
@@ -333,7 +367,7 @@ class ServerActivity : BaseActivity() {
container_public_key?.visibility = View.GONE container_public_key?.visibility = View.GONE
container_short_id?.visibility = View.GONE container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE container_spider_x?.visibility = View.GONE
} else if (config.security == REALITY) { // reality settings } else if (config.security == REALITY) {
container_public_key?.visibility = View.VISIBLE container_public_key?.visibility = View.VISIBLE
et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty()) et_public_key?.text = Utils.getEditable(config.publicKey.orEmpty())
container_short_id?.visibility = View.VISIBLE container_short_id?.visibility = View.VISIBLE
@@ -353,7 +387,6 @@ class ServerActivity : BaseActivity() {
container_short_id?.visibility = View.GONE container_short_id?.visibility = View.GONE
container_spider_x?.visibility = View.GONE container_spider_x?.visibility = View.GONE
} }
val network = Utils.arrayFind(networks, config.network.orEmpty()) val network = Utils.arrayFind(networks, config.network.orEmpty())
if (network >= 0) { if (network >= 0) {
sp_network?.setSelection(network) sp_network?.setSelection(network)
@@ -407,7 +440,8 @@ class ServerActivity : BaseActivity() {
return false return false
} }
} }
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.create(createConfigType) val config =
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())
@@ -428,6 +462,12 @@ class ServerActivity : BaseActivity() {
return false return false
} }
} }
if (et_extra?.text?.toString().isNotNullEmpty()) {
if (JsonUtil.parseString(et_extra?.text?.toString()) == null) {
toast(R.string.server_lab_xhttp_extra)
return false
}
}
saveCommon(config) saveCommon(config)
saveStreamSettings(config) saveStreamSettings(config)
@@ -436,7 +476,7 @@ class ServerActivity : BaseActivity() {
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty() config.subscriptionId = subscriptionId.orEmpty()
} }
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config)) Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "")
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
toast(R.string.toast_success) toast(R.string.toast_success)
finish() finish()
@@ -464,6 +504,7 @@ class ServerActivity : BaseActivity() {
} else if (config.configType == EConfigType.WIREGUARD) { } else if (config.configType == EConfigType.WIREGUARD) {
config.secretKey = et_id.text.toString().trim() config.secretKey = et_id.text.toString().trim()
config.publicKey = et_public_key?.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.reserved = et_reserved1?.text.toString().trim()
config.localAddress = et_local_address?.text.toString().trim() config.localAddress = et_local_address?.text.toString().trim()
config.mtu = Utils.parseInt(et_local_mtu?.text.toString()) config.mtu = Utils.parseInt(et_local_mtu?.text.toString())
@@ -492,6 +533,8 @@ class ServerActivity : BaseActivity() {
profileItem.mode = transportTypes(networks[network])[type] profileItem.mode = transportTypes(networks[network])[type]
profileItem.serviceName = path profileItem.serviceName = path
profileItem.authority = requestHost profileItem.authority = requestHost
profileItem.xhttpMode = transportTypes(networks[network])[type]
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
} }
private fun saveTls(config: ProfileItem) { private fun saveTls(config: ProfileItem) {
@@ -506,7 +549,7 @@ class ServerActivity : BaseActivity() {
val allowInsecure = val allowInsecure =
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) { if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE) == true MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE)
} else { } else {
allowinsecures[allowInsecureField].toBoolean() allowinsecures[allowInsecureField].toBoolean()
} }
@@ -523,18 +566,22 @@ class ServerActivity : BaseActivity() {
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.XHTTP.type -> {
xhttpMode
}
else -> { else -> {
arrayOf("---") arrayOf("---")
} }
@@ -553,7 +600,7 @@ class ServerActivity : BaseActivity() {
MmkvManager.removeServer(editGuid) MmkvManager.removeServer(editGuid)
finish() finish()
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -12,10 +12,9 @@ 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.ProfileItem 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.fmt.CustomFmt
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat import me.drakeet.support.toast.ToastCompat
@@ -75,8 +74,8 @@ 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()
@@ -84,7 +83,11 @@ class ServerCustomConfigActivity : BaseActivity() {
} }
val config = MmkvManager.decodeServerConfig(editGuid) ?: ProfileItem.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.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())
@@ -103,7 +106,7 @@ class ServerCustomConfigActivity : BaseActivity() {
MmkvManager.removeServer(editGuid) MmkvManager.removeServer(editGuid)
finish() finish()
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -40,6 +40,7 @@ class SettingsActivity : BaseActivity() {
private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) } private val perAppProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_PER_APP_PROXY) }
private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) } private val localDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_LOCAL_DNS_ENABLED) }
private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) } private val fakeDns by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_FAKE_DNS_ENABLED) }
private val appendHttpProxy by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_APPEND_HTTP_PROXY) }
private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) } private val localDnsPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_LOCAL_DNS_PORT) }
private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) } private val vpnDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_VPN_DNS) }
@@ -57,9 +58,9 @@ class SettingsActivity : BaseActivity() {
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) } private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) } private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
private val httpPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HTTP_PORT) }
private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) } private val remoteDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_REMOTE_DNS) }
private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) } private val domesticDns by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DOMESTIC_DNS) }
private val dnsHosts by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DNS_HOSTS) }
private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) } private val delayTestUrl by lazy { findPreference<EditTextPreference>(AppConfig.PREF_DELAY_TEST_URL) }
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) } private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
@@ -141,11 +142,7 @@ class SettingsActivity : BaseActivity() {
socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval socksPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_SOCKS else nval
true true
} }
httpPort?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
httpPort?.summary = if (TextUtils.isEmpty(nval)) AppConfig.PORT_HTTP else nval
true
}
remoteDns?.setOnPreferenceChangeListener { _, any -> remoteDns?.setOnPreferenceChangeListener { _, any ->
val nval = any as String val nval = any as String
remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval remoteDns?.summary = if (nval == "") AppConfig.DNS_PROXY else nval
@@ -156,6 +153,11 @@ class SettingsActivity : BaseActivity() {
domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval domesticDns?.summary = if (nval == "") AppConfig.DNS_DIRECT else nval
true true
} }
dnsHosts?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
dnsHosts?.summary = nval
true
}
delayTestUrl?.setOnPreferenceChangeListener { _, any -> delayTestUrl?.setOnPreferenceChangeListener { _, any ->
val nval = any as String val nval = any as String
delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
@@ -175,6 +177,7 @@ class SettingsActivity : BaseActivity() {
updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN)) updateMode(MmkvManager.decodeSettingsString(AppConfig.PREF_MODE, VPN))
localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false) localDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false) fakeDns?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED, false)
appendHttpProxy?.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_APPEND_HTTP_PROXY, false)
localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS) localDnsPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT, AppConfig.PORT_LOCAL_DNS)
vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN) vpnDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
@@ -195,9 +198,9 @@ class SettingsActivity : BaseActivity() {
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS) socksPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT, AppConfig.PORT_SOCKS)
httpPort?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_HTTP_PORT, AppConfig.PORT_HTTP)
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY) remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT) domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl) delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
initSharedPreference() initSharedPreference()
@@ -213,7 +216,6 @@ class SettingsActivity : BaseActivity() {
fragmentInterval, fragmentInterval,
autoUpdateInterval, autoUpdateInterval,
socksPort, socksPort,
httpPort,
remoteDns, remoteDns,
domesticDns, domesticDns,
delayTestUrl delayTestUrl
@@ -264,6 +266,7 @@ class SettingsActivity : BaseActivity() {
perAppProxy?.isChecked = MmkvManager.decodeSettingsBool(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
appendHttpProxy?.isEnabled = vpn
localDnsPort?.isEnabled = vpn localDnsPort?.isEnabled = vpn
vpnDns?.isEnabled = vpn vpnDns?.isEnabled = vpn
if (vpn) { if (vpn) {
@@ -113,7 +113,7 @@ class SubEditActivity : BaseActivity() {
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -75,6 +75,7 @@ class UserAssetActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.add_file -> showFileChooser().let { true } R.id.add_file -> showFileChooser().let { true }
R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true } R.id.add_url -> startActivity(Intent(this, UserAssetUrlActivity::class.java)).let { true }
R.id.add_qrcode -> importAssetFromQRcode().let { true }
R.id.download_file -> downloadGeoFiles().let { true } R.id.download_file -> downloadGeoFiles().let { true }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -156,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)
@@ -298,7 +333,7 @@ class UserAssetActivity : BaseActivity() {
MmkvManager.removeAssetUrl(item.first) MmkvManager.removeAssetUrl(item.first)
initAssets() initAssets()
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting //do noting
} }
.show() .show()
@@ -14,6 +14,11 @@ 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()
} }
} }
@@ -106,7 +116,7 @@ class UserAssetUrlActivity : BaseActivity() {
MmkvManager.removeAssetUrl(editAssetId) MmkvManager.removeAssetUrl(editAssetId)
finish() finish()
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -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,11 +17,13 @@ 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()
@@ -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
}
}
} }

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