Compare commits

...

201 Commits

Author SHA1 Message Date
2dust
da347492d3 up 1.9.39 2025-03-07 14:25:30 +08:00
2dust
2ec5d8db3c Update AndroidLibXrayLite 2025-03-07 14:24:07 +08:00
2dust
fd9c5040bf up 1.9.38 2025-03-04 10:19:39 +08:00
2dust
aa328f0add Update AndroidLibXrayLite 2025-03-04 10:12:46 +08:00
hhhkkmk
9743d7b87b update (#4365) 2025-03-04 10:05:52 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
98fb0c433e fixup! Fix badvpn (#4302) (#4352)
thx
2025-02-25 09:31:11 +08:00
2dust
7c9fcd9f43 Update build.gradle.kts 2025-02-22 17:08:23 +08:00
2dust
54c76d9968 git submodule update --remote 2025-02-22 16:51:24 +08:00
2dust
40b3f0fedc up 1.9.37 2025-02-22 15:17:29 +08:00
2dust
dcfcf83430 Update AndroidLibXrayLite 2025-02-22 14:52:05 +08:00
2dust
e46b354643 up 1.9.36 2025-02-19 18:18:43 +08:00
2dust
f497e4e301 Update AndroidLibXrayLite 2025-02-19 18:13:34 +08:00
2dust
b65e4b3819 Bug fix
https://github.com/2dust/v2rayNG/issues/4329
2025-02-11 10:46:01 +08:00
Hossin Asaadi
d166b036fc Update ServerActivity.kt (#4326) 2025-02-11 10:31:22 +08:00
2dust
ddf5f22037 up 1.9.35 2025-02-09 10:41:09 +08:00
2dust
7d8a9f2b6d Update AndroidLibXrayLite 2025-02-09 10:33:41 +08:00
alphax-hue3682
0a1695e3d7 Update kotlin version to 2.1.10 (#4305)
* Update libs.versions.toml

* Update README.md
2025-02-07 14:31:45 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
4a653d4935 Fix badvpn (#4302)
* copying from df181a3065

* add missing includes of dc99ade18d

* update workflow

* fixup! update workflow
2025-01-31 13:58:59 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
2bc31a10c5 rm AndroidLibV2rayLite (#4303) 2025-01-31 13:54:08 +08:00
alphax-hue3682
e8d2c6214b Update dependencies (#4301)
* Update dependencies

* Update dependencies
2025-01-30 20:46:01 +08:00
alphax-hue3682
3a0f2687e9 Update_Submodules (#4292)
* UpdateSubmodules

* Update _Submodules
2025-01-30 20:20:26 +08:00
2dust
04c98326b2 up 1.9.34 2025-01-30 19:55:47 +08:00
2dust
eb22c7f303 up 1.9.33 2025-01-25 13:40:08 +08:00
alphax-hue3682
d51a4d7a7e Update libs.versions.toml (#4291)
* Update libs.versions.toml

* Update gradle-wrapper.properties

* Update libs.versions.toml

* Update libs.versions.toml
2025-01-25 13:37:47 +08:00
2dust
0fb705e1e2 Update libs.versions.toml 2025-01-25 10:28:23 +08:00
kore kas nadar
10b849ef09 Update Luri Bakhtiari translation (#4286) 2025-01-25 10:03:01 +08:00
solokot
d7d3b23cea Update Russian translation (#4281) 2025-01-25 10:02:51 +08:00
alphax-hue3682
c3786d434e remove patch (#4279)
remove patch
2025-01-19 18:36:46 +08:00
2dust
9e3b92014a logcat content reversed 2025-01-17 13:53:35 +08:00
alphax-hue3682
f4e088131b Update Persian translate (#4269) 2025-01-16 15:56:23 +08:00
2dust
e55e069fe3 Add bandwidth to hysteria2 settings
https://github.com/2dust/v2rayNG/issues/4261
2025-01-16 14:44:19 +08:00
alphax-hue3682
d8d3767798 Update Persian translate (#4264) 2025-01-14 09:53:57 +08:00
2dust
7e99b1ac78 up 1.9.32 2025-01-13 15:04:01 +08:00
2dust
6ff3a73bf2 Adjust UI for subscription 2025-01-13 14:56:48 +08:00
2dust
2a43b52344 Logcat add pull-down refresh 2025-01-13 14:31:36 +08:00
2dust
abff80ec23 Adjust UI 2025-01-13 12:51:26 +08:00
2dust
a4edf86195 Improved logcat 2025-01-13 12:50:35 +08:00
alphax-hue3682
0d0da6bfec Update Persian translate (#4256)
* Update Persian translate

* Update strings.xml
2025-01-12 13:47:18 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
e0c8ece9b5 Reproducible Builds for libhysteria2.so (#4249)
* Patch Go use 600296

* -buildvcs=false for libhysteria2

* fix if

* fixup! Build and cache libhysteria2.so (#4226)
2025-01-11 10:38:47 +08:00
2dust
4d875bc3d4 Add theme to SwitchCompat for tasker 2025-01-09 09:48:37 +08:00
2dust
3a6e23bcef Fix the bug of mux parameter taking 2025-01-08 11:23:21 +08:00
2dust
efd0716707 Custom configuration can use any outbound
https://github.com/2dust/v2rayNG/issues/4243
2025-01-07 17:14:23 +08:00
2dust
c94a5fb743 Update Luri Bakhtiari translation 2025-01-07 14:47:18 +08:00
2dust
047011f60b up 1.9.31 2025-01-07 14:21:08 +08:00
2dust
a54ed3a51a Update libs.versions.toml 2025-01-07 14:19:31 +08:00
2dust
c37f09bfcd Fix logcat 2025-01-07 14:03:26 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
1c7042463d Fixup! 7dbda3c (#4237) 2025-01-05 19:04:50 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
dcb003f9ab Submodules (#4234)
* add submodule AndroidLibXrayLite

* rm -r AndroidLibV2rayLite

* add submodule AndroidLibV2rayLite

* update cache key for libtun2socks due to submodules

* fetch-depth: '0'

* fail safe

* Revert "add submodule AndroidLibV2rayLite"

This reverts commit 816f75e0f9.

* sync with 2dust/AndroidLibXrayLite#90

* checkout to 664c389 of AndroidLibXrayLite

* refine cache key of libtun2socks
2025-01-05 11:52:31 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
7dbda3cee7 Build and cache libhysteria2.so (#4226)
* add submodule apernet/hysteria

* remove libhysteria2 binary from git repo

* libhysteria2.sh

* ignore *.so
2025-01-04 10:49:04 +08:00
solokot
26bee229a1 Update Russian translation (#4221) 2025-01-03 09:47:35 +08:00
alphax-hue3682
5bf2beb179 Update Persian translate (#4219)
* Update Persian translate

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml
2025-01-03 09:47:21 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
4a5c551678 ndk for gradlew (#4220) 2025-01-02 14:57:44 +08:00
2dust
277894215d up 1.9.30 2025-01-02 10:11:57 +08:00
alphax-hue3682
684e08a3a1 Update Persian translate (#4214)
* Update Persian translate

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml
2025-01-02 09:37:24 +08:00
2dust
19dbc2f9b9 up 1.9.29 2025-01-01 16:45:08 +08:00
2dust
833a1e06f0 Add VPN bypass LAN option
https://github.com/2dust/v2rayNG/pull/4208
2025-01-01 16:43:48 +08:00
2dust
daca0831a4 Remove the last rule from the Whitelist 2024-12-31 21:19:15 +08:00
2dust
337889c5f1 up 1.9.28 2024-12-29 14:29:22 +08:00
2dust
244d2d3866 Fix bugs related to routing rules
https://github.com/2dust/v2rayNG/issues/4196
https://github.com/2dust/v2rayNG/issues/4199
2024-12-29 11:06:08 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
c0fed0ba4f Download libv2ray from 2dust/AndroidLibXrayLite (#4200) 2024-12-28 19:46:51 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
affb107b9d Sagernet (#4194)
* switch to sagernet/gomobile

* arguments for rb

* match ndk version

* with more options

* nttld/setup-ndk#518
2024-12-28 19:29:00 +08:00
2dust
f96073af99 Update build.yml 2024-12-25 10:23:30 +08:00
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
2dust
884b444a41 up 1.9.12 2024-11-05 19:01:03 +08:00
2dust
e1ff2df36e Bug fix 2024-11-05 18:59:05 +08:00
2dust
61c0111778 Revert "Refactor listenForPackageChanges to remove redundant registerReceiver calls (#3872)"
This reverts commit 5f167512f5.
2024-11-05 18:07:55 +08:00
Tamim Hossain
bfc9a64e07 Correct isEmpty syntax for collection check (#3881)
Fixed the syntax for checking if `filesToCompress` is empty by using `isEmpty()` instead of `isEmpty`. This ensures correct functionality when verifying if the collection has elements.
2024-11-05 18:01:32 +08:00
Tamim Hossain
2ba92045cc Remove unnecessary Boolean comparisons in conditional checks (#3880)
Simplified conditional checks by removing unnecessary `== true` comparisons. The `decodeSettingsBool` function returns a non-nullable Boolean with a default value, so direct usage improves readability and keeps the code concise.
2024-11-05 18:00:52 +08:00
Tamim Hossain
aeca9f51c8 Remove redundant Boolean comparison in runLoop call (#3879)
Simplified the call to `runLoop` by removing the redundant `== true` comparison. Since `decodeSettingsBool` returns a non-nullable Boolean, direct usage improves readability.
2024-11-05 18:00:23 +08:00
Tamim Hossain
b107c0ac1d Remove redundant TAG field in ProcessService (#3878)
Refactored `ProcessService` by removing the redundant `TAG` variable and using `ANG_PACKAGE` directly in logging calls, simplifying the code and reducing unnecessary field assignments.
2024-11-05 17:59:37 +08:00
Tamim Hossain
65a04b4784 Refactor getString call to use orEmpty for null safety (#3877)
Updated `getString` call to use `orEmpty()` instead of specifying a default empty string, making the code cleaner and handling nullability more effectively.
2024-11-05 17:58:23 +08:00
Tamim Hossain
eab9f50cfd Refactor BootReceiver for improved null handling and readability (#3876)
Refactored `BootReceiver` to simplify null checks and conditional structure. Combined context and intent checks into a single early return and refactored logic for `decodeStartOnBoot` and `getSelectServer` to improve readability.
2024-11-05 17:57:51 +08:00
Tamim Hossain
b8bb83b524 Used safecall ? (#3874)
Used safecall `?`
2024-11-05 17:56:32 +08:00
Tamim Hossain
93eb9fe3b9 Introduce NetworkType enum to improve network type handling (#3873)
* Introduce NetworkType enum to improve network type handling

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

* Refactor to use NetworkType enum in VmessFmt

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

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

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

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

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

* Update dependabot.yml

* Update libs.versions.toml

* Delete .github/dependabot.yml
2024-11-05 15:03:07 +08:00
2dust
153b4cffef Unable to obtain the notification permission 2024-11-05 14:29:44 +08:00
2dust
f09a413232 Bug fix
https://github.com/2dust/v2rayNG/issues/3858
2024-11-05 10:24:06 +08:00
886963226
cba58f6ae2 Update V2rayConfigManager.kt (#3860) 2024-11-05 09:31:01 +08:00
2dust
2bf4b91488 Bug fix
https://github.com/2dust/v2rayNG/issues/3852
2024-11-04 20:22:40 +08:00
178 changed files with 5005 additions and 2036 deletions

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:
@@ -16,6 +17,102 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: 'recursive'
fetch-depth: '0'
- name: Restore cached libtun2socks
id: cache-libtun2socks-restore
uses: actions/cache/restore@v4
with:
path: ${{ github.workspace }}/libs
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
- name: Setup Android NDK
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
add-to-path: true
link-to-sdk: true
local-cache: true
- name: Restore Android Symlinks
run: |
directory="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin"
find "$directory" -type l | while read link; do
current_target=$(readlink "$link")
new_target="$directory/$(basename "$current_target")"
ln -sf "$new_target" "$link"
echo "Changed $(basename "$link") from $current_target to $new_target"
done
- name: Build libtun2socks
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
run: |
bash compile-tun2socks.sh
tar -xvzf libtun2socks.so.tgz
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 }}/libs
key: libtun2socks-${{ runner.os }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
- name: Copy libtun2socks
run: |
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
- name: Fetch AndroidLibXrayLite tag
run: |
pushd AndroidLibXrayLite
CURRENT_TAG=$(git describe --tags --abbrev=0)
echo "Current tag in this repo: $CURRENT_TAG"
echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV
popd
- name: Download libv2ray
uses: robinraju/release-downloader@v1
with:
repository: '2dust/AndroidLibXrayLite'
tag: ${{ env.CURRENT_TAG }}
fileName: 'libv2ray.aar'
out-file-path: V2rayNG/app/libs/
- name: Restore cached libhysteria2
id: cache-libhysteria2-restore
uses: actions/cache/restore@v4
with:
path: ${{ github.workspace }}/hysteria/libs
key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
- name: Setup Golang
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
uses: actions/setup-go@v5
with:
go-version-file: 'AndroidLibXrayLite/go.mod'
- name: Build libhysteria2
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
run: |
bash libhysteria2.sh
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Save libhysteria2
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ${{ github.workspace }}/hysteria/libs
key: libhysteria2-${{ runner.os }}-${{ hashFiles('.git/modules/hysteria/HEAD') }}-${{ hashFiles('libhysteria2.sh') }}
- name: Copy libhysteria2
run: |
cp -r ${{ github.workspace }}/hysteria/libs ${{ github.workspace }}/V2rayNG/app
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@@ -23,71 +120,51 @@ jobs:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '21'
- name: Setup Golang
uses: actions/setup-go@v5
with:
go-version: '1.23.2'
cache: false
- name: Patch Go use 600296
#https://go-review.googlesource.com/c/go/+/600296
run: |
cd "$(go env GOROOT)"
curl "https://go-review.googlesource.com/changes/go~600296/revisions/5/patch" | base64 -d | patch --verbose -p 1
- name: Install gomobile
run: |
go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240806205939-81131f6468ab
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Setup Android environment - name: Setup Android environment
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
- name: Build dependencies - name: Decode Keystore
run: | uses: timheuer/base64-to-file@v1
mkdir ${{ github.workspace }}/build id: android_keystore
cd ${{ github.workspace }}/build with:
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git fileName: "android_keystore.jks"
cd AndroidLibXrayLite encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }}
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
gomobile init
go mod tidy -v
gomobile bind -v -androidapi 21 -ldflags='-s -w' ./
cp *.aar ${{ github.workspace }}/V2rayNG/app/libs/
- 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 }}
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- 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 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 prerelease: true
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*armeabi-v7a*.apk
!${{ github.workspace }}/V2rayNG/app/build/outputs/apk/debug/*x86*.apk

16
.github/workflows/fastlane.yml vendored Normal file
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

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
V2rayNG/app/release/output.json V2rayNG/app/release/output.json
.idea/ .idea/
.gradle/ .gradle/
*.so

12
.gitmodules vendored Normal file
View File

@@ -0,0 +1,12 @@
[submodule "hysteria"]
path = hysteria
url = https://github.com/apernet/hysteria
[submodule "AndroidLibXrayLite"]
path = AndroidLibXrayLite
url = https://github.com/2dust/AndroidLibXrayLite
[submodule "badvpn"]
path = badvpn
url = https://github.com/XTLS/badvpn
[submodule "libancillary"]
path = libancillary
url = https://github.com/shadowsocks/libancillary

View File

@@ -1,20 +0,0 @@
# AndroidLibV2rayLite
### Preparation
- latest Ubuntu environment
- At lease 30G free space
- Get Repo [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
### Prepare Go
- Go to https://golang.org/doc/install and install latest go
- Make sure `go version` works as expected
### Prepare gomobile
- Go to https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile and install gomobile
- export PATH=$PATH:~/go/bin
- Make sure `gomobile init` works as expected
### Prepare NDK
- Go to https://developer.android.com/ndk/downloads and install latest NDK
- export PATH=$PATH:<wherever you ndk is located>
- Make sure `ndk-build -v` works as expected
### Make
- sudo apt install make
- Read and understand [build script](https://github.com/2dust/AndroidLibV2rayLite/blob/master/Makefile)

1
AndroidLibXrayLite Submodule

Submodule AndroidLibXrayLite added at 08c4a038f0

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.10-blue.svg)](https://kotlinlang.org)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master)
[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng) [![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng)
[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases) [![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases)

View File

@@ -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 = 605 versionCode = 636
versionName = "1.9.11" versionName = "1.9.39"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
splits { splits {
abi { abi {
isEnable = true isEnable = true
reset()
if (abiFilterList != null && abiFilterList.isNotEmpty()) {
include(*abiFilterList.toTypedArray())
} else {
include( include(
"arm64-v8a", "arm64-v8a",
"armeabi-v7a", "armeabi-v7a",
"x86_64", "x86_64",
"x86" "x86"
) )
isUniversalApk = true }
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,12 +67,37 @@ 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 isFdroid = variant.productFlavors.any { it.name == "fdroid" }
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
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
val abi = output.getFilter("ABI") ?: "universal"
output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
if (versionCodes.containsKey(abi)) {
output.versionCodeOverride =
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
} else {
return@forEach
}
}
} else {
val versionCodes = val versionCodes =
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4) mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
@@ -69,12 +111,14 @@ android {
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk" output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if (versionCodes.containsKey(abi)) { if (versionCodes.containsKey(abi)) {
output.versionCodeOverride = (1000000 * versionCodes[abi]!!).plus(variant.versionCode) output.versionCodeOverride =
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} else { } else {
return@forEach return@forEach
} }
} }
} }
}
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
@@ -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.androidx.swiperefreshlayout)
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.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
// 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.

View File

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

View File

@@ -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",

View File

@@ -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,18 +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"
] ]
},
{
"remarks": "最终代理",
"port": "0-65535",
"outboundTag": "proxy"
} }
] ]

View File

@@ -40,10 +40,5 @@
"ip": [ "ip": [
"geoip:ir" "geoip:ir"
] ]
},
{
"remarks": "Final Agent",
"port": "0-65535",
"outboundTag": "proxy"
} }
] ]

File diff suppressed because it is too large Load Diff

View File

@@ -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
)!!
} }

View File

@@ -22,8 +22,10 @@ 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_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy" const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
const val PREF_ROUTING_RULESET = "pref_routing_ruleset" const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
const val PREF_MUX_ENABLED = "pref_mux_enabled" const val PREF_MUX_ENABLED = "pref_mux_enabled"
@@ -47,9 +49,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 +113,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 +162,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"

View File

@@ -9,6 +9,7 @@ data class Hysteria2Bean(
val http: Socks5Bean? = null, val http: Socks5Bean? = null,
val tls: TlsBean? = null, val tls: TlsBean? = null,
val transport: TransportBean? = null, val transport: TransportBean? = null,
val bandwidth: BandwidthBean? = null,
) { ) {
data class ObfsBean( data class ObfsBean(
val type: String?, val type: String?,
@@ -37,4 +38,9 @@ data class Hysteria2Bean(
val hopInterval: String?, val hopInterval: String?,
) )
} }
data class BandwidthBean(
val down: String?,
val up: String?,
)
} }

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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,
) )

View File

@@ -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
@@ -461,25 +476,25 @@ data class V2rayConfig(
data class MuxBean( data class MuxBean(
var enabled: Boolean, var enabled: Boolean,
var concurrency: Int = 8, var concurrency: Int? = null,
var xudpConcurrency: Int = 8, var xudpConcurrency: Int? = null,
var xudpProxyUDP443: String = "", var xudpProxyUDP443: String? = null,
) )
fun getServerAddress(): String? { fun getServerAddress(): String? {
if (protocol.equals(EConfigType.VMESS.name, true) if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true) || protocol.equals(EConfigType.VLESS.name, true)
) { ) {
return settings?.vnext?.get(0)?.address return settings?.vnext?.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",

View File

@@ -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())

View File

@@ -1,6 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
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.isNotNullEmpty
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -29,9 +30,41 @@ 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()
config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() } config.sni.let { if (it.isNotNullEmpty()) dicQuery["sni"] = it.orEmpty() }
config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() } config.alpn.let { if (it.isNotNullEmpty()) dicQuery["alpn"] = it.orEmpty() }
@@ -41,37 +74,45 @@ open class FmtBase {
config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() } config.spiderX.let { if (it.isNotNullEmpty()) dicQuery["spx"] = it.orEmpty() }
config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() } config.flow.let { if (it.isNotNullEmpty()) dicQuery["flow"] = it.orEmpty() }
dicQuery["type"] = config.network?.ifEmpty { AppConfig.DEFAULT_NETWORK }.orEmpty() val networkType = NetworkType.fromString(config.network)
dicQuery["type"] = networkType.type
when (config.network) { when (networkType) {
"tcp" -> { NetworkType.TCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty() dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() } config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
} }
"kcp" -> { NetworkType.KCP -> {
dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty() dicQuery["headerType"] = config.headerType?.ifEmpty { "none" }.orEmpty()
config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() } config.seed.let { if (it.isNotNullEmpty()) dicQuery["seed"] = it.orEmpty() }
} }
"ws", "httpupgrade", "splithttp" -> { 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() }
} }
"http", "h2" -> { NetworkType.XHTTP -> {
config.host.let { if (it.isNotNullEmpty()) dicQuery["host"] = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
config.xhttpExtra.let { if (it.isNotNullEmpty()) dicQuery["extra"] = it.orEmpty() }
}
NetworkType.HTTP, NetworkType.H2 -> {
dicQuery["type"] = "http" 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() }
} }
"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() }
} // }
"grpc" -> { NetworkType.GRPC -> {
config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() } config.mode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() } config.authority.let { if (it.isNotNullEmpty()) dicQuery["authority"] = it.orEmpty() }
config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() } config.serviceName.let { if (it.isNotNullEmpty()) dicQuery["serviceName"] = it.orEmpty() }

View File

@@ -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()) {

View File

@@ -85,6 +85,12 @@ object Hysteria2Fmt : FmtBase() {
) )
) )
val bandwidth = if (config.bandwidthDown.isNullOrEmpty() || config.bandwidthUp.isNullOrEmpty()) null else
Hysteria2Bean.BandwidthBean(
down = config.bandwidthDown,
up = config.bandwidthUp,
)
val server = val server =
if (config.portHopping.isNullOrEmpty()) if (config.portHopping.isNullOrEmpty())
config.getServerAddressAndPort() config.getServerAddressAndPort()
@@ -96,6 +102,7 @@ object Hysteria2Fmt : FmtBase() {
auth = config.password, auth = config.password,
obfs = obfs, obfs = obfs,
transport = transport, transport = transport,
bandwidth = bandwidth,
socks5 = Hysteria2Bean.Socks5Bean( socks5 = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}", listen = "$LOOPBACK:${socksPort}",
), ),

View File

@@ -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
}
}

View File

@@ -15,13 +15,14 @@ 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
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
if (uri.userInfo?.isEmpty() == false) { if (uri.userInfo?.isEmpty() == false) {
val result = Utils.decode(uri.userInfo).split(":") val result = Utils.decode(uri.userInfo).split(":", limit = 2)
if (result.count() == 2) { if (result.count() == 2) {
config.username = result.first() config.username = result.first()
config.password = result.last() config.password = result.last()
@@ -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()) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -4,6 +4,7 @@ import android.text.TextUtils
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.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.dto.VmessQRCode import com.v2ray.ang.dto.VmessQRCode
@@ -47,27 +48,29 @@ 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
when (config.network) { when (NetworkType.fromString(config.network)) {
"kcp" -> { NetworkType.KCP -> {
config.seed = vmessQRCode.path config.seed = vmessQRCode.path
} }
"quic" -> { // NetworkType.QUIC -> {
config.quicSecurity = vmessQRCode.host // config.quicSecurity = vmessQRCode.host
config.quicKey = vmessQRCode.path // config.quicKey = vmessQRCode.path
} // }
"grpc" -> { NetworkType.GRPC -> {
config.mode = vmessQRCode.type config.mode = vmessQRCode.type
config.serviceName = vmessQRCode.path config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host config.authority = vmessQRCode.host
} }
else -> {}
} }
config.security = vmessQRCode.tls config.security = vmessQRCode.tls
config.insecure = allowInsecure config.insecure = allowInsecure
config.sni = vmessQRCode.sni config.sni = vmessQRCode.sni
@@ -90,22 +93,24 @@ object VmessFmt : FmtBase() {
vmessQRCode.net = config.network.orEmpty() vmessQRCode.net = config.network.orEmpty()
vmessQRCode.type = config.headerType.orEmpty() vmessQRCode.type = config.headerType.orEmpty()
when (config.network) { when (NetworkType.fromString(config.network)) {
"kcp" -> { NetworkType.KCP -> {
vmessQRCode.path = config.seed.orEmpty() vmessQRCode.path = config.seed.orEmpty()
} }
"quic" -> { // NetworkType.QUIC -> {
vmessQRCode.host = config.quicSecurity.orEmpty() // vmessQRCode.host = config.quicSecurity.orEmpty()
vmessQRCode.path = config.quicKey.orEmpty() // vmessQRCode.path = config.quicKey.orEmpty()
} // }
"grpc" -> { NetworkType.GRPC -> {
vmessQRCode.type = config.mode.orEmpty() vmessQRCode.type = config.mode.orEmpty()
vmessQRCode.path = config.serviceName.orEmpty() vmessQRCode.path = config.serviceName.orEmpty()
vmessQRCode.host = config.authority.orEmpty() vmessQRCode.host = config.authority.orEmpty()
} }
else -> {}
} }
config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() } config.host.let { if (it.isNotNullEmpty()) vmessQRCode.host = it.orEmpty() }
config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() } config.path.let { if (it.isNotNullEmpty()) vmessQRCode.path = it.orEmpty() }
@@ -119,7 +124,7 @@ object VmessFmt : FmtBase() {
} }
fun parseVmessStd(str: String): ProfileItem? { fun parseVmessStd(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS) val config = ProfileItem.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str)) val uri = URI(Utils.fixIllegalUrl(str))
@@ -132,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 = 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"].orEmpty()) == "1") true else allowInsecure
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
config.alpn = queryParam["alpn"]
return config return config
} }
@@ -157,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,
@@ -180,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,

View File

@@ -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
}
} }

View File

@@ -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
@@ -277,10 +284,10 @@ object AngConfigManager {
if (serverList.isNotEmpty()) { if (serverList.isNotEmpty()) {
var count = 0 var count = 0
for (srv in serverList.reversed()) { for (srv in serverList.reversed()) {
val config = CustomFmt.parse(server) ?: 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

View File

@@ -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
@@ -19,7 +20,7 @@ object MigrateManager {
if (serverStorage.count().toInt() == 0) { if (serverStorage.count().toInt() == 0) {
return false return false
} }
var serverList = serverStorage.allKeys() ?: return false val serverList = serverStorage.allKeys() ?: return false
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count()) Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count())
for (guid in serverList) { for (guid in serverList) {
@@ -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,10 +71,10 @@ 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[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 ?: NetworkType.TCP.type
outbound.getTransportSettingDetails()?.let { transportDetails -> outbound.getTransportSettingDetails()?.let { transportDetails ->
config.network = "tcp"
config.headerType = transportDetails[0].orEmpty() config.headerType = transportDetails[0].orEmpty()
config.host = transportDetails[1].orEmpty() config.host = transportDetails[1].orEmpty()
config.path = transportDetails[2].orEmpty() config.path = transportDetails[2].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

View File

@@ -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,17 +342,13 @@ object MmkvManager {
} }
fun decodeSettingsBool(key: String): Boolean { fun decodeSettingsBool(key: String): Boolean {
return settingsStorage.decodeBool(key) return settingsStorage.decodeBool(key, false)
} }
fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean { fun decodeSettingsBool(key: String, defaultValue: Boolean): Boolean {
return settingsStorage.decodeBool(key, defaultValue) return settingsStorage.decodeBool(key, defaultValue)
} }
fun decodeSettingsInt(key: String, defaultValue: Int): Int {
return settingsStorage.decodeInt(key, defaultValue)
}
fun decodeSettingsStringSet(key: String): MutableSet<String>? { fun decodeSettingsStringSet(key: String): MutableSet<String>? {
return settingsStorage.decodeStringSet(key) return settingsStorage.decodeStringSet(key)
} }

View File

@@ -9,9 +9,11 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.GEOIP_PRIVATE import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RoutingType import com.v2ray.ang.dto.RoutingType
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerList import com.v2ray.ang.handler.MmkvManager.decodeServerList
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
@@ -43,12 +45,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 +72,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)
} }
} }
@@ -91,11 +93,13 @@ object SettingsManager {
fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) { fun saveRoutingRuleset(index: Int, ruleset: RulesetItem?) {
if (ruleset == null) return if (ruleset == null) return
val rulesetList = MmkvManager.decodeRoutingRulesets() var rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return if (rulesetList.isNullOrEmpty()) {
rulesetList = mutableListOf()
}
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
} }
@@ -113,6 +117,25 @@ object SettingsManager {
} }
fun routingRulesetsBypassLan(): Boolean { fun routingRulesetsBypassLan(): Boolean {
val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "0"
if (vpnBypassLan == "1") {
return true
} else if (vpnBypassLan == "2") {
return false
}
//Follow config
val guid = MmkvManager.getSelectServer() ?: return false
val config = MmkvManager.decodeServerConfig(guid) ?: return false
if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) ?: return false
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }?.any {
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
}
return exist == true
}
val rulesetItems = MmkvManager.decodeRoutingRulesets() val rulesetItems = MmkvManager.decodeRoutingRulesets()
val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any { val exist = rulesetItems?.filter { it.enabled && it.outboundTag == TAG_DIRECT }?.any {
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
@@ -155,7 +178,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) {

View File

@@ -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")
} }
if (Utils.isXray()) {
v2rayConfig.inbounds.removeAt(1)
} else {
val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds[1].port = httpPort 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,7 +334,7 @@ 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(),
@@ -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对象
@@ -418,19 +438,18 @@ object V2rayConfigManager {
|| protocol.equals(EConfigType.HYSTERIA2.name, true) || protocol.equals(EConfigType.HYSTERIA2.name, true)
) { ) {
muxEnabled = false muxEnabled = false
} else if (protocol.equals(EConfigType.VLESS.name, true) } else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
&& outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.isNotEmpty() == true
) {
muxEnabled = false muxEnabled = false
} }
if (muxEnabled == true) { if (muxEnabled == true) {
outbound.mux?.enabled = true outbound.mux?.enabled = true
outbound.mux?.concurrency = outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt()
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_CONCURRENCY, 8) outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt()
outbound.mux?.xudpConcurrency = outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC,"reject")
MmkvManager.decodeSettingsInt(AppConfig.PREF_MUX_XUDP_CONCURRENCY, 16) if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true) {
outbound.mux?.xudpProxyUDP443 = outbound.mux?.concurrency = -1
MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC) ?: "reject" }
} else { } else {
outbound.mux?.enabled = false outbound.mux?.enabled = false
outbound.mux?.concurrency = -1 outbound.mux?.concurrency = -1
@@ -455,7 +474,7 @@ object V2rayConfigManager {
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
val requestString: String by lazy { val requestString: String by lazy {
"""{"version":"1.1","method":"GET","headers":{"User-Agent":"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"Safari/537.36,"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}""" """{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
} }
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson( outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString, requestString,

View File

@@ -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)
} }

View File

@@ -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()
} }

View File

@@ -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; // 增加滑动逃逸速度,使得更难触发滑动
}
}

View File

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

View File

@@ -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)

View File

@@ -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
)!!
} }

View File

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

View File

@@ -16,9 +16,9 @@ class TaskerReceiver : BroadcastReceiver() {
try { try {
val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE) val bundle = intent?.getBundleExtra(AppConfig.TASKER_EXTRA_BUNDLE)
val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false) val switch = bundle?.getBoolean(AppConfig.TASKER_EXTRA_BUNDLE_SWITCH, false)
val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID, "") val guid = bundle?.getString(AppConfig.TASKER_EXTRA_BUNDLE_GUID).orEmpty()
if (switch == null || guid == null || TextUtils.isEmpty(guid)) { if (switch == null || TextUtils.isEmpty(guid)) {
return return
} else if (switch) { } else if (switch) {
if (guid == AppConfig.TASKER_DEFAULT_GUID) { if (guid == AppConfig.TASKER_DEFAULT_GUID) {

View File

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

View File

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

View File

@@ -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
@@ -13,11 +14,13 @@ import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toSpeedString import com.v2ray.ang.extension.toSpeedString
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
@@ -28,10 +31,11 @@ import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.PluginUtil import com.v2ray.ang.util.PluginUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import go.Seq import go.Seq
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import libv2ray.Libv2ray import libv2ray.Libv2ray
import libv2ray.V2RayPoint import libv2ray.V2RayPoint
@@ -43,6 +47,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)
@@ -58,14 +63,17 @@ object V2RayServiceManager {
private var lastQueryTime = 0L private var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null private var mBuilder: NotificationCompat.Builder? = null
private var mDisposable: Disposable? = null private var speedNotificationJob: Job? = null
private var mNotificationManager: NotificationManager? = null private var mNotificationManager: NotificationManager? = null
fun startV2Ray(context: Context) { fun startV2Ray(context: Context) {
if (v2rayPoint.isRunning) return if (v2rayPoint.isRunning) return
val guid = MmkvManager.getSelectServer() ?: return val guid = MmkvManager.getSelectServer() ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return val config = MmkvManager.decodeServerConfig(guid) ?: return
if (!Utils.isValidUrl(config.server) && !Utils.isIpAddress(config.server)) return if (config.configType != EConfigType.CUSTOM
&& !Utils.isValidUrl(config.server)
&& !Utils.isIpAddress(config.server)
) return
// val result = V2rayConfigUtil.getV2rayConfig(context, guid) // val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) return // if (!result.status) return
@@ -126,6 +134,11 @@ object V2RayServiceManager {
} }
} }
/**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
*/
fun startV2rayPoint() { fun startV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
val guid = MmkvManager.getSelectServer() ?: return val guid = MmkvManager.getSelectServer() ?: return
@@ -142,11 +155,7 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_SCREEN_ON) mFilter.addAction(Intent.ACTION_SCREEN_ON)
mFilter.addAction(Intent.ACTION_SCREEN_OFF) mFilter.addAction(Intent.ACTION_SCREEN_OFF)
mFilter.addAction(Intent.ACTION_USER_PRESENT) mFilter.addAction(Intent.ACTION_USER_PRESENT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
service.registerReceiver(mMsgReceive, mFilter, Context.RECEIVER_EXPORTED)
} else {
service.registerReceiver(mMsgReceive, mFilter)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
@@ -156,7 +165,7 @@ object V2RayServiceManager {
currentConfig = config currentConfig = config
try { try {
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.d(ANG_PACKAGE, e.toString())
} }
@@ -217,11 +226,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 -> {
@@ -276,30 +289,24 @@ object V2RayServiceManager {
private fun showNotification() { private fun showNotification() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
val startMainIntent = Intent(service, MainActivity::class.java) val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val contentPendingIntent = PendingIntent.getActivity(
service,
NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else { } else {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
} }
)
val startMainIntent = Intent(service, MainActivity::class.java)
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
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) {
@@ -323,6 +330,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) //取消震动,铃声其他都不好使
@@ -347,10 +359,15 @@ object V2RayServiceManager {
fun cancelNotification() { fun cancelNotification() {
val service = serviceControl?.get()?.getService() ?: return val service = serviceControl?.get()?.getService() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
} else {
service.stopForeground(true) service.stopForeground(true)
}
mBuilder = null mBuilder = null
mDisposable?.dispose() speedNotificationJob?.cancel()
mDisposable = null speedNotificationJob = null
} }
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) { private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
@@ -377,7 +394,7 @@ object V2RayServiceManager {
} }
private fun startSpeedNotification() { private fun startSpeedNotification() {
if (mDisposable == null && if (speedNotificationJob == null &&
v2rayPoint.isRunning && v2rayPoint.isRunning &&
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
) { ) {
@@ -385,8 +402,8 @@ object V2RayServiceManager {
val outboundTags = currentConfig?.getAllOutboundTags() val outboundTags = currentConfig?.getAllOutboundTags()
outboundTags?.remove(TAG_DIRECT) outboundTags?.remove(TAG_DIRECT)
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS) speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
.subscribe { while (isActive) {
val queryTime = System.currentTimeMillis() val queryTime = System.currentTimeMillis()
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0 val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
var proxyTotal = 0L var proxyTotal = 0L
@@ -414,6 +431,8 @@ object V2RayServiceManager {
} }
lastZeroSpeed = zeroSpeed lastZeroSpeed = zeroSpeed
lastQueryTime = queryTime lastQueryTime = queryTime
delay(3000)
}
} }
} }
} }
@@ -429,11 +448,10 @@ object V2RayServiceManager {
} }
private fun stopSpeedNotification() { private fun stopSpeedNotification() {
mDisposable?.let { speedNotificationJob?.let {
it.dispose() //stop queryStats it.cancel()
mDisposable = null speedNotificationJob = null
updateNotification(currentConfig?.remarks, 0, 0) updateNotification(currentConfig?.remarks, 0, 0)
} }
} }
} }

View File

@@ -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()

View File

@@ -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
@@ -17,6 +18,7 @@ import android.os.StrictMode
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -33,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"
} }
@@ -153,9 +155,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty()) builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty())
val selfPackageName = BuildConfig.APPLICATION_ID val selfPackageName = BuildConfig.APPLICATION_ID
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET) val apps = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS) == true val bypassApps = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS)
//process self package //process self package
if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName) if (bypassApps) apps?.add(selfPackageName) else apps?.remove(selfPackageName)
apps?.forEach { apps?.forEach {
@@ -165,6 +167,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
else else
builder.addAllowedApplication(it) builder.addAllowedApplication(it)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
Log.d(ANG_PACKAGE, "setup error : --${e.localizedMessage}")
} }
} }
} else { } else {
@@ -188,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.
@@ -215,11 +221,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
"--loglevel", "notice" "--loglevel", "notice"
) )
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
cmd.add("--netif-ip6addr") cmd.add("--netif-ip6addr")
cmd.add(PRIVATE_VLAN6_ROUTER) cmd.add(PRIVATE_VLAN6_ROUTER)
} }
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt()) val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
cmd.add("--dnsgw") cmd.add("--dnsgw")
cmd.add("$LOOPBACK:${localDnsPort}") cmd.add("$LOOPBACK:${localDnsPort}")
@@ -232,7 +238,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
process = proBuilder process = proBuilder
.directory(applicationContext.filesDir) .directory(applicationContext.filesDir)
.start() .start()
Thread(Runnable { Thread {
Log.d(packageName, "$TUN2SOCKS check") Log.d(packageName, "$TUN2SOCKS check")
process.waitFor() process.waitFor()
Log.d(packageName, "$TUN2SOCKS exited") Log.d(packageName, "$TUN2SOCKS exited")
@@ -240,7 +246,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
Log.d(packageName, "$TUN2SOCKS restart") Log.d(packageName, "$TUN2SOCKS restart")
runTun2socks() runTun2socks()
} }
}).start() }.start()
Log.d(packageName, process.toString()) Log.d(packageName, process.toString())
sendFd() sendFd()

View File

@@ -5,15 +5,16 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.tbruyelle.rxpermissions3.RxPermissions
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.SpeedtestUtil import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil import com.v2ray.ang.util.ZipUtil
@@ -22,9 +23,23 @@ 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)) }
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
}
} else {
toast(R.string.toast_permission_denied)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
@@ -32,6 +47,7 @@ class AboutActivity : BaseActivity() {
title = getString(R.string.title_about) title = getString(R.string.title_about)
binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir) binding.tvBackupSummary.text = this.getString(R.string.summary_configuration_backup, extDir)
binding.layoutBackup.setOnClickListener { binding.layoutBackup.setOnClickListener {
val ret = backupConfiguration(extDir.absolutePath) val ret = backupConfiguration(extDir.absolutePath)
if (ret.first) { if (ret.first) {
@@ -49,7 +65,8 @@ class AboutActivity : BaseActivity() {
Intent(Intent.ACTION_SEND).setType("application/zip") Intent(Intent.ACTION_SEND).setType("application/zip")
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra( .putExtra(
Intent.EXTRA_STREAM, FileProvider.getUriForFile( Intent.EXTRA_STREAM,
FileProvider.getUriForFile(
this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second) this, BuildConfig.APPLICATION_ID + ".cache", File(ret.second)
) )
), getString(R.string.title_configuration_share) ), getString(R.string.title_configuration_share)
@@ -61,22 +78,21 @@ class AboutActivity : BaseActivity() {
} }
binding.layoutRestore.setOnClickListener { binding.layoutRestore.setOnClickListener {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val permission =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES Manifest.permission.READ_MEDIA_IMAGES
} else { } else {
Manifest.permission.READ_EXTERNAL_STORAGE Manifest.permission.READ_EXTERNAL_STORAGE
} }
RxPermissions(this)
.request(permission) if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
.subscribe {
if (it) {
try { try {
showFileChooser() showFileChooser()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} else } else {
toast(R.string.toast_permission_denied) requestPermissionLauncher.launch(permission)
} }
} }
@@ -88,6 +104,16 @@ class AboutActivity : BaseActivity() {
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") { dialog, _ -> dialog.dismiss() }
.show()
}
binding.layoutTgChannel.setOnClickListener { binding.layoutTgChannel.setOnClickListener {
Utils.openUri(this, AppConfig.TgChannelUrl) Utils.openUri(this, AppConfig.TgChannelUrl)
} }
@@ -148,9 +174,9 @@ class AboutActivity : BaseActivity() {
} }
private val chooseFile = private val chooseFile =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = it.data?.data val uri = result.data?.data
if (it.resultCode == RESULT_OK && uri != null) { if (result.resultCode == RESULT_OK && uri != null) {
try { try {
val targetFile = val targetFile =
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip") File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
@@ -171,4 +197,7 @@ class AboutActivity : BaseActivity() {
} }
} }
private fun toast(messageResId: Int) {
Toast.makeText(this, getString(messageResId), Toast.LENGTH_SHORT).show()
}
} }

View File

@@ -0,0 +1,149 @@
package com.v2ray.ang.ui
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
var logsetsAll: MutableList<String> = mutableListOf()
var logsets: MutableList<String> = mutableListOf()
private val adapter by lazy { LogcatRecyclerAdapter(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
title = getString(R.string.title_logcat)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
binding.refreshLayout.setOnRefreshListener(this)
logsets.add(getString(R.string.pull_down_to_refresh))
}
private fun getLogcat() {
try {
binding.refreshLayout.isRefreshing = true
lifecycleScope.launch(Dispatchers.Default) {
val lst = LinkedHashSet<String>()
lst.add("logcat")
lst.add("-d")
lst.add("-v")
lst.add("time")
lst.add("-s")
lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
val process = withContext(Dispatchers.IO) {
Runtime.getRuntime().exec(lst.toTypedArray())
}
val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
launch(Dispatchers.Main) {
logsetsAll = allText.toMutableList()
logsets = allText.toMutableList()
adapter.notifyDataSetChanged()
binding.refreshLayout.isRefreshing = false
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun clearLogcat() {
try {
lifecycleScope.launch(Dispatchers.Default) {
val lst = LinkedHashSet<String>()
lst.add("logcat")
lst.add("-c")
withContext(Dispatchers.IO) {
val process = Runtime.getRuntime().exec(lst.toTypedArray())
process.waitFor()
}
launch(Dispatchers.Main) {
logsetsAll.clear()
logsets.clear()
adapter.notifyDataSetChanged()
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_logcat, menu)
val searchItem = menu.findItem(R.id.search_view)
if (searchItem != null) {
val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
filterLogs(newText)
return false
}
})
searchView.setOnCloseListener {
filterLogs("")
false
}
}
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.copy_all -> {
Utils.setClipboard(this, logsets.joinToString("\n"))
toast(R.string.toast_success)
true
}
R.id.clear_all -> {
clearLogcat()
true
}
else -> super.onOptionsItemSelected(item)
}
private fun filterLogs(content: String?): Boolean {
val key = content?.trim()
logsets = if (key.isNullOrEmpty()) {
logsetsAll.toMutableList()
} else {
logsetsAll.filter { it.contains(key) }.toMutableList()
}
adapter?.notifyDataSetChanged()
return true
}
override fun onRefresh() {
getLogcat()
}
}

View File

@@ -0,0 +1,31 @@
package com.v2ray.ang.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
private var mActivity: LogcatActivity = activity
override fun getItemCount() = mActivity.logsets.size
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val content = mActivity.logsets[position]
holder.itemSubSettingBinding.logContent.text = content
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
return MainViewHolder(
ItemRecyclerLogcatBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerLogcatBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
}

View File

@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.net.Uri import android.net.Uri
import android.net.VpnService import android.net.VpnService
@@ -27,7 +28,6 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -41,8 +41,6 @@ import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel import com.v2ray.ang.viewmodel.MainViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -81,6 +79,60 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
private var mItemTouchHelper: ItemTouchHelper? = null private var mItemTouchHelper: ItemTouchHelper? = null
val mainViewModel: MainViewModel by viewModels() val mainViewModel: MainViewModel by viewModels()
// register activity result for requesting permission
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
when (pendingAction) {
Action.IMPORT_QR_CODE_CONFIG ->
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
Action.IMPORT_QR_CODE_URL ->
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
Action.READ_CONTENT_FROM_URI ->
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
addCategory(Intent.CATEGORY_OPENABLE)
}, getString(R.string.title_file_chooser)))
Action.POST_NOTIFICATIONS -> {}
else -> {}
}
} else {
toast(R.string.toast_permission_denied)
}
pendingAction = Action.NONE
}
private var pendingAction: Action = Action.NONE
enum class Action {
NONE,
IMPORT_QR_CODE_CONFIG,
IMPORT_QR_CODE_URL,
READ_CONTENT_FROM_URI,
POST_NOTIFICATIONS
}
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
readContentFromUri(uri)
}
}
private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"))
}
}
private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
@@ -129,11 +181,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
migrateLegacy() migrateLegacy()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RxPermissions(this) if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
.request(Manifest.permission.POST_NOTIFICATIONS) pendingAction = Action.POST_NOTIFICATIONS
.subscribe { requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
if (!it)
toast(R.string.toast_permission_denied)
} }
} }
@@ -226,9 +276,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
Utils.stopVService(this) Utils.stopVService(this)
} }
Observable.timer(500, TimeUnit.MILLISECONDS) lifecycleScope.launch {
.observeOn(AndroidSchedulers.mainThread()) delay(500)
.subscribe {
startV2Ray() startV2Ray()
} }
} }
@@ -346,8 +395,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 +407,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 +428,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 +456,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting //do noting
} }
.show() .show()
@@ -418,14 +468,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()
@@ -460,38 +511,20 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* import config from qrcode * import config from qrcode
*/ */
private fun importQRcode(forConfig: Boolean): Boolean { private fun importQRcode(forConfig: Boolean): Boolean {
// try { val permission = Manifest.permission.CAMERA
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN") if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
// .addCategory(Intent.CATEGORY_DEFAULT) if (forConfig) {
// .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP), requestCode)
// } catch (e: Exception) {
RxPermissions(this)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
if (forConfig)
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
else } else {
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java)) scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
} }
// } } else {
pendingAction = if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL
requestPermissionLauncher.launch(permission)
}
return true return true
} }
private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importBatchConfig(it.data?.getStringExtra("SCAN_RESULT"))
}
}
private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
}
}
/** /**
* import config from clipboard * import config from clipboard
*/ */
@@ -517,7 +550,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()
} }
@@ -612,10 +645,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* import config from sub * import config from sub
*/ */
private fun importConfigViaSub(): Boolean { private fun importConfigViaSub(): Boolean {
// val dialog = AlertDialog.Builder(this)
// .setView(LayoutProgressBinding.inflate(layoutInflater).root)
// .setCancelable(false)
// .show()
binding.pbWaiting.show() binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@@ -623,12 +652,11 @@ 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)
} }
//dialog.dismiss()
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
} }
@@ -643,17 +671,17 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
intent.type = "*/*" intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addCategory(Intent.CATEGORY_OPENABLE)
try { val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) Manifest.permission.READ_MEDIA_IMAGES
} catch (ex: ActivityNotFoundException) { } else {
toast(R.string.toast_require_file_manager) Manifest.permission.READ_EXTERNAL_STORAGE
}
} }
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
val uri = it.data?.data pendingAction = Action.READ_CONTENT_FROM_URI
if (it.resultCode == RESULT_OK && uri != null) { chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
readContentFromUri(uri) } else {
requestPermissionLauncher.launch(permission)
} }
} }
@@ -666,10 +694,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} else { } else {
Manifest.permission.READ_EXTERNAL_STORAGE Manifest.permission.READ_EXTERNAL_STORAGE
} }
RxPermissions(this)
.request(permission) if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
.subscribe {
if (it) {
try { try {
contentResolver.openInputStream(uri).use { input -> contentResolver.openInputStream(uri).use { input ->
importCustomizeConfig(input?.bufferedReader()?.readText()) importCustomizeConfig(input?.bufferedReader()?.readText())
@@ -677,8 +703,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} else } else {
toast(R.string.toast_permission_denied) requestPermissionLauncher.launch(permission)
} }
} }

View File

@@ -8,6 +8,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AngApplication.Companion.application import com.v2ray.ang.AngApplication.Companion.application
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
@@ -23,9 +24,9 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import kotlinx.coroutines.delay
import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit import java.util.*
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter { class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
companion object { companion object {
@@ -143,7 +144,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()
@@ -165,10 +166,13 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
notifyItemChanged(mActivity.mainViewModel.getPosition(guid)) notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) { if (isRunning) {
Utils.stopVService(mActivity) Utils.stopVService(mActivity)
Observable.timer(500, TimeUnit.MILLISECONDS) mActivity.lifecycleScope.launch {
.observeOn(AndroidSchedulers.mainThread()) try {
.subscribe { delay(500)
V2RayServiceManager.startV2Ray(mActivity) V2RayServiceManager.startV2Ray(mActivity)
} catch (e: Exception) {
e.printStackTrace()
}
} }
} }
} }

View File

@@ -20,10 +20,9 @@ import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.AppManagerUtil import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.Collator import java.text.Collator
class PerAppProxyActivity : BaseActivity() { class PerAppProxyActivity : BaseActivity() {
@@ -43,94 +42,40 @@ class PerAppProxyActivity : BaseActivity() {
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET) val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
AppManagerUtil.rxLoadNetworkAppList(this) lifecycleScope.launch {
.subscribeOn(Schedulers.io()) try {
.map { binding.pbWaiting.visibility = View.VISIBLE
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val apps = withContext(Dispatchers.IO) {
val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
if (blacklist != null) { if (blacklist != null) {
it.forEach { one -> appsList.forEach { app ->
if (blacklist.contains(one.packageName)) { app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
one.isSelected = 1
} else {
one.isSelected = 0
} }
} appsList.sortedWith(Comparator { p1, p2 ->
val comparator = Comparator<AppInfo> { p1, p2 ->
when { when {
p1.isSelected > p2.isSelected -> -1 p1.isSelected > p2.isSelected -> -1
p1.isSelected == p2.isSelected -> 0 p1.isSelected == p2.isSelected -> 0
else -> 1 else -> 1
} }
} })
it.sortedWith(comparator)
} else { } else {
val comparator = object : Comparator<AppInfo> {
val collator = Collator.getInstance() val collator = Collator.getInstance()
override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName) appsList.sortedWith(compareBy(collator) { it.appName })
}
it.sortedWith(comparator)
} }
} }
// .map {
// val comparator = object : Comparator<AppInfo> { appsAll = apps
// val collator = Collator.getInstance() adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
// override fun compare(o1: AppInfo, o2: AppInfo) = collator.compare(o1.appName, o2.appName)
// }
// it.sortedWith(comparator)
// }
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
appsAll = it
adapter = PerAppProxyAdapter(this, it, blacklist)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.pbWaiting.visibility = View.GONE binding.pbWaiting.visibility = View.GONE
} } catch (e: Exception) {
/*** binding.pbWaiting.visibility = View.GONE
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { Log.e(ANG_PACKAGE, "Error loading apps", e)
var dst = 0
val threshold = resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 2
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
dst += dy
if (dst > threshold) {
header_view.hide()
dst = 0
} else if (dst < -20) {
header_view.show()
dst = 0
} }
} }
var hiding = false
fun View.hide() {
val target = -height.toFloat()
if (hiding || translationY == target) return
animate()
.translationY(target)
.setInterpolator(AccelerateInterpolator(2F))
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
hiding = false
}
})
hiding = true
}
var showing = false
fun View.show() {
val target = 0f
if (showing || translationY == target) return
animate()
.translationY(target)
.setInterpolator(DecelerateInterpolator(2F))
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
showing = false
}
})
showing = true
}
})
***/
binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked -> binding.switchPerAppProxy.setOnCheckedChangeListener { _, isChecked ->
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked) MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY, isChecked)
} }
@@ -140,36 +85,6 @@ class PerAppProxyActivity : BaseActivity() {
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked) MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
} }
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false) binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
/***
et_search.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
//hide
var imm: InputMethodManager = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS)
val key = v.text.toString().toUpperCase()
val apps = ArrayList<AppInfo>()
if (TextUtils.isEmpty(key)) {
appsAll?.forEach {
apps.add(it)
}
} else {
appsAll?.forEach {
if (it.appName.toUpperCase().indexOf(key) >= 0) {
apps.add(it)
}
}
}
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
recycler_view.adapter = adapter
adapter?.notifyDataSetChanged()
true
} else {
false
}
}
***/
} }
override fun onPause() { override fun onPause() {

View File

@@ -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()

View File

@@ -1,11 +1,13 @@
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
@@ -37,6 +39,16 @@ class RoutingSettingActivity : BaseActivity() {
resources.getStringArray(R.array.preset_rulesets) resources.getStringArray(R.array.preset_rulesets)
} }
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
} else {
toast(R.string.toast_permission_denied)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
@@ -83,13 +95,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)
@@ -99,11 +111,9 @@ class RoutingSettingActivity : BaseActivity() {
e.printStackTrace() e.printStackTrace()
} }
}.show() }.show()
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting //do nothing
} }
.show() .show()
true true
@@ -120,7 +130,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 +141,17 @@ 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 -> {
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
true
}
R.id.export_rulesets_to_clipboard -> { R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets() val rulesetList = MmkvManager.decodeRoutingRulesets()
@@ -153,10 +167,37 @@ 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())
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
} }

View File

@@ -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 {

View File

@@ -4,13 +4,23 @@ import android.Manifest
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
class ScScannerActivity : BaseActivity() { class ScScannerActivity : BaseActivity() {
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
} else {
toast(R.string.toast_permission_denied)
finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_none) setContentView(R.layout.activity_none)
@@ -18,19 +28,10 @@ class ScScannerActivity : BaseActivity() {
} }
fun importQRcode(): Boolean { fun importQRcode(): Boolean {
RxPermissions(this) requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
.request(Manifest.permission.CAMERA)
.subscribe { granted ->
if (granted) {
scanQRCode.launch(Intent(this, ScannerActivity::class.java))
} else {
toast(R.string.toast_permission_denied)
}
}
return true return true
} }
private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val scanQRCode = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) { if (it.resultCode == RESULT_OK) {
val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty() val scanResult = it.data?.getStringExtra("SCAN_RESULT").orEmpty()
@@ -46,5 +47,4 @@ class ScScannerActivity : BaseActivity() {
} }
finish() finish()
} }
} }

View File

@@ -2,13 +2,15 @@ package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Build import android.os.Build
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 androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import com.tbruyelle.rxpermissions3.RxPermissions import androidx.core.app.ActivityCompat
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.toast import com.v2ray.ang.extension.toast
@@ -21,6 +23,37 @@ import io.github.g00fy2.quickie.config.ScannerConfig
class ScannerActivity : BaseActivity() { class ScannerActivity : BaseActivity() {
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult) private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
try {
val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
if (text.isNullOrEmpty()) {
toast(R.string.toast_decoding_failed)
} else {
finished(text)
}
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.toast_decoding_failed)
}
}
}
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
showFileChooser()
} else {
toast(R.string.toast_permission_denied)
}
}
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -72,14 +105,11 @@ class ScannerActivity : BaseActivity() {
} else { } else {
Manifest.permission.READ_EXTERNAL_STORAGE Manifest.permission.READ_EXTERNAL_STORAGE
} }
RxPermissions(this)
.request(permission) if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
.subscribe { granted ->
if (granted) {
showFileChooser() showFileChooser()
} else { } else {
toast(R.string.toast_permission_denied) requestPermissionLauncher.launch(permission)
}
} }
true true
} }
@@ -100,26 +130,4 @@ class ScannerActivity : BaseActivity() {
toast(R.string.toast_require_file_manager) toast(R.string.toast_require_file_manager)
} }
} }
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
try {
val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
if (text.isNullOrEmpty()) {
toast(R.string.toast_decoding_failed)
} else {
finished(text)
}
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.toast_decoding_failed)
}
}
}
} }

View File

@@ -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,10 @@ 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_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
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 +153,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 +161,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 +212,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 +246,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 +324,23 @@ 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)
et_bandwidth_down?.text = Utils.getEditable(config.bandwidthDown)
et_bandwidth_up?.text = Utils.getEditable(config.bandwidthUp)
} }
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)
@@ -318,11 +356,11 @@ class ServerActivity : BaseActivity() {
et_sni?.text = Utils.getEditable(config.sni) et_sni?.text = Utils.getEditable(config.sni)
config.fingerPrint?.let { config.fingerPrint?.let {
val utlsIndex = Utils.arrayFind(uTlsItems, it) val utlsIndex = Utils.arrayFind(uTlsItems, it)
sp_stream_fingerprint?.setSelection(utlsIndex) utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) }
} }
config.alpn?.let { config.alpn?.let {
val alpnIndex = Utils.arrayFind(alpns, it) val alpnIndex = Utils.arrayFind(alpns, it)
sp_stream_alpn?.setSelection(alpnIndex) alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) }
} }
if (config.security == TLS) { if (config.security == TLS) {
container_allow_insecure?.visibility = View.VISIBLE container_allow_insecure?.visibility = View.VISIBLE
@@ -333,7 +371,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 +391,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 +444,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 +466,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 +480,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 +508,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())
@@ -472,6 +517,8 @@ class ServerActivity : BaseActivity() {
config.portHopping = et_port_hop?.text?.toString() config.portHopping = et_port_hop?.text?.toString()
config.portHoppingInterval = et_port_hop_interval?.text?.toString() config.portHoppingInterval = et_port_hop_interval?.text?.toString()
config.pinSHA256 = et_pinsha256?.text?.toString() config.pinSHA256 = et_pinsha256?.text?.toString()
config.bandwidthDown = et_bandwidth_down?.text?.toString()
config.bandwidthUp = et_bandwidth_up?.text?.toString()
} }
} }
@@ -492,6 +539,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 +555,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 +572,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 +606,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()

View File

@@ -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()

View File

@@ -40,8 +40,10 @@ 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) }
private val vpnBypassLan by lazy { findPreference<ListPreference>(AppConfig.PREF_VPN_BYPASS_LAN) }
private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) } private val mux by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_MUX_ENABLED) }
private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) } private val muxConcurrency by lazy { findPreference<EditTextPreference>(AppConfig.PREF_MUX_CONCURRENCY) }
@@ -57,9 +59,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 +143,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 +154,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 +178,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 +199,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 +217,6 @@ class SettingsActivity : BaseActivity() {
fragmentInterval, fragmentInterval,
autoUpdateInterval, autoUpdateInterval,
socksPort, socksPort,
httpPort,
remoteDns, remoteDns,
domesticDns, domesticDns,
delayTestUrl delayTestUrl
@@ -244,6 +247,7 @@ class SettingsActivity : BaseActivity() {
} }
listOf( listOf(
AppConfig.PREF_VPN_BYPASS_LAN,
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
AppConfig.PREF_MUX_XUDP_QUIC, AppConfig.PREF_MUX_XUDP_QUIC,
AppConfig.PREF_FRAGMENT_PACKETS, AppConfig.PREF_FRAGMENT_PACKETS,
@@ -264,8 +268,11 @@ 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
vpnBypassLan?.isEnabled = vpn
vpn
if (vpn) { if (vpn) {
updateLocalDns( updateLocalDns(
MmkvManager.decodeSettingsBool( MmkvManager.decodeSettingsBool(

View File

@@ -113,7 +113,7 @@ class SubEditActivity : BaseActivity() {
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()

View File

@@ -4,10 +4,15 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLDecoder import java.net.URLDecoder
class UrlSchemeActivity : BaseActivity() { class UrlSchemeActivity : BaseActivity() {
@@ -66,7 +71,9 @@ class UrlSchemeActivity : BaseActivity() {
decodedUrl += "#${fragment}" decodedUrl += "#${fragment}"
} }
Log.d("UrlScheme-decodedUrl", decodedUrl) Log.d("UrlScheme-decodedUrl", decodedUrl)
lifecycleScope.launch(Dispatchers.IO) {
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false) val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
withContext(Dispatchers.Main) {
if (count + countSub > 0) { if (count + countSub > 0) {
toast(R.string.import_subscription_success) toast(R.string.import_subscription_success)
} else { } else {
@@ -75,3 +82,5 @@ class UrlSchemeActivity : BaseActivity() {
} }
} }
} }
}
}

View File

@@ -19,7 +19,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -50,6 +49,38 @@ class UserAssetActivity : BaseActivity() {
val extDir by lazy { File(Utils.userAssetPath(this)) } val extDir by lazy { File(Utils.userAssetPath(this)) }
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat") val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
private val requestStoragePermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
try {
chooseFile.launch(
Intent.createChooser(
intent,
getString(R.string.title_file_chooser)
)
)
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
}
} else {
toast(R.string.toast_permission_denied)
}
}
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
} else {
toast(R.string.toast_permission_denied)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -75,6 +106,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)
} }
@@ -85,27 +117,7 @@ class UserAssetActivity : BaseActivity() {
} else { } else {
Manifest.permission.READ_EXTERNAL_STORAGE Manifest.permission.READ_EXTERNAL_STORAGE
} }
RxPermissions(this) requestStoragePermissionLauncher.launch(permission)
.request(permission)
.subscribe {
if (it) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
try {
chooseFile.launch(
Intent.createChooser(
intent,
getString(R.string.title_file_chooser)
)
)
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
}
} else
toast(R.string.toast_permission_denied)
}
} }
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@@ -156,6 +168,33 @@ class UserAssetActivity : BaseActivity() {
null null
} }
private fun importAssetFromQRcode(): Boolean {
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
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 +337,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()

View File

@@ -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()

View File

@@ -0,0 +1,30 @@
package com.v2ray.ang.util
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import com.v2ray.ang.dto.AppInfo
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
object AppManagerUtil {
suspend fun loadNetworkAppList(context: Context): ArrayList<AppInfo> =
withContext(Dispatchers.IO) {
val packageManager = context.packageManager
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS)
val apps = ArrayList<AppInfo>()
for (pkg in packages) {
val applicationInfo = pkg.applicationInfo ?: continue
val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo)
}
return@withContext apps
}
}

View File

@@ -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