Compare commits

...

220 Commits

Author SHA1 Message Date
2dust 1672494ee9 1.9.45 2025-04-01 10:26:59 +08:00
2dust bbbbc72d22 Update AndroidLibXrayLite 2025-04-01 10:26:45 +08:00
2dust 1e7f49b756 up 1.9.44 2025-03-30 19:14:55 +08:00
2dust ac4c0f7ee1 Optimize and improve Log 2025-03-30 19:05:35 +08:00
2dust 6cc91b1a89 Optimize and improve log
Use Log.e() instead of e.printStackTrace()
2025-03-30 17:40:36 +08:00
2dust 45facff41d Optimize and improve Utils 2025-03-30 16:28:14 +08:00
2dust ee703e6c95 Remove ads rules from default routing rules 2025-03-30 11:18:39 +08:00
hhhkkmk 87213c34a6 Revert "Optimization (#4426)" (#4437)
This reverts commit d111328541.
2025-03-29 18:06:09 +08:00
solokot 73a7c76183 Update Russian translation (#4435) 2025-03-29 18:05:43 +08:00
Pk-web6936 ed5282f2b3 Update dependencies (#4432)
* Update libs.versions.toml

Update agp

* Update validate-fastlane-supply-metadata

Update validate-fastlane-supply-metadata
2025-03-29 10:47:09 +08:00
Pk-web6936 390c657047 Switch to Loyalsoldier's v2ray-rules-dat (#4431)
https://github.com/2dust/AndroidLibXrayLite/pull/132/files
2025-03-29 10:46:49 +08:00
Pk-web6936 7071072862 Fix Arabic Language (#4430)
* Fix Arabic Language

* Fix Arabic Language

Fix Arabic Language
2025-03-29 10:46:29 +08:00
Pk-web6936 d111328541 Optimization (#4426)
* Optimization

Add security flags to CFLAGS and LDFLAGS. Use local variables instead of global variables. Clean up and simplify the script.

* Optimizition

Add security flags to CFLAGS and LDFLAGS. Simplify and organize the file. Makefile

* Optimization

Add security flags to CFLAGS and LDFLAGS. Use local variables instead of global variables. Clean up and simplify the script.

* Optimization
2025-03-29 10:44:21 +08:00
Pk-web6936 76cb2aaf46 Update Persian translate (#4424) 2025-03-29 10:44:03 +08:00
2dust 7ff1397163 Optimize the test true delay function
When testing, remove unnecessary configurations such as dns and routing to reduce resource usage, except for custom configurations.
2025-03-29 10:43:25 +08:00
2dust bcf5d49a3d up 1.9.43 2025-03-28 16:20:12 +08:00
2dust 4fffb17283 Added user asset file settings in the drawer menu 2025-03-28 15:08:52 +08:00
2dust 83b8bdfdf4 Optimize UI 2025-03-28 15:07:24 +08:00
2dust cc1538a24d Code clean 2025-03-28 11:02:45 +08:00
2dust eb19199d18 Update .gitignore 2025-03-28 10:56:59 +08:00
Pk-web6936 441e5ef8d5 Consolidate and Optimize .gitignore Files (#4421)
* Delete .gitignore

* Delete V2rayNG/.gitignore

* Delete V2rayNG/app/.gitignore

* Create .gitignore

* Add New .gitignore
2025-03-28 09:24:22 +08:00
2dust d768774aad Optimize and improve toast
Migrate from https://github.com/PureWriter/ToastCompat to https://github.com/GrenderG/Toasty
2025-03-27 17:43:58 +08:00
2dust c3d83907a5 Optimize and improve 2025-03-27 13:46:35 +08:00
solokot b52a98ae5e Update Russian translation (#4416) 2025-03-27 09:12:52 +08:00
Pk-web6936 a70e4089e3 Update Persian translate (#4415) 2025-03-27 09:12:41 +08:00
2dust 397989769c up 1.9.42 2025-03-26 18:54:42 +08:00
2dust 796676abdc Enable double column display
The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect.
2025-03-26 18:48:14 +08:00
2dust 3c93ccb86c Optimize and improve 2025-03-25 20:26:28 +08:00
2dust fc8c74184a Optimize and improve for toast 2025-03-25 17:44:30 +08:00
solokot 1939e6b5cf Optimize Russian translation (#4411) 2025-03-25 17:05:12 +08:00
2dust 1c2ac9385d Fix
https://github.com/2dust/v2rayNG/issues/4410
2025-03-25 10:49:07 +08:00
2dust 7513f1fe07 Optimize and improve 2025-03-25 10:44:06 +08:00
2dust 63e2c02daa Fix
https://github.com/2dust/v2rayNG/issues/4408
2025-03-24 20:17:49 +08:00
2dust fec76385e1 Bug fix
https://github.com/2dust/v2rayNG/issues/4409
2025-03-24 20:17:05 +08:00
2dust bf21428746 up 1.9.41 2025-03-24 15:45:56 +08:00
2dust 01ab8dad17 Optimize and improve removeWhiteSpace 2025-03-24 15:44:08 +08:00
2dust 8974f73841 Fix
https://github.com/2dust/v2rayNG/issues/4407
2025-03-24 14:43:07 +08:00
Pk-web6936 d3a2a2413c Update Persian translate (#4404)
* Update Persian translate

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml

* Update strings.xml
2025-03-24 10:50:13 +08:00
2dust 9b52863270 Bug fix
https://github.com/2dust/v2rayNG/issues/4405
2025-03-24 10:34:59 +08:00
2dust 733914a7a7 Add colorPrimaryDark to theme 2025-03-23 19:25:20 +08:00
2dust 7b0ab1ea4d up 1.9.40 2025-03-23 16:37:22 +08:00
2dust 51ff95f071 Update AndroidLibXrayLite 2025-03-23 16:37:01 +08:00
2dust fc7aca46ce Update hysteria 2025-03-23 16:36:50 +08:00
2dust 78e34caff3 Optimize and improve style 2025-03-23 15:12:28 +08:00
2dust d2aecd4dee Optimize and improve string 2025-03-23 15:12:14 +08:00
Pk-web6936 9be1083495 Fix (#4401)
* Update fastlane.yml

* Update libs.versions.toml

* Update libs.versions.toml

* Update fastlane.yml

* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml
2025-03-23 15:11:38 +08:00
2dust 38eb9ee13f Update libs.versions.toml 2025-03-23 10:44:59 +08:00
Pk-web6936 15b36dfc57 Update dependencies (#4400)
* Update libs.versions.toml

* Update build.yml
2025-03-23 10:29:59 +08:00
2dust 40a83f7cac Optimize and improve 2025-03-22 18:02:51 +08:00
2dust fd9f912c18 Optimize and improve theme 2025-03-22 17:50:55 +08:00
hhhkkmk eef6e60dbb optimization (#4393)
* optimization

* fix

* fix2

* fix3
2025-03-21 18:13:04 +08:00
Pk-web6936 f0de5275b9 Update kotlin version to 2.1.20 (#4397)
* Update kotlin version to 2.1.20

Update kotlin version to 2.1.20

* Update kotlin version to 2.1.20

Update kotlin version to 2.1.20

* Update gradle-wrapper.properties
2025-03-21 18:10:10 +08:00
2dust 4eb5c0263c Optimize and improve 2025-03-21 16:53:30 +08:00
solokot 7a0d997a81 Update Russian translation (#4396) 2025-03-21 13:57:38 +08:00
Pk-web6936 17b7c6d357 Update Persian translate (#4395)
* Update Persian translate 

Update Persian translate

* Update strings.xml
2025-03-21 13:56:35 +08:00
2dust 206f2cb306 Optimize and improve string 2025-03-20 20:16:54 +08:00
2dust 1bc433097b Add per_app_proxy_settings to the drawer menu 2025-03-20 16:14:41 +08:00
2dust 9bee5fbe99 Refactoring to import custom configuration function
Most of the functions have been merged into the existing menu, only the local import function needs to be added.
The code is temporarily commented and will be deleted after user testing.
2025-03-20 15:36:06 +08:00
2dust e819798d80 Optimize and improve 2025-03-20 14:37:53 +08:00
2dust 14ff9eb527 Optimize and improve PerAppProxyActivity 2025-03-20 14:37:38 +08:00
2dust a60f45ce31 The timeout was changed from 30s to 15s 2025-03-20 12:06:13 +08:00
2dust 0b9a96209f Improved progress bar 2025-03-20 12:00:33 +08:00
2dust 3747e58e4e Adjust dimen resources 2025-03-20 10:24:38 +08:00
2dust 28b1788dc1 Remove the use of androidx.cardview.widget.CardView in the list 2025-03-19 20:21:12 +08:00
2dust 12ab2954b0 Using AI to improve function documentation 2025-03-19 14:12:30 +08:00
2dust d0e8937f03 Using AI to improve function documentation 2025-03-19 12:15:43 +08:00
2dust 1a7ab97a3a Using AI to improve function documentation 2025-03-19 12:08:19 +08:00
2dust ef4145787b Using AI to improve function documentation 2025-03-19 11:57:56 +08:00
2dust 899e4c1b14 Using AI to improve function documentation 2025-03-19 11:41:12 +08:00
2dust faa4385087 Using AI to improve function documentation 2025-03-19 11:01:07 +08:00
2dust 172d9fd093 Migrate SpeedtestUtil to SpeedtestManager 2025-03-19 10:43:00 +08:00
2dust 093716baaa Optimize and improve SettingsManager 2025-03-19 10:38:58 +08:00
2dust 0165ad54b3 Update build.gradle.kts 2025-03-18 21:04:51 +08:00
2dust b52dd33102 Optimize and improve V2RayServiceManager 2025-03-18 21:00:22 +08:00
2dust 00aed90f2f Add NotificationService, refactor V2RayServiceManager and V2RayVpnService 2025-03-18 17:21:42 +08:00
2dust 5f3d7c0213 Refactor the start and stop service 2025-03-18 15:05:42 +08:00
2dust 401f051774 Update AndroidLibXrayLite 2025-03-18 15:04:33 +08:00
2dust e14b48f3eb Code clean 2025-03-18 15:04:25 +08:00
2dust 1972f83b86 Add HttpUtil, refactor the network connection function 2025-03-18 09:55:40 +08:00
2dust 55a11bbeee Update libs.versions.toml 2025-03-18 09:31:00 +08:00
2dust ca32ee3a0e Update proxy_packagename.txt 2025-03-17 20:55:08 +08:00
2dust 159a370286 Changed tun2socks config private vlan
https://github.com/2dust/v2rayNG/issues/4373
2025-03-16 14:44:25 +08:00
2dust d1904d52d9 Bug fix
Exception java.lang.StackOverflowError: stack size 8188KB
2025-03-16 10:24:32 +08:00
Noo6 5e993fd91b Fix subscription link redirect handling (#4378)
* Fix subscription link redirect handling

* optimize code format
2025-03-12 08:02:35 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ bada7c93d7 Allow user certificate (#4375)
* Add network security config for user CA

* Use network security config

* mv

* Suppress Inspection for user CA
2025-03-12 08:02:02 +08:00
DHR60 38850597f3 Add kcp DNS masquerade support (#4368) 2025-03-07 17:45:21 +08:00
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 https://github.com/2dust/AndroidLibXrayLite/commit/df181a30659867653270d7306780da5aa7c03a6d

* add missing includes of https://github.com/XTLS/badvpn/commit/dc99ade18dbd32a286bb4159bf9a9779c21a66ed

* 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
197 changed files with 7890 additions and 3466 deletions
+119 -53
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:
@@ -15,79 +16,144 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4.2.2
with:
submodules: 'recursive'
fetch-depth: '0'
- name: Setup Android SDK
uses: android-actions/setup-android@v3.2.0
with:
log-accepted-android-sdk-licenses: false
cmdline-tools-version: '12266719'
packages: 'platforms;android-35 build-tools;35.0.0 platform-tools'
- name: Install NDK
run: |
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
--channel=3 \
--install "ndk;29.0.13113456"
echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.13113456" >> $GITHUB_ENV
sed -i '10i\
\
ndkVersion = "29.0.13113456"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
- name: Restore cached libtun2socks
id: cache-libtun2socks-restore
uses: actions/cache/restore@v4
with:
path: ${{ github.workspace }}/libs
key: libtun2socks-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/badvpn/HEAD') }}-${{ hashFiles('.git/modules/libancillary/HEAD') }}
- name: Build libtun2socks
if: steps.cache-libtun2socks-restore.outputs.cache-hit != 'true'
run: |
bash compile-tun2socks.sh
- 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 }}-${{ env.NDK_HOME }}-${{ 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.12
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 }}-${{ env.NDK_HOME }}-${{ 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.4.0
with:
go-version-file: 'AndroidLibXrayLite/go.mod'
cache: false
- name: Build libhysteria2
if: steps.cache-libhysteria2-restore.outputs.cache-hit != 'true'
run: |
bash libhysteria2.sh
- 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 }}-${{ env.NDK_HOME }}-${{ 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.7.0
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '21'
- name: Setup Golang - name: Decode Keystore
uses: actions/setup-go@v5 uses: timheuer/base64-to-file@v1.2.4
id: android_keystore
with: with:
go-version: '1.23.2' fileName: "android_keystore.jks"
cache: false encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }}
- 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
uses: android-actions/setup-android@v3
- name: Build dependencies
run: |
mkdir ${{ github.workspace }}/build
cd ${{ github.workspace }}/build
git clone --depth=1 -b main https://github.com/2dust/AndroidLibXrayLite.git
cd AndroidLibXrayLite
go get github.com/xtls/xray-core@${{ github.event.inputs.XRAY_CORE_VERSION }} || true
gomobile init
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
echo "sdk.dir=${ANDROID_HOME}" > local.properties
chmod 755 gradlew chmod 755 gradlew
./gradlew assembleDebug ./gradlew licenseFdroidReleaseReport
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
- name: Upload arm64-v8a APK - name: Upload arm64-v8a APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4.6.2
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.6.2
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.6.2
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
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.1.0
+61
View File
@@ -1,5 +1,66 @@
# Ignore data and key store files
*.dat *.dat
*.jks *.jks
# Ignore output JSON file
V2rayNG/app/release/output.json V2rayNG/app/release/output.json
# Ignore IDE and build system directories
.idea/ .idea/
.gradle/ .gradle/
*.iml
# Ignore local properties and DS_Store files
/local.properties
.DS_Store
# Ignore build directories and captures
/build
/captures
V2rayNG/app/build
V2rayNG/build
V2rayNG/local.properties
# Ignore APK and AAR files
*.apk
*.aar
# Ignore signing properties
signing.properties
# Ignore shared object files
*.so
# Ignore Google services JSON
V2rayNG/app/google-services.json
# Additional common Android/Java ignores
*.log
*.tmp
*.bak
*.swp
*.orig
*.class
*.jar
*.war
*.ear
# Ignore executable files
*.exe
*.dll
*.obj
*.o
*.pyc
*.pyo
# Ignore files from other IDEs
.vscode/
.classpath
.project
.settings/
*.sublime-workspace
*.sublime-project
# Ignore OS-specific files
Thumbs.db
.DS_Store
+12
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
-20
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)
Submodule AndroidLibXrayLite added at 2715a3b110
+2 -2
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.20-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)
@@ -21,7 +21,7 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
#### Geoip and Geosite #### Geoip and Geosite
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device) - geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy) - download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
- latest official [domain list](https://github.com/v2fly/domain-list-community) and [ip list](https://github.com/v2fly/geoip) can be imported manually - latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6) - possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki) ### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
-10
View File
@@ -1,10 +0,0 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
*.apk
signing.properties
*.aar
-2
View File
@@ -1,2 +0,0 @@
/build
/google-services.json
+77 -34
View File
@@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
id("com.jaredsburrows.license")
} }
android { android {
@@ -11,20 +12,26 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 21 minSdk = 21
targetSdk = 35 targetSdk = 35
versionCode = 614 versionCode = 645
versionName = "1.9.18" versionName = "1.9.45"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
splits { splits {
abi { abi {
isEnable = true isEnable = true
include( reset()
"arm64-v8a", if (abiFilterList != null && abiFilterList.isNotEmpty()) {
"armeabi-v7a", include(*abiFilterList.toTypedArray())
"x86_64", } else {
"x86" include(
) "arm64-v8a",
isUniversalApk = true "armeabi-v7a",
"x86_64",
"x86"
)
}
isUniversalApk = abiFilterList.isNullOrEmpty()
} }
} }
@@ -41,6 +48,19 @@ android {
} }
} }
flavorDimensions.add("distribution")
productFlavors {
create("fdroid") {
dimension = "distribution"
applicationIdSuffix = ".fdroid"
buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"")
}
create("playstore") {
dimension = "distribution"
buildConfigField("String", "DISTRIBUTION", "\"Play Store\"")
}
}
sourceSets { sourceSets {
getByName("main") { getByName("main") {
jniLibs.srcDirs("libs") jniLibs.srcDirs("libs")
@@ -49,34 +69,56 @@ android {
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString() jvmTarget = JavaVersion.VERSION_17.toString()
} }
applicationVariants.all { applicationVariants.all {
val variant = this val variant = this
val versionCodes = val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4) if (isFdroid) {
val versionCodes =
mapOf(
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
)
variant.outputs variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output -> .forEach { output ->
val abi = if (output.getFilter("ABI") != null) val abi = output.getFilter("ABI") ?: "universal"
output.getFilter("ABI") output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
else if (versionCodes.containsKey(abi)) {
"universal" output.versionCodeOverride =
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk" } else {
if (versionCodes.containsKey(abi)) { return@forEach
output.versionCodeOverride = }
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} else {
return@forEach
} }
} } else {
val versionCodes =
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
val abi = if (output.getFilter("ABI") != null)
output.getFilter("ABI")
else
"universal"
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if (versionCodes.containsKey(abi)) {
output.versionCodeOverride =
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
} else {
return@forEach
}
}
}
} }
buildFeatures { buildFeatures {
@@ -103,10 +145,11 @@ dependencies {
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.preference.ktx) implementation(libs.preference.ktx)
implementation(libs.recyclerview) implementation(libs.recyclerview)
implementation(libs.androidx.swiperefreshlayout)
// UI Libraries // UI Libraries
implementation(libs.material) implementation(libs.material)
implementation(libs.toastcompat) implementation(libs.toasty)
implementation(libs.editorkit) implementation(libs.editorkit)
implementation(libs.flexbox) implementation(libs.flexbox)
@@ -115,16 +158,15 @@ dependencies {
implementation(libs.gson) implementation(libs.gson)
// Reactive and Utility Libraries // Reactive and Utility Libraries
implementation(libs.rxjava) implementation(libs.kotlinx.coroutines.android)
implementation(libs.rxandroid) implementation(libs.kotlinx.coroutines.core)
implementation(libs.rxpermissions)
// Language and Processing Libraries // Language and Processing Libraries
implementation(libs.language.base) implementation(libs.language.base)
implementation(libs.language.json) implementation(libs.language.json)
// Intent and Utility Libraries // Intent and Utility Libraries
implementation(libs.quickie.bundled) implementation(libs.quickie.foss)
implementation(libs.core) implementation(libs.core)
// AndroidX Lifecycle and Architecture Components // AndroidX Lifecycle and Architecture Components
@@ -145,4 +187,5 @@ dependencies {
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
testImplementation(libs.org.mockito.mockito.inline) testImplementation(libs.org.mockito.mockito.inline)
testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.kotlin)
coreLibraryDesugaring(libs.desugar.jdk.libs)
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -35,7 +35,6 @@
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- <useapplications-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission <uses-permission
@@ -52,6 +51,7 @@
android:banner="@mipmap/ic_banner" android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppThemeDayNight" android:theme="@style/AppThemeDayNight"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
@@ -212,7 +212,8 @@
android:icon="@drawable/ic_stat_name" android:icon="@drawable/ic_stat_name"
android:label="@string/app_tile_name" android:label="@string/app_tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:process=":RunSoLibV2RayDaemon"> android:process=":RunSoLibV2RayDaemon"
tools:targetApi="24">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
@@ -20,13 +20,6 @@
"port": "443", "port": "443",
"network": "udp" "network": "udp"
}, },
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{ {
"remarks": "绕过局域网IP", "remarks": "绕过局域网IP",
"outboundTag": "direct", "outboundTag": "direct",
@@ -5,13 +5,6 @@
"port": "443", "port": "443",
"network": "udp" "network": "udp"
}, },
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{ {
"remarks": "绕过局域网IP", "remarks": "绕过局域网IP",
"outboundTag": "direct", "outboundTag": "direct",
@@ -13,13 +13,6 @@
"port": "443", "port": "443",
"network": "udp" "network": "udp"
}, },
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{ {
"remarks": "绕过局域网IP", "remarks": "绕过局域网IP",
"outboundTag": "direct", "outboundTag": "direct",
@@ -99,10 +92,5 @@
"domain": [ "domain": [
"geosite:cn" "geosite:cn"
] ]
},
{
"remarks": "最终代理",
"port": "0-65535",
"outboundTag": "proxy"
} }
] ]
@@ -5,13 +5,6 @@
"port": "443", "port": "443",
"network": "udp" "network": "udp"
}, },
{
"remarks": "Block ads and trackers",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{ {
"remarks": "Direct LAN IP", "remarks": "Direct LAN IP",
"outboundTag": "direct", "outboundTag": "direct",
@@ -40,10 +33,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
@@ -4,6 +4,7 @@ au.com.shiftyjelly.pocketcasts
bbc.mobile.news.ww bbc.mobile.news.ww
be.mygod.vpnhotspot be.mygod.vpnhotspot
ch.protonmail.android ch.protonmail.android
cm.aptoide.pt
co.wanqu.android co.wanqu.android
com.alphainventor.filemanager com.alphainventor.filemanager
com.amazon.kindle com.amazon.kindle
@@ -34,7 +35,9 @@ com.chrome.canary
com.chrome.dev com.chrome.dev
com.cl.newt66y com.cl.newt66y
com.cradle.iitc_mobile com.cradle.iitc_mobile
org.exarhteam.iitc_mobile
com.cygames.shadowverse com.cygames.shadowverse
com.dcard.freedom
com.devhd.feedly com.devhd.feedly
com.devolver.reigns2 com.devolver.reigns2
com.discord com.discord
@@ -108,6 +111,7 @@ com.ifttt.ifttt
com.imgur.mobile com.imgur.mobile
com.innologica.inoreader com.innologica.inoreader
com.instagram.android com.instagram.android
com.instagram.lite
com.instapaper.android com.instapaper.android
com.jarvanh.vpntether com.jarvanh.vpntether
com.kapp.youtube.final com.kapp.youtube.final
@@ -115,6 +119,7 @@ com.klinker.android.twitter_l
com.lastpass.lpandroid com.lastpass.lpandroid
com.linecorp.linelite com.linecorp.linelite
com.lingodeer com.lingodeer
com.ltnnews.news
com.mediapods.tumbpods com.mediapods.tumbpods
com.mgoogle.android.gms com.mgoogle.android.gms
com.microsoft.emmx com.microsoft.emmx
@@ -159,6 +164,7 @@ com.slack
com.snaptube.premium com.snaptube.premium
com.sololearn com.sololearn
com.sonelli.juicessh com.sonelli.juicessh
com.sparkslab.dcardreader
com.spotify.music com.spotify.music
com.tencent.huatuo com.tencent.huatuo
com.termux com.termux
@@ -173,10 +179,13 @@ com.twitter.android
com.u91porn com.u91porn
com.u9porn com.u9porn
com.ubisoft.dance.justdance2015companion com.ubisoft.dance.justdance2015companion
com.udn.news
com.utopia.pxview com.utopia.pxview
com.valvesoftware.android.steam.communimunity
com.valvesoftware.android.steam.community com.valvesoftware.android.steam.community
com.vanced.manager
com.vanced.android.youtube com.vanced.android.youtube
com.vanced.android.apps.youtube.music
com.mgoogle.android.gms
com.vimeo.android.videoapp com.vimeo.android.videoapp
com.vivaldi.browser com.vivaldi.browser
com.vivaldi.browser.snapshot com.vivaldi.browser.snapshot
@@ -186,10 +195,12 @@ com.wire
com.wuxiangai.refactor com.wuxiangai.refactor
com.xda.labs com.xda.labs
com.xvideos.app com.xvideos.app
com.yahoo.mobile.client.android.superapp
com.yandex.browser com.yandex.browser
com.yandex.browser.beta com.yandex.browser.beta
com.yandex.browser.alpha com.yandex.browser.alpha
com.z28j.feel com.z28j.feel
com.zhiliaoapp.musically
con.medium.reader con.medium.reader
de.apkgrabber de.apkgrabber
de.robv.android.xposed.installer de.robv.android.xposed.installer
@@ -210,6 +221,7 @@ jp.bokete.app.android
jp.naver.line.android jp.naver.line.android
jp.pxv.android jp.pxv.android
luo.speedometergpspro luo.speedometergpspro
m.cna.com.tw.App
mark.via.gp mark.via.gp
me.tshine.easymark me.tshine.easymark
net.teeha.android.url_shortener net.teeha.android.url_shortener
@@ -226,6 +238,7 @@ org.mozilla.firefox_beta
org.mozilla.focus org.mozilla.focus
org.schabi.newpipe org.schabi.newpipe
org.telegram.messenger org.telegram.messenger
org.telegram.messenger.web
org.telegram.multi org.telegram.multi
org.telegram.plus org.telegram.plus
org.thunderdog.challegram org.thunderdog.challegram
@@ -239,3 +252,162 @@ tw.com.gamer.android.activecenter
videodownloader.downloadvideo.downloader videodownloader.downloadvideo.downloader
uk.co.bbc.learningenglish uk.co.bbc.learningenglish
com.ted.android com.ted.android
de.danoeh.antennapod
com.kiwibrowser.browser
nekox.messenger
com.nextcloud.client
com.aurora.store
com.aurora.adroid
chat.simplex.app
im.vector.app
network.loki.messenger
eu.siacs.conversations
xyz.nextalone.nagram
net.programmierecke.radiodroid2
im.fdx.v2ex
ml.docilealligator.infinityforreddit
com.bytemyth.ama
app.vanadium.browser
com.cakewallet.cake_wallet
org.purplei2p.i2pd
dk.tacit.android.foldersync.lite
com.nononsenseapps.feeder
com.m2049r.xmrwallet
com.paypal.android.p2pmobile
com.google.android.apps.googlevoice
com.readdle.spark
org.torproject.torbrowser
com.deepl.mobiletranslator
com.microsoft.bing
com.keylesspalace.tusky
com.ottplay.ottplay
ru.iptvremote.android.iptv.pro
jp.naver.line.android
com.xmflsct.app.tooot
com.forem.android
app.revanced.android.youtube
com.mgoogle.android.gms
com.pionex.client
vip.mytokenpocket
im.token.app
com.linekong.mars24
com.feixiaohao
com.aicoin.appandroid
com.binance.dev
com.kraken.trade
com.okinc.okex.gp
com.authy.authy
air.com.rosettastone.mobile.CoursePlayer
com.blizzard.bma
com.amazon.kindle
com.google.android.apps.fitness
net.tsapps.appsales
com.wemesh.android
com.google.android.apps.googleassistant
allen.town.focus.reader
me.hyliu.fluent_reader_lite
com.aljazeera.mobile
com.ft.news
de.marmaro.krt.ffupdater
myradio.radio.fmradio.liveradio.radiostation
com.google.earth
eu.kanade.tachiyomi.j2k
com.audials
com.microsoft.skydrive
com.mb.android.tg
com.melodis.midomiMusicIdentifier.freemium
com.foxnews.android
ch.threema.app
com.briarproject.briar.android
foundation.e.apps
com.valvesoftware.android.steam.friendsui
com.imback.yeetalk
so.onekey.app.wallet
com.xc3fff0e.xmanager
meditofoundation.medito
com.picol.client
com.streetwriters.notesnook
shanghai.panewsApp.com
org.coursera.android
com.positron_it.zlib
com.blizzard.messenger
com.javdb.javrocket
com.picacomic.fregata
com.fxl.chacha
me.proton.android.drive
com.lastpass.lpandroid
com.tradingview.tradingviewapp
com.deviantart.android.damobile
com.fusionmedia.investing
com.ewa.ewaapp
com.duolingo
com.hellotalk
io.github.huskydg.magisk
com.jsy.xpgbox
com.hostloc.app.hostloc
com.dena.pokota
com.vitorpamplona.amethyst
com.zhiliaoapp.musically
us.spotco.fennec_dos
com.fongmi.android.tv
com.pocketprep.android.itcybersecurity
com.cloudtv
com.glassdoor.app
com.indeed.android.jobsearch
com.linkedin.android
com.github.tvbox.osc.bh
com.example.douban
com.sipnetic.app
com.microsoft.rdc.androidx
org.zwanoo.android.speedtest
com.sonelli.juicessh
com.scmp.newspulse
org.lsposed.manager
mnn.Android
com.thomsonretuers.reuters
com.guardian
com.ttxapps.onesyncv2
org.fcitx.fcitx5.android.updater
com.tailscale.ipn
tw.nekomimi.nekogram
com.nexon.kartdrift
io.syncapps.lemmy_sync
com.seazon.feedme
com.readwise
de.spiritcroc.riotx
com.openai.chatgpt
io.changenow.changenow
com.poe.android
com.twingate
com.blinkslabs.blinkist.android
com.ichi2.anki
md.obsidian
com.musixmatch.android.lyrify
com.cyber.turbo
com.offsec.nethunter
me.ghui.v2er
com.samruston.twitter
org.adaway
org.swiftapps.swiftbackup
com.zerotier.one
com.quietmobile
com.instagram.barcelona
im.molly.app
com.rvx.android.youtube
com.deepl.mobiletranslator
com.qingsong.yingmi
com.lemurbrowser.exts
com.silverdev.dnartdroid
me.ash.reader
de.tutao.tutanota
dev.imranr.obtainium
com.getsomeheadspace.android
org.cromite.cromite
com.nutomic.syncthingandroid
com.bumble.app
com.cnn.mobile.android.phone
com.google.android.apps.authenticator2
com.microsoft.copilot
com.netflix.NGP.Storyteller
com.Slack
com.server.auditor.ssh.client
@@ -1,22 +1,22 @@
package com.v2ray.ang package com.v2ray.ang
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.multidex.MultiDexApplication 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.AppConfig.ANG_PACKAGE
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils
class AngApplication : MultiDexApplication() { class AngApplication : MultiDexApplication() {
companion object { companion object {
//const val PREF_LAST_VERSION = "pref_last_version"
lateinit var application: AngApplication lateinit var application: AngApplication
} }
/**
* Attaches the base context to the application.
* @param base The base context.
*/
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
application = this application = this
@@ -26,28 +26,18 @@ class AngApplication : MultiDexApplication() {
.setDefaultProcessName("${ANG_PACKAGE}:bg") .setDefaultProcessName("${ANG_PACKAGE}:bg")
.build() .build()
/**
* Initializes the application.
*/
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// LeakCanary.install(this)
// val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
// firstRun = defaultSharedPreferences.getInt(PREF_LAST_VERSION, 0) != BuildConfig.VERSION_CODE
// if (firstRun)
// defaultSharedPreferences.edit().putInt(PREF_LAST_VERSION, BuildConfig.VERSION_CODE).apply()
MMKV.initialize(this) MMKV.initialize(this)
Utils.setNightMode() SettingsManager.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
)!!
} }
@@ -5,6 +5,7 @@ object AppConfig {
/** The application's package name. */ /** The application's package name. */
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
const val TAG = BuildConfig.APPLICATION_ID
/** Directory names used in the app's file system. */ /** Directory names used in the app's file system. */
const val DIR_ASSETS = "assets" const val DIR_ASSETS = "assets"
@@ -12,7 +13,6 @@ object AppConfig {
/** Legacy configuration keys. */ /** Legacy configuration keys. */
const val ANG_CONFIG = "ang_config" const val ANG_CONFIG = "ang_config"
const val PREF_INAPP_BUY_IS_PREMIUM = "pref_inapp_buy_is_premium"
/** Preferences mapped to MMKV storage. */ /** Preferences mapped to MMKV storage. */
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled" const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
@@ -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"
@@ -41,15 +43,16 @@ object AppConfig {
const val PREF_SPEED_ENABLED = "pref_speed_enabled" const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove" const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate" const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
const val PREF_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display"
const val PREF_LANGUAGE = "pref_language" const val PREF_LANGUAGE = "pref_language"
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night" const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6" const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
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 +114,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"
@@ -168,7 +170,7 @@ object AppConfig {
const val DNS_QUAD9_DOMAIN = "dns.quad9.net" const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net" const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1") val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001") val DNS_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_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
@@ -11,7 +11,8 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
VLESS(5, AppConfig.VLESS), VLESS(5, AppConfig.VLESS),
TROJAN(6, AppConfig.TROJAN), TROJAN(6, AppConfig.TROJAN),
WIREGUARD(7, AppConfig.WIREGUARD), WIREGUARD(7, AppConfig.WIREGUARD),
// TUIC(8, AppConfig.TUIC),
// TUIC(8, AppConfig.TUIC),
HYSTERIA2(9, AppConfig.HYSTERIA2), HYSTERIA2(9, AppConfig.HYSTERIA2),
HTTP(10, AppConfig.HTTP); HTTP(10, AppConfig.HTTP);
@@ -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?,
)
}
@@ -8,6 +8,7 @@ enum class Language(val code: String) {
VIETNAMESE("vi"), VIETNAMESE("vi"),
RUSSIAN("ru"), RUSSIAN("ru"),
PERSIAN("fa"), PERSIAN("fa"),
ARABIC("ar"),
BANGLA("bn"), BANGLA("bn"),
BAKHTIARI("bqi-rIR"); BAKHTIARI("bqi-rIR");
@@ -5,10 +5,10 @@ enum class NetworkType(val type: String) {
KCP("kcp"), KCP("kcp"),
WS("ws"), WS("ws"),
HTTP_UPGRADE("httpupgrade"), HTTP_UPGRADE("httpupgrade"),
SPLIT_HTTP("splithttp"),
XHTTP("xhttp"), XHTTP("xhttp"),
HTTP("http"), HTTP("http"),
H2("h2"), H2("h2"),
//QUIC("quic"), //QUIC("quic"),
GRPC("grpc"); GRPC("grpc");
@@ -1,5 +1,7 @@
package com.v2ray.ang.dto 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_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_PROXY import com.v2ray.ang.AppConfig.TAG_PROXY
@@ -53,6 +55,8 @@ data class ProfileItem(
var portHopping: String? = null, var portHopping: String? = null,
var portHoppingInterval: String? = null, var portHoppingInterval: String? = null,
var pinSHA256: String? = null, var pinSHA256: String? = null,
var bandwidthDown: String? = null,
var bandwidthUp: String? = null,
) { ) {
companion object { companion object {
@@ -66,6 +70,9 @@ data class ProfileItem(
} }
fun getServerAddressAndPort(): String { fun getServerAddressAndPort(): String {
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
return "$LOOPBACK:$PORT_SOCKS"
}
return Utils.getIpv6Address(server) + ":" + serverPort return Utils.getIpv6Address(server) + ":" + serverPort
} }
@@ -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,
) )
@@ -8,8 +8,10 @@ import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.* import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.ServersBean
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.* import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.lang.reflect.Type import java.lang.reflect.Type
@@ -20,7 +22,7 @@ data class V2rayConfig(
var policy: PolicyBean?, var policy: PolicyBean?,
val inbounds: ArrayList<InboundBean>, val inbounds: ArrayList<InboundBean>,
var outbounds: ArrayList<OutboundBean>, var outbounds: ArrayList<OutboundBean>,
var dns: DnsBean, var dns: DnsBean? = null,
val routing: RoutingBean, val routing: RoutingBean,
val api: Any? = null, val api: Any? = null,
val transport: Any? = null, val transport: Any? = null,
@@ -202,7 +204,7 @@ data class V2rayConfig(
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,
@@ -257,7 +259,10 @@ data class V2rayConfig(
var header: HeaderBean = HeaderBean(), var header: HeaderBean = HeaderBean(),
var seed: String? = null var seed: String? = null
) { ) {
data class HeaderBean(var type: String = "none") data class HeaderBean(
var type: String = "none",
var domain: String? = null
)
} }
data class WsSettingsBean( data class WsSettingsBean(
@@ -357,7 +362,7 @@ data class V2rayConfig(
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) {
NetworkType.TCP.type -> { NetworkType.TCP.type -> {
val tcpSetting = TcpSettingsBean() val tcpSetting = TcpSettingsBean()
@@ -368,11 +373,11 @@ 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
} }
@@ -385,13 +390,18 @@ data class V2rayConfig(
} else { } else {
kcpsetting.seed = seed kcpsetting.seed = seed
} }
if (host.isNullOrEmpty()) {
kcpsetting.header.domain = null
} else {
kcpsetting.header.domain = host
}
kcpSettings = kcpsetting kcpSettings = kcpsetting
} }
NetworkType.WS.type -> { 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
} }
@@ -399,15 +409,15 @@ data class V2rayConfig(
NetworkType.HTTP_UPGRADE.type -> { 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
} }
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> { NetworkType.XHTTP.type -> {
val xhttpSetting = XhttpSettingsBean() val xhttpSetting = XhttpSettingsBean()
xhttpSetting.host = host.orEmpty() xhttpSetting.host = host.orEmpty()
sni = xhttpSetting.host sni = host
xhttpSetting.path = path ?: "/" xhttpSetting.path = path ?: "/"
xhttpSettings = xhttpSetting xhttpSettings = xhttpSetting
} }
@@ -416,7 +426,7 @@ data class V2rayConfig(
network = NetworkType.H2.type 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
} }
@@ -436,7 +446,7 @@ data class V2rayConfig(
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
} }
} }
@@ -453,15 +463,16 @@ data class V2rayConfig(
shortId: String?, shortId: String?,
spiderX: 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
@@ -475,9 +486,9 @@ 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? {
@@ -595,7 +606,7 @@ data class V2rayConfig(
) )
} }
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> { NetworkType.XHTTP.type -> {
val xhttpSettings = streamSettings?.xhttpSettings ?: return null val xhttpSettings = streamSettings?.xhttpSettings ?: return null
listOf( listOf(
"", "",
@@ -8,7 +8,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import com.v2ray.ang.AngApplication import com.v2ray.ang.AngApplication
import me.drakeet.support.toast.ToastCompat import es.dmoral.toasty.Toasty
import org.json.JSONObject import org.json.JSONObject
import java.io.Serializable import java.io.Serializable
import java.net.URI import java.net.URI
@@ -17,18 +17,75 @@ import java.net.URLConnection
val Context.v2RayApplication: AngApplication? val Context.v2RayApplication: AngApplication?
get() = applicationContext as? AngApplication get() = applicationContext as? AngApplication
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toast(message: Int) { fun Context.toast(message: Int) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() Toasty.normal(this, message).show()
} }
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toast(message: CharSequence) { fun Context.toast(message: CharSequence) {
ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() Toasty.normal(this, message).show()
} }
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastSuccess(message: Int) {
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastSuccess(message: CharSequence) {
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given resource ID.
*
* @param message The resource ID of the message to show.
*/
fun Context.toastError(message: Int) {
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Shows a toast message with the given text.
*
* @param message The text of the message to show.
*/
fun Context.toastError(message: CharSequence) {
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
}
/**
* Puts a key-value pair into the JSONObject.
*
* @param pair The key-value pair to put.
*/
fun JSONObject.putOpt(pair: Pair<String, Any?>) { fun JSONObject.putOpt(pair: Pair<String, Any?>) {
put(pair.first, pair.second) put(pair.first, pair.second)
} }
/**
* Puts multiple key-value pairs into the JSONObject.
*
* @param pairs The map of key-value pairs to put.
*/
fun JSONObject.putOpt(pairs: Map<String, Any?>) { fun JSONObject.putOpt(pairs: Map<String, Any?>) {
pairs.forEach { put(it.key, it.value) } pairs.forEach { put(it.key, it.value) }
} }
@@ -36,8 +93,18 @@ fun JSONObject.putOpt(pairs: Map<String, Any?>) {
const val THRESHOLD = 1000L const val THRESHOLD = 1000L
const val DIVISOR = 1024.0 const val DIVISOR = 1024.0
/**
* Converts a Long value to a speed string.
*
* @return The speed string.
*/
fun Long.toSpeedString(): String = this.toTrafficString() + "/s" fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
/**
* Converts a Long value to a traffic string.
*
* @return The traffic string.
*/
fun Long.toTrafficString(): String { fun Long.toTrafficString(): String {
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB") val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
var size = this.toDouble() var size = this.toDouble()
@@ -59,10 +126,27 @@ val URLConnection.responseLength: Long
val URI.idnHost: String val URI.idnHost: String
get() = host?.replace("[", "")?.replace("]", "").orEmpty() get() = host?.replace("[", "")?.replace("]", "").orEmpty()
fun String.removeWhiteSpace(): String = replace("\\s+".toRegex(), "") /**
* Removes all whitespace from the string.
*
* @return The string without whitespace.
*/
fun String?.removeWhiteSpace(): String? = this?.replace(" ", "")
/**
* Converts the string to a Long value, or returns 0 if the conversion fails.
*
* @return The Long value.
*/
fun String.toLongEx(): Long = toLongOrNull() ?: 0 fun String.toLongEx(): Long = toLongOrNull() ?: 0
/**
* Listens for package changes and executes a callback when a change occurs.
*
* @param onetime Whether to unregister the receiver after the first callback.
* @param callback The callback to execute when a package change occurs.
* @return The BroadcastReceiver that was registered.
*/
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
object : BroadcastReceiver() { object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@@ -85,14 +169,31 @@ fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Uni
} }
} }
/**
* Retrieves a serializable object from the Bundle.
*
* @param key The key of the serializable object.
* @return The serializable object, or null if not found.
*/
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when { inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java) Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as? T else -> @Suppress("DEPRECATION") getSerializable(key) as? T
} }
/**
* Retrieves a serializable object from the Intent.
*
* @param key The key of the serializable object.
* @return The serializable object, or null if not found.
*/
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when { inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java) Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
} }
inline fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty()) /**
* Checks if the CharSequence is not null and not empty.
*
* @return True if the CharSequence is not null and not empty, false otherwise.
*/
fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
@@ -6,6 +6,12 @@ import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
object CustomFmt : FmtBase() { object CustomFmt : FmtBase() {
/**
* Parses a JSON string into a ProfileItem object.
*
* @param str the JSON string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.CUSTOM) val config = ProfileItem.create(EConfigType.CUSTOM)
@@ -1,5 +1,6 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.NetworkType 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
@@ -7,6 +8,14 @@ import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
open class FmtBase { open class FmtBase {
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @param userInfo the user information to include in the URI
* @param dicQuery the query parameters to include in the URI
* @return the converted URI string
*/
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String { fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap<String, String>?): String {
val query = if (dicQuery != null) val query = if (dicQuery != null)
("?" + dicQuery.toList().joinToString( ("?" + dicQuery.toList().joinToString(
@@ -24,15 +33,26 @@ open class FmtBase {
return "${url}${query}#${Utils.urlEncode(config.remarks)}" return "${url}${query}#${Utils.urlEncode(config.remarks)}"
} }
/**
* Extracts query parameters from a URI.
*
* @param uri the URI to extract query parameters from
* @return a map of query parameters
*/
fun getQueryParam(uri: URI): Map<String, String> { fun getQueryParam(uri: URI): Map<String, String> {
return uri.rawQuery.split("&") return uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } .associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
} }
/**
* Populates a ProfileItem object with values from query parameters.
*
* @param config the ProfileItem object to populate
* @param queryParam the query parameters to use for populating the ProfileItem
* @param allowInsecure whether to allow insecure connections
*/
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) { fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
config.network = queryParam["type"] ?: NetworkType.TCP.type config.network = queryParam["type"] ?: NetworkType.TCP.type
//TODO
if (config.network == NetworkType.SPLIT_HTTP.type) config.network = NetworkType.XHTTP.type
config.headerType = queryParam["headerType"] config.headerType = queryParam["headerType"]
config.host = queryParam["host"] config.host = queryParam["host"]
config.path = queryParam["path"] config.path = queryParam["path"]
@@ -47,6 +67,9 @@ open class FmtBase {
config.xhttpExtra = queryParam["extra"] config.xhttpExtra = queryParam["extra"]
config.security = queryParam["security"] config.security = queryParam["security"]
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
config.security = null
}
config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) { config.insecure = if (queryParam["allowInsecure"].isNullOrEmpty()) {
allowInsecure allowInsecure
} else { } else {
@@ -61,6 +84,12 @@ open class FmtBase {
config.flow = queryParam["flow"] config.flow = queryParam["flow"]
} }
/**
* Creates a map of query parameters from a ProfileItem object.
*
* @param config the ProfileItem object to create query parameters from
* @return a map of query parameters
*/
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()
@@ -91,7 +120,7 @@ open class FmtBase {
config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() } config.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
} }
NetworkType.SPLIT_HTTP, NetworkType.XHTTP -> { NetworkType.XHTTP -> {
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() }
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() } config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
@@ -119,5 +148,4 @@ open class FmtBase {
return dicQuery return dicQuery
} }
} }
@@ -4,9 +4,14 @@ import com.v2ray.ang.dto.EConfigType
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.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import kotlin.text.orEmpty
object HttpFmt : FmtBase() { object HttpFmt : FmtBase() {
/**
* Converts a ProfileItem object to an OutboundBean object.
*
* @param profileItem the ProfileItem object to convert
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HTTP) val outboundBean = OutboundBean.create(EConfigType.HTTP)
@@ -23,6 +28,4 @@ object HttpFmt : FmtBase() {
return outboundBean return outboundBean
} }
} }
@@ -13,6 +13,12 @@ import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
object Hysteria2Fmt : FmtBase() { object Hysteria2Fmt : FmtBase() {
/**
* Parses a Hysteria2 URI string into a ProfileItem object.
*
* @param str the Hysteria2 URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.HYSTERIA2) val config = ProfileItem.create(EConfigType.HYSTERIA2)
@@ -45,6 +51,12 @@ object Hysteria2Fmt : FmtBase() {
return config return config
} }
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String { fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>() val dicQuery = HashMap<String, String>()
@@ -67,6 +79,13 @@ object Hysteria2Fmt : FmtBase() {
return toUri(config, config.password, dicQuery) return toUri(config, config.password, dicQuery)
} }
/**
* Converts a ProfileItem object to a Hysteria2Bean object.
*
* @param config the ProfileItem object to convert
* @param socksPort the port number for the socks5 proxy
* @return the converted Hysteria2Bean object, or null if conversion fails
*/
fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? { fun toNativeConfig(config: ProfileItem, socksPort: Int): Hysteria2Bean? {
val obfs = if (config.obfsPassword.isNullOrEmpty()) null else val obfs = if (config.obfsPassword.isNullOrEmpty()) null else
@@ -85,6 +104,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 +121,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}",
), ),
@@ -111,10 +137,14 @@ object Hysteria2Fmt : FmtBase() {
return bean return bean
} }
/**
* Converts a ProfileItem object to an OutboundBean object.
*
* @param profileItem the ProfileItem object to convert
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2) val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
return outboundBean return outboundBean
} }
} }
@@ -1,5 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import android.util.Log
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.NetworkType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
@@ -9,10 +11,22 @@ import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
object ShadowsocksFmt : FmtBase() { object ShadowsocksFmt : FmtBase() {
/**
* Parses a Shadowsocks URI string into a ProfileItem object.
*
* @param str the Shadowsocks URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem? {
return parseSip002(str) ?: parseLegacy(str) return parseSip002(str) ?: parseLegacy(str)
} }
/**
* Parses a SIP002 Shadowsocks URI string into a ProfileItem object.
*
* @param str the SIP002 Shadowsocks URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseSip002(str: String): ProfileItem? { fun parseSip002(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS) val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
@@ -37,18 +51,30 @@ object ShadowsocksFmt : FmtBase() {
if (!uri.rawQuery.isNullOrEmpty()) { if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri) val queryParam = getQueryParam(uri)
if (queryParam["plugin"]?.contains("obfs=http") == true) {
if (queryParam["plugin"] == "obfs-local" && queryParam["obfs"] == "http") { 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.network = NetworkType.TCP.type
config.headerType = "http" config.headerType = "http"
config.host = queryParam["obfs-host"] config.host = queryPairs["obfs-host"]
config.path = queryParam["path"] config.path = queryPairs["path"]
} }
} }
return config return config
} }
/**
* Parses a legacy Shadowsocks URI string into a ProfileItem object.
*
* @param str the legacy Shadowsocks URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseLegacy(str: String): ProfileItem? { fun parseLegacy(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SHADOWSOCKS) val config = ProfileItem.create(EConfigType.SHADOWSOCKS)
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "") var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
@@ -58,7 +84,7 @@ object ShadowsocksFmt : FmtBase() {
config.remarks = config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length)) Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
} }
result = result.substring(0, indexSplit) result = result.substring(0, indexSplit)
@@ -86,12 +112,24 @@ object ShadowsocksFmt : FmtBase() {
return config return config
} }
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String { fun toUri(config: ProfileItem): String {
val pw = "${config.method}:${config.password}" val pw = "${config.method}:${config.password}"
return toUri(config, Utils.encode(pw), null) return toUri(config, Utils.encode(pw), null)
} }
/**
* Converts a ProfileItem object to an OutboundBean object.
*
* @param profileItem the ProfileItem object to convert
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS) val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
@@ -102,8 +140,30 @@ object ShadowsocksFmt : FmtBase() {
server.method = profileItem.method 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 return outboundBean
} }
} }
@@ -7,9 +7,14 @@ import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
import kotlin.text.orEmpty
object SocksFmt : FmtBase() { object SocksFmt : FmtBase() {
/**
* Parses a Socks URI string into a ProfileItem object.
*
* @param str the Socks URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.SOCKS) val config = ProfileItem.create(EConfigType.SOCKS)
@@ -32,6 +37,12 @@ object SocksFmt : FmtBase() {
return config return config
} }
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String { fun toUri(config: ProfileItem): String {
val pw = val pw =
if (config.username.isNotNullEmpty()) if (config.username.isNotNullEmpty())
@@ -42,6 +53,12 @@ object SocksFmt : FmtBase() {
return toUri(config, Utils.encode(pw), null) return toUri(config, Utils.encode(pw), null)
} }
/**
* Converts a ProfileItem object to an OutboundBean object.
*
* @param profileItem the ProfileItem object to convert
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SOCKS) val outboundBean = OutboundBean.create(EConfigType.SOCKS)
@@ -58,5 +75,4 @@ object SocksFmt : FmtBase() {
return outboundBean return outboundBean
} }
} }
@@ -9,9 +9,14 @@ import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
import kotlin.text.orEmpty
object TrojanFmt : FmtBase() { object TrojanFmt : FmtBase() {
/**
* Parses a Trojan URI string into a ProfileItem object.
*
* @param str the Trojan URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.TROJAN) val config = ProfileItem.create(EConfigType.TROJAN)
@@ -36,12 +41,24 @@ object TrojanFmt : FmtBase() {
return config return config
} }
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String { fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config) val dicQuery = getQueryDic(config)
return toUri(config, config.password, dicQuery) return toUri(config, config.password, dicQuery)
} }
/**
* Converts a ProfileItem object to an OutboundBean object.
*
* @param profileItem the ProfileItem object to convert
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.TROJAN) val outboundBean = OutboundBean.create(EConfigType.TROJAN)
@@ -52,7 +69,7 @@ object TrojanFmt : FmtBase() {
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,
@@ -68,7 +85,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,
@@ -12,6 +12,12 @@ import java.net.URI
object VlessFmt : FmtBase() { object VlessFmt : FmtBase() {
/**
* Parses a Vless URI string into a ProfileItem object.
*
* @param str the Vless URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VLESS) val config = ProfileItem.create(EConfigType.VLESS)
@@ -26,11 +32,17 @@ object VlessFmt : FmtBase() {
config.password = uri.userInfo config.password = uri.userInfo
config.method = queryParam["encryption"] ?: "none" config.method = queryParam["encryption"] ?: "none"
getItemFormQuery(config, queryParam, allowInsecure) getItemFormQuery(config, queryParam, allowInsecure)
return config return config
} }
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String { fun toUri(config: ProfileItem): String {
val dicQuery = getQueryDic(config) val dicQuery = getQueryDic(config)
dicQuery["encryption"] = config.method ?: "none" dicQuery["encryption"] = config.method ?: "none"
@@ -38,7 +50,12 @@ object VlessFmt : FmtBase() {
return toUri(config, config.password, dicQuery) return toUri(config, config.password, dicQuery)
} }
/**
* Converts a ProfileItem object to an OutboundBean object.
*
* @param profileItem the ProfileItem object to convert
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VLESS) val outboundBean = OutboundBean.create(EConfigType.VLESS)
@@ -50,7 +67,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,
@@ -68,7 +85,7 @@ object VlessFmt : 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,
@@ -78,6 +95,4 @@ object VlessFmt : FmtBase() {
return outboundBean return outboundBean
} }
} }
@@ -14,9 +14,14 @@ import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil 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
import kotlin.text.orEmpty
object VmessFmt : FmtBase() { object VmessFmt : FmtBase() {
/**
* Parses a Vmess string into a ProfileItem object.
*
* @param str the Vmess string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) { if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str) return parseVmessStd(str)
@@ -28,7 +33,7 @@ object VmessFmt : FmtBase() {
var result = str.replace(EConfigType.VMESS.protocolScheme, "") var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result) result = Utils.decode(result)
if (TextUtils.isEmpty(result)) { if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed") Log.w(AppConfig.TAG, "Toast decoding failed")
return null return null
} }
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java) val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
@@ -38,7 +43,7 @@ object VmessFmt : FmtBase() {
|| TextUtils.isEmpty(vmessQRCode.id) || TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net) || TextUtils.isEmpty(vmessQRCode.net)
) { ) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol") Log.w(AppConfig.TAG, "Toast incorrect protocol")
return null return null
} }
@@ -68,6 +73,7 @@ object VmessFmt : FmtBase() {
config.serviceName = vmessQRCode.path config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host config.authority = vmessQRCode.host
} }
else -> {} else -> {}
} }
@@ -80,6 +86,12 @@ object VmessFmt : FmtBase() {
return config return config
} }
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String { fun toUri(config: ProfileItem): String {
val vmessQRCode = VmessQRCode() val vmessQRCode = VmessQRCode()
@@ -108,6 +120,7 @@ object VmessFmt : FmtBase() {
vmessQRCode.path = config.serviceName.orEmpty() vmessQRCode.path = config.serviceName.orEmpty()
vmessQRCode.host = config.authority.orEmpty() vmessQRCode.host = config.authority.orEmpty()
} }
else -> {} else -> {}
} }
@@ -123,6 +136,12 @@ object VmessFmt : FmtBase() {
return Utils.encode(json) return Utils.encode(json)
} }
/**
* Parses a standard Vmess URI string into a ProfileItem object.
*
* @param str the standard Vmess URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseVmessStd(str: String): ProfileItem? { fun parseVmessStd(str: String): ProfileItem? {
val 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)
@@ -142,7 +161,12 @@ object VmessFmt : FmtBase() {
return config return config
} }
/**
* Converts a ProfileItem object to an OutboundBean object.
*
* @param profileItem the ProfileItem object to convert
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VMESS) val outboundBean = OutboundBean.create(EConfigType.VMESS)
@@ -153,7 +177,7 @@ object VmessFmt : FmtBase() {
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,
@@ -169,7 +193,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,
@@ -6,11 +6,17 @@ import com.v2ray.ang.dto.EConfigType
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
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
import kotlin.text.orEmpty
object WireguardFmt : FmtBase() { object WireguardFmt : FmtBase() {
/**
* Parses a URI string into a ProfileItem object.
*
* @param str the URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD) val config = ProfileItem.create(EConfigType.WIREGUARD)
@@ -32,6 +38,12 @@ object WireguardFmt : FmtBase() {
return config return config
} }
/**
* Parses a Wireguard configuration file string into a ProfileItem object.
*
* @param str the Wireguard configuration file string to parse
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseWireguardConfFile(str: String): ProfileItem? { fun parseWireguardConfFile(str: String): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD) val config = ProfileItem.create(EConfigType.WIREGUARD)
@@ -86,6 +98,12 @@ object WireguardFmt : FmtBase() {
return config return config
} }
/**
* Converts a ProfileItem object to an OutboundBean object.
*
* @param profileItem the ProfileItem object to convert
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? { fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD) val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
@@ -104,19 +122,25 @@ object WireguardFmt : FmtBase() {
return outboundBean return outboundBean
} }
/**
* Converts a ProfileItem object to a URI string.
*
* @param config the ProfileItem object to convert
* @return the converted URI string
*/
fun toUri(config: ProfileItem): String { fun toUri(config: ProfileItem): String {
val dicQuery = HashMap<String, String>() val dicQuery = HashMap<String, String>()
dicQuery["publickey"] = config.publicKey.orEmpty() dicQuery["publickey"] = config.publicKey.orEmpty()
if (config.reserved != null) { if (config.reserved != null) {
dicQuery["reserved"] = Utils.removeWhiteSpace(config.reserved).orEmpty() dicQuery["reserved"] = config.reserved.removeWhiteSpace().orEmpty()
} }
dicQuery["address"] = Utils.removeWhiteSpace(config.localAddress).orEmpty() dicQuery["address"] = config.localAddress.removeWhiteSpace().orEmpty()
if (config.mtu != null) { if (config.mtu != null) {
dicQuery["mtu"] = config.mtu.toString() dicQuery["mtu"] = config.mtu.toString()
} }
if (config.preSharedKey != null) { if (config.preSharedKey != null) {
dicQuery["presharedkey"] = Utils.removeWhiteSpace(config.preSharedKey).orEmpty() dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty()
} }
return toUri(config, config.secretKey, dicQuery) return toUri(config, config.secretKey, dicQuery)
@@ -16,14 +16,314 @@ import com.v2ray.ang.fmt.TrojanFmt
import com.v2ray.ang.fmt.VlessFmt import com.v2ray.ang.fmt.VlessFmt
import com.v2ray.ang.fmt.VmessFmt import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
object AngConfigManager { object AngConfigManager {
/** /**
* parse config form qrcode or... * Shares the configuration to the clipboard.
*
* @param context The context.
* @param guid The GUID of the configuration.
* @return The result code.
*/
fun share2Clipboard(context: Context, guid: String): Int {
try {
val conf = shareConfig(guid)
if (TextUtils.isEmpty(conf)) {
return -1
}
Utils.setClipboard(context, conf)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share config to clipboard", e)
return -1
}
return 0
}
/**
* Shares non-custom configurations to the clipboard.
*
* @param context The context.
* @param serverList The list of server GUIDs.
* @return The number of configurations shared.
*/
fun shareNonCustomConfigsToClipboard(context: Context, serverList: List<String>): Int {
try {
val sb = StringBuilder()
for (guid in serverList) {
val url = shareConfig(guid)
if (TextUtils.isEmpty(url)) {
continue
}
sb.append(url)
sb.appendLine()
}
if (sb.count() > 0) {
Utils.setClipboard(context, sb.toString())
}
return sb.lines().count()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
return -1
}
}
/**
* Shares the configuration as a QR code.
*
* @param guid The GUID of the configuration.
* @return The QR code bitmap.
*/
fun share2QRCode(guid: String): Bitmap? {
try {
val conf = shareConfig(guid)
if (TextUtils.isEmpty(conf)) {
return null
}
return QRCodeDecoder.createQRCode(conf)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share config as QR code", e)
return null
}
}
/**
* Shares the full content of the configuration to the clipboard.
*
* @param context The context.
* @param guid The GUID of the configuration.
* @return The result code.
*/
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
try {
if (guid == null) return -1
val result = V2rayConfigManager.getV2rayConfig(context, guid)
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)
} else {
return -1
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
return -1
}
return 0
}
/**
* Shares the configuration.
*
* @param guid The GUID of the configuration.
* @return The configuration string.
*/
private fun shareConfig(guid: String): String {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
return config.configType.protocolScheme + when (config.configType) {
EConfigType.VMESS -> VmessFmt.toUri(config)
EConfigType.CUSTOM -> ""
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
EConfigType.SOCKS -> SocksFmt.toUri(config)
EConfigType.HTTP -> ""
EConfigType.VLESS -> VlessFmt.toUri(config)
EConfigType.TROJAN -> TrojanFmt.toUri(config)
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
return ""
}
}
/**
* Imports a batch of configurations.
*
* @param server The server string.
* @param subid The subscription ID.
* @param append Whether to append the configurations.
* @return A pair containing the number of configurations and subscriptions imported.
*/
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid)
}
var countSub = parseBatchSubscription(server)
if (countSub <= 0) {
countSub = parseBatchSubscription(Utils.decode(server))
}
if (countSub > 0) {
updateConfigViaSubAll()
}
return count to countSub
}
/**
* Parses a batch of subscriptions.
*
* @param servers The servers string.
* @return The number of subscriptions parsed.
*/
private fun parseBatchSubscription(servers: String?): Int {
try {
if (servers == null) {
return 0
}
var count = 0
servers.lines()
.distinct()
.forEach { str ->
if (Utils.isValidSubUrl(str)) {
count += importUrlAsSubscription(str)
}
}
return count
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse batch subscription", e)
}
return 0
}
/**
* Parses a batch of configurations.
*
* @param servers The servers string.
* @param subid The subscription ID.
* @param append Whether to append the configurations.
* @return The number of configurations parsed.
*/
private fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
try {
if (servers == null) {
return 0
}
val removedSelectedServer =
if (!TextUtils.isEmpty(subid) && !append) {
MmkvManager.decodeServerConfig(
MmkvManager.getSelectServer().orEmpty()
)?.let {
if (it.subscriptionId == subid) {
return@let it
}
return@let null
}
} else {
null
}
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
val subItem = MmkvManager.decodeSubscription(subid)
var count = 0
servers.lines()
.distinct()
.reversed()
.forEach {
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
if (resId == 0) {
count++
}
}
return count
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse batch config", e)
}
return 0
}
/**
* Parses a custom configuration server.
*
* @param server The server string.
* @param subid The subscription ID.
* @return The number of configurations parsed.
*/
private fun parseCustomConfigServer(server: String?, subid: String): Int {
if (server == null) {
return 0
}
if (server.contains("inbounds")
&& server.contains("outbounds")
&& server.contains("routing")
) {
try {
val serverList: Array<Any> =
JsonUtil.fromJson(server, Array<Any>::class.java)
if (serverList.isNotEmpty()) {
var count = 0
for (srv in serverList.reversed()) {
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
count += 1
}
return count
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
}
try {
// For compatibility
val config = CustomFmt.parse(server) ?: return 0
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
}
return 0
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
try {
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
}
return 0
} else {
return 0
}
}
/**
* Parses the configuration from a QR code or string.
*
* @param str The configuration string.
* @param subid The subscription ID.
* @param subItem The subscription item.
* @param removedSelectedServer The removed selected server.
* @return The result code.
*/ */
private fun parseConfig( private fun parseConfig(
str: String?, str: String?,
@@ -72,249 +372,17 @@ object AngConfigManager {
MmkvManager.setSelectServer(guid) MmkvManager.setSelectServer(guid)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to parse config", e)
return -1 return -1
} }
return 0 return 0
} }
/** /**
* share config * Updates the configuration via all subscriptions.
*
* @return The number of configurations updated.
*/ */
private fun shareConfig(guid: String): String {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
return config.configType.protocolScheme + when (config.configType) {
EConfigType.VMESS -> VmessFmt.toUri(config)
EConfigType.CUSTOM -> ""
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
EConfigType.SOCKS -> SocksFmt.toUri(config)
EConfigType.HTTP -> ""
EConfigType.VLESS -> VlessFmt.toUri(config)
EConfigType.TROJAN -> TrojanFmt.toUri(config)
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
}
} catch (e: Exception) {
e.printStackTrace()
return ""
}
}
/**
* share2Clipboard
*/
fun share2Clipboard(context: Context, guid: String): Int {
try {
val conf = shareConfig(guid)
if (TextUtils.isEmpty(conf)) {
return -1
}
Utils.setClipboard(context, conf)
} catch (e: Exception) {
e.printStackTrace()
return -1
}
return 0
}
/**
* share2Clipboard
*/
fun shareNonCustomConfigsToClipboard(context: Context, serverList: List<String>): Int {
try {
val sb = StringBuilder()
for (guid in serverList) {
val url = shareConfig(guid)
if (TextUtils.isEmpty(url)) {
continue
}
sb.append(url)
sb.appendLine()
}
if (sb.count() > 0) {
Utils.setClipboard(context, sb.toString())
}
} catch (e: Exception) {
e.printStackTrace()
return -1
}
return 0
}
/**
* share2QRCode
*/
fun share2QRCode(guid: String): Bitmap? {
try {
val conf = shareConfig(guid)
if (TextUtils.isEmpty(conf)) {
return null
}
return QRCodeDecoder.createQRCode(conf)
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
/**
* shareFullContent2Clipboard
*/
fun shareFullContent2Clipboard(context: Context, guid: String?): Int {
try {
if (guid == null) return -1
val result = V2rayConfigManager.getV2rayConfig(context, guid)
if (result.status) {
Utils.setClipboard(context, result.content)
} else {
return -1
}
} catch (e: Exception) {
e.printStackTrace()
return -1
}
return 0
}
fun importBatchConfig(server: String?, subid: String, append: Boolean): Pair<Int, Int> {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid)
}
var countSub = parseBatchSubscription(server)
if (countSub <= 0) {
countSub = parseBatchSubscription(Utils.decode(server))
}
if (countSub > 0) {
updateConfigViaSubAll()
}
return count to countSub
}
fun parseBatchSubscription(servers: String?): Int {
try {
if (servers == null) {
return 0
}
var count = 0
servers.lines()
.distinct()
.forEach { str ->
if (Utils.isValidSubUrl(str)) {
count += importUrlAsSubscription(str)
}
}
return count
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
try {
if (servers == null) {
return 0
}
val removedSelectedServer =
if (!TextUtils.isEmpty(subid) && !append) {
MmkvManager.decodeServerConfig(
MmkvManager.getSelectServer().orEmpty()
)?.let {
if (it.subscriptionId == subid) {
return@let it
}
return@let null
}
} else {
null
}
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
val subItem = MmkvManager.decodeSubscription(subid)
var count = 0
servers.lines()
.distinct()
.reversed()
.forEach {
val resId = parseConfig(it, subid, subItem, removedSelectedServer)
if (resId == 0) {
count++
}
}
return count
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
fun parseCustomConfigServer(server: String?, subid: String): Int {
if (server == null) {
return 0
}
if (server.contains("inbounds")
&& server.contains("outbounds")
&& server.contains("routing")
) {
try {
val serverList: Array<Any> =
JsonUtil.fromJson(server, Array<Any>::class.java)
if (serverList.isNotEmpty()) {
var count = 0
for (srv in serverList.reversed()) {
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv)?:"")
count += 1
}
return count
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
// For compatibility
val config = CustomFmt.parse(server) ?: return 0
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
e.printStackTrace()
}
return 0
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
try {
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
return 1
} catch (e: Exception) {
e.printStackTrace()
}
return 0
} else {
return 0
}
}
fun updateConfigViaSubAll(): Int { fun updateConfigViaSubAll(): Int {
var count = 0 var count = 0
try { try {
@@ -322,12 +390,18 @@ object AngConfigManager {
count += updateConfigViaSub(it) count += updateConfigViaSub(it)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
return 0 return 0
} }
return count return count
} }
/**
* Updates the configuration via a subscription.
*
* @param it The subscription item.
* @return The number of configurations updated.
*/
fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int { fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
try { try {
if (TextUtils.isEmpty(it.first) if (TextUtils.isEmpty(it.first)
@@ -339,25 +413,24 @@ object AngConfigManager {
if (!it.second.enabled) { if (!it.second.enabled) {
return 0 return 0
} }
val url = Utils.idnToASCII(it.second.url) val url = HttpUtil.idnToASCII(it.second.url)
if (!Utils.isValidUrl(url)) { if (!Utils.isValidUrl(url)) {
return 0 return 0
} }
Log.d(AppConfig.ANG_PACKAGE, url) Log.i(AppConfig.TAG, url)
var configText = try { var configText = try {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort) HttpUtil.getUrlContentWithUserAgent(url, 15000, httpPort)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error, try……") Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
//e.printStackTrace()
"" ""
} }
if (configText.isEmpty()) { if (configText.isEmpty()) {
configText = try { configText = try {
Utils.getUrlContentWithCustomUserAgent(url) HttpUtil.getUrlContentWithUserAgent(url)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to get URL content with user agent", e)
"" ""
} }
} }
@@ -366,11 +439,19 @@ object AngConfigManager {
} }
return parseConfigViaSub(configText, it.first, false) return parseConfigViaSub(configText, it.first, false)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
return 0 return 0
} }
} }
/**
* Parses the configuration via a subscription.
*
* @param server The server string.
* @param subid The subscription ID.
* @param append Whether to append the configurations.
* @return The number of configurations parsed.
*/
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int { private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
var count = parseBatchConfig(Utils.decode(server), subid, append) var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) { if (count <= 0) {
@@ -382,6 +463,12 @@ object AngConfigManager {
return count return count
} }
/**
* Imports a URL as a subscription.
*
* @param url The URL.
* @return The number of subscriptions imported.
*/
private fun importUrlAsSubscription(url: String): Int { private fun importUrlAsSubscription(url: String): Int {
val subscriptions = MmkvManager.decodeSubscriptions() val subscriptions = MmkvManager.decodeSubscriptions()
subscriptions.forEach { subscriptions.forEach {
@@ -3,25 +3,29 @@ package com.v2ray.ang.handler
import android.util.Log import android.util.Log
import com.tencent.mmkv.MMKV 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.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType 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.extension.removeWhiteSpace
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object MigrateManager { object MigrateManager {
private const val ID_SERVER_CONFIG = "SERVER_CONFIG" private const val ID_SERVER_CONFIG = "SERVER_CONFIG"
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) } private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
/**
* Migrates server configurations to profile items.
*
* @return True if migration was successful, false otherwise.
*/
fun migrateServerConfig2Profile(): Boolean { fun migrateServerConfig2Profile(): Boolean {
if (serverStorage.count().toInt() == 0) { if (serverStorage.count().toInt() == 0) {
return false return false
} }
val serverList = serverStorage.allKeys() ?: return false val serverList = serverStorage.allKeys() ?: return false
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-" + serverList.count()) Log.i(AppConfig.TAG, "migrateServerConfig2Profile-" + serverList.count())
for (guid in serverList) { for (guid in serverList) {
var configOld = decodeServerConfigOld(guid) ?: continue var configOld = decodeServerConfigOld(guid) ?: continue
@@ -38,12 +42,18 @@ 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.i(AppConfig.TAG, "migrateServerConfig2Profile-" + config.remarks)
} }
Log.d(ANG_PACKAGE, "migrateServerConfig2Profile-end") Log.i(AppConfig.TAG, "migrateServerConfig2Profile-end")
return true return true
} }
/**
* Migrates a server configuration to a profile item.
*
* @param configOld The old server configuration.
* @return The profile item.
*/
private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? { private fun migrateServerConfig2ProfileSub(configOld: ServerConfig): ProfileItem? {
return when (configOld.getProxyOutbound()?.protocol) { return when (configOld.getProxyOutbound()?.protocol) {
EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld) EConfigType.VMESS.name.lowercase() -> migrate2ProfileCommon(configOld)
@@ -62,6 +72,12 @@ object MigrateManager {
} }
} }
/**
* Migrates a common server configuration to a profile item.
*
* @param configOld The old server configuration.
* @return The profile item.
*/
private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? { private fun migrate2ProfileCommon(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(configOld.configType) val config = ProfileItem.create(configOld.configType)
@@ -92,7 +108,7 @@ object MigrateManager {
config.insecure = tlsSettings?.allowInsecure config.insecure = tlsSettings?.allowInsecure
config.sni = tlsSettings?.serverName config.sni = tlsSettings?.serverName
config.fingerPrint = tlsSettings?.fingerprint config.fingerPrint = tlsSettings?.fingerprint
config.alpn = Utils.removeWhiteSpace(tlsSettings?.alpn?.joinToString(",")).toString() config.alpn = tlsSettings?.alpn?.joinToString(",").removeWhiteSpace().toString()
config.publicKey = tlsSettings?.publicKey config.publicKey = tlsSettings?.publicKey
config.shortId = tlsSettings?.shortId config.shortId = tlsSettings?.shortId
@@ -101,6 +117,12 @@ object MigrateManager {
return config return config
} }
/**
* Migrates a SOCKS server configuration to a profile item.
*
* @param configOld The old server configuration.
* @return The profile item.
*/
private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? { private fun migrate2ProfileSocks(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.SOCKS) val config = ProfileItem.create(EConfigType.SOCKS)
@@ -114,6 +136,12 @@ object MigrateManager {
return config return config
} }
/**
* Migrates an HTTP server configuration to a profile item.
*
* @param configOld The old server configuration.
* @return The profile item.
*/
private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? { private fun migrate2ProfileHttp(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.HTTP) val config = ProfileItem.create(EConfigType.HTTP)
@@ -127,6 +155,12 @@ object MigrateManager {
return config return config
} }
/**
* Migrates a WireGuard server configuration to a profile item.
*
* @param configOld The old server configuration.
* @return The profile item.
*/
private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? { private fun migrate2ProfileWireguard(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.WIREGUARD) val config = ProfileItem.create(EConfigType.WIREGUARD)
@@ -137,14 +171,20 @@ object MigrateManager {
outbound.settings?.let { wireguard -> outbound.settings?.let { wireguard ->
config.secretKey = wireguard.secretKey config.secretKey = wireguard.secretKey
config.localAddress = Utils.removeWhiteSpace((wireguard.address as List<*>).joinToString(",")).toString() config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().toString()
config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey config.publicKey = wireguard.peers?.getOrNull(0)?.publicKey
config.mtu = wireguard.mtu config.mtu = wireguard.mtu
config.reserved = Utils.removeWhiteSpace(wireguard.reserved?.joinToString(",")).toString() config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString()
} }
return config return config
} }
/**
* Migrates a Hysteria2 server configuration to a profile item.
*
* @param configOld The old server configuration.
* @return The profile item.
*/
private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? { private fun migrate2ProfileHysteria2(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.HYSTERIA2) val config = ProfileItem.create(EConfigType.HYSTERIA2)
@@ -158,7 +198,7 @@ object MigrateManager {
outbound.streamSettings?.tlsSettings?.let { tlsSetting -> outbound.streamSettings?.tlsSettings?.let { tlsSetting ->
config.insecure = tlsSetting.allowInsecure config.insecure = tlsSetting.allowInsecure
config.sni = tlsSetting.serverName config.sni = tlsSetting.serverName
config.alpn = Utils.removeWhiteSpace(tlsSetting.alpn?.joinToString(",")).orEmpty() config.alpn = tlsSetting.alpn?.joinToString(",").removeWhiteSpace().orEmpty()
} }
config.obfsPassword = outbound.settings?.obfsPassword config.obfsPassword = outbound.settings?.obfsPassword
@@ -166,6 +206,12 @@ object MigrateManager {
return config return config
} }
/**
* Migrates a custom server configuration to a profile item.
*
* @param configOld The old server configuration.
* @return The profile item.
*/
private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? { private fun migrate2ProfileCustom(configOld: ServerConfig): ProfileItem? {
val config = ProfileItem.create(EConfigType.CUSTOM) val config = ProfileItem.create(EConfigType.CUSTOM)
@@ -177,7 +223,12 @@ object MigrateManager {
return config return config
} }
/**
* Decodes the old server configuration.
*
* @param guid The server GUID.
* @return The old server configuration.
*/
private fun decodeServerConfigOld(guid: String): ServerConfig? { private fun decodeServerConfigOld(guid: String): ServerConfig? {
if (guid.isBlank()) { if (guid.isBlank()) {
return null return null
@@ -1,6 +1,5 @@
package com.v2ray.ang.handler package com.v2ray.ang.handler
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
@@ -41,18 +40,38 @@ object MmkvManager {
//region Server //region Server
/**
* Gets the selected server GUID.
*
* @return The selected server GUID.
*/
fun getSelectServer(): String? { fun getSelectServer(): String? {
return mainStorage.decodeString(KEY_SELECTED_SERVER) return mainStorage.decodeString(KEY_SELECTED_SERVER)
} }
/**
* Sets the selected server GUID.
*
* @param guid The server GUID.
*/
fun setSelectServer(guid: String) { fun setSelectServer(guid: String) {
mainStorage.encode(KEY_SELECTED_SERVER, guid) mainStorage.encode(KEY_SELECTED_SERVER, guid)
} }
/**
* Encodes the server list.
*
* @param serverList The list of server GUIDs.
*/
fun encodeServerList(serverList: MutableList<String>) { fun encodeServerList(serverList: MutableList<String>) {
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList)) mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
} }
/**
* Decodes the server list.
*
* @return The list of server GUIDs.
*/
fun decodeServerList(): MutableList<String> { fun decodeServerList(): MutableList<String> {
val json = mainStorage.decodeString(KEY_ANG_CONFIGS) val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
return if (json.isNullOrBlank()) { return if (json.isNullOrBlank()) {
@@ -62,7 +81,12 @@ object MmkvManager {
} }
} }
/**
* Decodes the server configuration.
*
* @param guid The server GUID.
* @return The server configuration.
*/
fun decodeServerConfig(guid: String): ProfileItem? { fun decodeServerConfig(guid: String): ProfileItem? {
if (guid.isBlank()) { if (guid.isBlank()) {
return null return null
@@ -85,6 +109,13 @@ object MmkvManager {
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java) // return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
// } // }
/**
* Encodes the server configuration.
*
* @param guid The server GUID.
* @param config The server configuration.
* @return The server GUID.
*/
fun encodeServerConfig(guid: String, config: ProfileItem): String { fun encodeServerConfig(guid: String, config: ProfileItem): String {
val key = guid.ifBlank { Utils.getUuid() } val key = guid.ifBlank { Utils.getUuid() }
profileFullStorage.encode(key, JsonUtil.toJson(config)) profileFullStorage.encode(key, JsonUtil.toJson(config))
@@ -107,6 +138,11 @@ object MmkvManager {
return key return key
} }
/**
* Removes the server configuration.
*
* @param guid The server GUID.
*/
fun removeServer(guid: String) { fun removeServer(guid: String) {
if (guid.isBlank()) { if (guid.isBlank()) {
return return
@@ -122,6 +158,11 @@ object MmkvManager {
serverAffStorage.remove(guid) serverAffStorage.remove(guid)
} }
/**
* Removes the server configurations via subscription ID.
*
* @param subid The subscription ID.
*/
fun removeServerViaSubid(subid: String) { fun removeServerViaSubid(subid: String) {
if (subid.isBlank()) { if (subid.isBlank()) {
return return
@@ -135,6 +176,12 @@ object MmkvManager {
} }
} }
/**
* Decodes the server affiliation information.
*
* @param guid The server GUID.
* @return The server affiliation information.
*/
fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? { fun decodeServerAffiliationInfo(guid: String): ServerAffiliationInfo? {
if (guid.isBlank()) { if (guid.isBlank()) {
return null return null
@@ -146,6 +193,12 @@ object MmkvManager {
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java) return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
} }
/**
* Encodes the server test delay in milliseconds.
*
* @param guid The server GUID.
* @param testResult The test delay in milliseconds.
*/
fun encodeServerTestDelayMillis(guid: String, testResult: Long) { fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
if (guid.isBlank()) { if (guid.isBlank()) {
return return
@@ -155,6 +208,11 @@ object MmkvManager {
serverAffStorage.encode(guid, JsonUtil.toJson(aff)) serverAffStorage.encode(guid, JsonUtil.toJson(aff))
} }
/**
* Clears all test delay results.
*
* @param keys The list of server GUIDs.
*/
fun clearAllTestDelayResults(keys: List<String>?) { fun clearAllTestDelayResults(keys: List<String>?) {
keys?.forEach { key -> keys?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff -> decodeServerAffiliationInfo(key)?.let { aff ->
@@ -164,18 +222,33 @@ object MmkvManager {
} }
} }
fun removeAllServer() { /**
* Removes all server configurations.
*
* @return The number of server configurations removed.
*/
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) { /**
* Removes invalid server configurations.
*
* @param guid The server GUID.
* @return The number of server configurations removed.
*/
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,16 +256,30 @@ 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
} }
/**
* Encodes the raw server configuration.
*
* @param guid The server GUID.
* @param config The raw server configuration.
*/
fun encodeServerRaw(guid: String, config: String) { fun encodeServerRaw(guid: String, config: String) {
serverRawStorage.encode(guid, config) serverRawStorage.encode(guid, config)
} }
/**
* Decodes the raw server configuration.
*
* @param guid The server GUID.
* @return The raw server configuration.
*/
fun decodeServerRaw(guid: String): String? { fun decodeServerRaw(guid: String): String? {
return serverRawStorage.decodeString(guid) return serverRawStorage.decodeString(guid)
} }
@@ -201,6 +288,9 @@ object MmkvManager {
//region Subscriptions //region Subscriptions
/**
* Initializes the subscription list.
*/
private fun initSubsList() { private fun initSubsList() {
val subsList = decodeSubsList() val subsList = decodeSubsList()
if (subsList.isNotEmpty()) { if (subsList.isNotEmpty()) {
@@ -212,6 +302,11 @@ object MmkvManager {
encodeSubsList(subsList) encodeSubsList(subsList)
} }
/**
* Decodes the subscriptions.
*
* @return The list of subscriptions.
*/
fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> { fun decodeSubscriptions(): List<Pair<String, SubscriptionItem>> {
initSubsList() initSubsList()
@@ -225,6 +320,11 @@ object MmkvManager {
return subscriptions return subscriptions
} }
/**
* Removes the subscription.
*
* @param subid The subscription ID.
*/
fun removeSubscription(subid: String) { fun removeSubscription(subid: String) {
subStorage.remove(subid) subStorage.remove(subid)
val subsList = decodeSubsList() val subsList = decodeSubsList()
@@ -234,6 +334,12 @@ object MmkvManager {
removeServerViaSubid(subid) removeServerViaSubid(subid)
} }
/**
* Encodes the subscription.
*
* @param guid The subscription GUID.
* @param subItem The subscription item.
*/
fun encodeSubscription(guid: String, subItem: SubscriptionItem) { fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
val key = guid.ifBlank { Utils.getUuid() } val key = guid.ifBlank { Utils.getUuid() }
subStorage.encode(key, JsonUtil.toJson(subItem)) subStorage.encode(key, JsonUtil.toJson(subItem))
@@ -245,15 +351,31 @@ object MmkvManager {
} }
} }
/**
* Decodes the subscription.
*
* @param subscriptionId The subscription ID.
* @return The subscription item.
*/
fun decodeSubscription(subscriptionId: String): SubscriptionItem? { fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
val json = subStorage.decodeString(subscriptionId) ?: return null val json = subStorage.decodeString(subscriptionId) ?: return null
return JsonUtil.fromJson(json, SubscriptionItem::class.java) return JsonUtil.fromJson(json, SubscriptionItem::class.java)
} }
/**
* Encodes the subscription list.
*
* @param subsList The list of subscription IDs.
*/
fun encodeSubsList(subsList: MutableList<String>) { fun encodeSubsList(subsList: MutableList<String>) {
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList)) mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
} }
/**
* Decodes the subscription list.
*
* @return The list of subscription IDs.
*/
fun decodeSubsList(): MutableList<String> { fun decodeSubsList(): MutableList<String> {
val json = mainStorage.decodeString(KEY_SUB_IDS) val json = mainStorage.decodeString(KEY_SUB_IDS)
return if (json.isNullOrBlank()) { return if (json.isNullOrBlank()) {
@@ -267,6 +389,11 @@ object MmkvManager {
//region Asset //region Asset
/**
* Decodes the asset URLs.
*
* @return The list of asset URLs.
*/
fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> { fun decodeAssetUrls(): List<Pair<String, AssetUrlItem>> {
val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>() val assetUrlItems = mutableListOf<Pair<String, AssetUrlItem>>()
assetStorage.allKeys()?.forEach { key -> assetStorage.allKeys()?.forEach { key ->
@@ -278,15 +405,32 @@ object MmkvManager {
return assetUrlItems.sortedBy { (_, value) -> value.addedTime } return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
} }
/**
* Removes the asset URL.
*
* @param assetid The asset ID.
*/
fun removeAssetUrl(assetid: String) { fun removeAssetUrl(assetid: String) {
assetStorage.remove(assetid) assetStorage.remove(assetid)
} }
/**
* Encodes the asset.
*
* @param assetid The asset ID.
* @param assetItem The asset item.
*/
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) { fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
val key = assetid.ifBlank { Utils.getUuid() } val key = assetid.ifBlank { Utils.getUuid() }
assetStorage.encode(key, JsonUtil.toJson(assetItem)) assetStorage.encode(key, JsonUtil.toJson(assetItem))
} }
/**
* Decodes the asset.
*
* @param assetid The asset ID.
* @return The asset item.
*/
fun decodeAsset(assetid: String): AssetUrlItem? { fun decodeAsset(assetid: String): AssetUrlItem? {
val json = assetStorage.decodeString(assetid) ?: return null val json = assetStorage.decodeString(assetid) ?: return null
return JsonUtil.fromJson(json, AssetUrlItem::class.java) return JsonUtil.fromJson(json, AssetUrlItem::class.java)
@@ -296,12 +440,22 @@ object MmkvManager {
//region Routing //region Routing
/**
* Decodes the routing rulesets.
*
* @return The list of routing rulesets.
*/
fun decodeRoutingRulesets(): MutableList<RulesetItem>? { fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET) val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
if (ruleset.isNullOrEmpty()) return null if (ruleset.isNullOrEmpty()) return null
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList() return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
} }
/**
* Encodes the routing rulesets.
*
* @param rulesetList The list of routing rulesets.
*/
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) { fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
if (rulesetList.isNullOrEmpty()) if (rulesetList.isNullOrEmpty())
encodeSettings(PREF_ROUTING_RULESET, "") encodeSettings(PREF_ROUTING_RULESET, "")
@@ -310,43 +464,99 @@ object MmkvManager {
} }
//endregion //endregion
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: String?): Boolean { fun encodeSettings(key: String, value: String?): Boolean {
return settingsStorage.encode(key, value) return settingsStorage.encode(key, value)
} }
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: Int): Boolean { fun encodeSettings(key: String, value: Int): Boolean {
return settingsStorage.encode(key, value) return settingsStorage.encode(key, value)
} }
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: Boolean): Boolean { fun encodeSettings(key: String, value: Boolean): Boolean {
return settingsStorage.encode(key, value) return settingsStorage.encode(key, value)
} }
/**
* Encodes the settings.
*
* @param key The settings key.
* @param value The settings value.
* @return Whether the encoding was successful.
*/
fun encodeSettings(key: String, value: MutableSet<String>): Boolean { fun encodeSettings(key: String, value: MutableSet<String>): Boolean {
return settingsStorage.encode(key, value) return settingsStorage.encode(key, value)
} }
/**
* Decodes the settings string.
*
* @param key The settings key.
* @return The settings value.
*/
fun decodeSettingsString(key: String): String? { fun decodeSettingsString(key: String): String? {
return settingsStorage.decodeString(key) return settingsStorage.decodeString(key)
} }
/**
* Decodes the settings string.
*
* @param key The settings key.
* @param defaultValue The default value.
* @return The settings value.
*/
fun decodeSettingsString(key: String, defaultValue: String?): String? { fun decodeSettingsString(key: String, defaultValue: String?): String? {
return settingsStorage.decodeString(key, defaultValue) return settingsStorage.decodeString(key, defaultValue)
} }
/**
* Decodes the settings boolean.
*
* @param key The settings key.
* @return The settings value.
*/
fun decodeSettingsBool(key: String): Boolean { fun decodeSettingsBool(key: String): Boolean {
return settingsStorage.decodeBool(key,false) return settingsStorage.decodeBool(key, false)
} }
/**
* Decodes the settings boolean.
*
* @param key The settings key.
* @param defaultValue The default value.
* @return The settings value.
*/
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) * Decodes the settings string set.
} *
* @param key The settings key.
* @return The settings value.
*/
fun decodeSettingsStringSet(key: String): MutableSet<String>? { fun decodeSettingsStringSet(key: String): MutableSet<String>? {
return settingsStorage.decodeStringSet(key) return settingsStorage.decodeStringSet(key)
} }
@@ -355,10 +565,20 @@ object MmkvManager {
//region Others //region Others
/**
* Encodes the start on boot setting.
*
* @param startOnBoot Whether to start on boot.
*/
fun encodeStartOnBoot(startOnBoot: Boolean) { fun encodeStartOnBoot(startOnBoot: Boolean) {
MmkvManager.encodeSettings(PREF_IS_BOOTED, startOnBoot) encodeSettings(PREF_IS_BOOTED, startOnBoot)
} }
/**
* Decodes the start on boot setting.
*
* @return Whether to start on boot.
*/
fun decodeStartOnBoot(): Boolean { fun decodeStartOnBoot(): Boolean {
return decodeSettingsBool(PREF_IS_BOOTED, false) return decodeSettingsBool(PREF_IS_BOOTED, false)
} }
@@ -4,26 +4,33 @@ import android.content.Context
import android.content.res.AssetManager import android.content.res.AssetManager
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
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.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.Language
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
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.Utils.parseInt
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Collections import java.util.Collections
import kotlin.Int import java.util.Locale
object SettingsManager { object SettingsManager {
/**
* Initialize routing rulesets.
* @param context The application context.
*/
fun initRoutingRulesets(context: Context) { fun initRoutingRulesets(context: Context) {
val exist = MmkvManager.decodeRoutingRulesets() val exist = MmkvManager.decodeRoutingRulesets()
if (exist.isNullOrEmpty()) { if (exist.isNullOrEmpty()) {
@@ -32,6 +39,12 @@ object SettingsManager {
} }
} }
/**
* Get preset routing rulesets.
* @param context The application context.
* @param index The index of the routing type.
* @return A mutable list of RulesetItem.
*/
private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? { private fun getPresetRoutingRulesets(context: Context, index: Int = 0): MutableList<RulesetItem>? {
val fileName = RoutingType.fromIndex(index).fileName val fileName = RoutingType.fromIndex(index).fileName
val assets = Utils.readTextFromAssets(context, fileName) val assets = Utils.readTextFromAssets(context, fileName)
@@ -42,12 +55,21 @@ object SettingsManager {
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList() return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
} }
/**
* Reset routing rulesets from presets.
* @param context The application context.
* @param index The index of the routing type.
*/
fun resetRoutingRulesetsFromPresets(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)
} }
/**
* Reset routing rulesets.
* @param content The content of the rulesets.
* @return True if successful, false otherwise.
*/
fun resetRoutingRulesets(content: String?): Boolean { fun resetRoutingRulesets(content: String?): Boolean {
if (content.isNullOrEmpty()) { if (content.isNullOrEmpty()) {
return false return false
@@ -62,15 +84,19 @@ object SettingsManager {
resetRoutingRulesetsCommon(rulesetList) resetRoutingRulesetsCommon(rulesetList)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
return false return false
} }
} }
/**
* Common method to reset routing rulesets.
* @param rulesetList The list of rulesets.
*/
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)
} }
} }
@@ -79,6 +105,11 @@ object SettingsManager {
MmkvManager.encodeRoutingRulesets(rulesetNew) MmkvManager.encodeRoutingRulesets(rulesetNew)
} }
/**
* Get a routing ruleset by index.
* @param index The index of the ruleset.
* @return The RulesetItem.
*/
fun getRoutingRuleset(index: Int): RulesetItem? { fun getRoutingRuleset(index: Int): RulesetItem? {
if (index < 0) return null if (index < 0) return null
@@ -88,20 +119,31 @@ object SettingsManager {
return rulesetList[index] return rulesetList[index]
} }
/**
* Save a routing ruleset.
* @param index The index of the ruleset.
* @param ruleset The RulesetItem to save.
*/
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
} }
MmkvManager.encodeRoutingRulesets(rulesetList) MmkvManager.encodeRoutingRulesets(rulesetList)
} }
/**
* Remove a routing ruleset by index.
* @param index The index of the ruleset.
*/
fun removeRoutingRuleset(index: Int) { fun removeRoutingRuleset(index: Int) {
if (index < 0) return if (index < 0) return
@@ -112,7 +154,29 @@ object SettingsManager {
MmkvManager.encodeRoutingRulesets(rulesetList) MmkvManager.encodeRoutingRulesets(rulesetList)
} }
/**
* Check if routing rulesets bypass LAN.
* @return True if bypassing LAN, false otherwise.
*/
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
}
val guid = MmkvManager.getSelectServer() ?: return false
val config = 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
@@ -120,6 +184,11 @@ object SettingsManager {
return exist == true return exist == true
} }
/**
* Swap routing rulesets.
* @param fromPosition The position to swap from.
* @param toPosition The position to swap to.
*/
fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) { fun swapRoutingRuleset(fromPosition: Int, toPosition: Int) {
val rulesetList = MmkvManager.decodeRoutingRulesets() val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) return if (rulesetList.isNullOrEmpty()) return
@@ -128,6 +197,11 @@ object SettingsManager {
MmkvManager.encodeRoutingRulesets(rulesetList) MmkvManager.encodeRoutingRulesets(rulesetList)
} }
/**
* Swap subscriptions.
* @param fromPosition The position to swap from.
* @param toPosition The position to swap to.
*/
fun swapSubscriptions(fromPosition: Int, toPosition: Int) { fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
val subsList = MmkvManager.decodeSubsList() val subsList = MmkvManager.decodeSubsList()
if (subsList.isNullOrEmpty()) return if (subsList.isNullOrEmpty()) return
@@ -136,6 +210,11 @@ object SettingsManager {
MmkvManager.encodeSubsList(subsList) MmkvManager.encodeSubsList(subsList)
} }
/**
* Get server via remarks.
* @param remarks The remarks of the server.
* @return The ProfileItem.
*/
fun getServerViaRemarks(remarks: String?): ProfileItem? { fun getServerViaRemarks(remarks: String?): ProfileItem? {
if (remarks == null) { if (remarks == null) {
return null return null
@@ -150,14 +229,27 @@ object SettingsManager {
return null return null
} }
/**
* Get the SOCKS port.
* @return The SOCKS port.
*/
fun getSocksPort(): Int { fun getSocksPort(): Int {
return parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
} }
/**
* Get the HTTP port.
* @return The HTTP port.
*/
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)
} }
/**
* Initialize assets.
* @param context The application context.
* @param assets The AssetManager.
*/
fun initAssets(context: Context, assets: AssetManager) { fun initAssets(context: Context, assets: AssetManager) {
val extFolder = Utils.userAssetPath(context) val extFolder = Utils.userAssetPath(context)
@@ -173,14 +265,95 @@ object SettingsManager {
input.copyTo(output) input.copyTo(output)
} }
} }
Log.i( Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e) Log.e(ANG_PACKAGE, "asset copy failed", e)
} }
}
/**
* Get domestic DNS servers from preference.
* @return A list of domestic DNS servers.
*/
fun getDomesticDnsServers(): List<String> {
val domesticDns =
MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
val ret = domesticDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) }
if (ret.isEmpty()) {
return listOf(AppConfig.DNS_DIRECT)
}
return ret
}
/**
* Get remote DNS servers from preference.
* @return A list of remote DNS servers.
*/
fun getRemoteDnsServers(): List<String> {
val remoteDns =
MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
val ret = remoteDns.split(",").filter { Utils.isPureIpAddress(it) || Utils.isCoreDNSAddress(it) }
if (ret.isEmpty()) {
return listOf(AppConfig.DNS_PROXY)
}
return ret
}
/**
* Get VPN DNS servers from preference.
* @return A list of VPN DNS servers.
*/
fun getVpnDnsServers(): List<String> {
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
return vpnDns.split(",").filter { Utils.isPureIpAddress(it) }
}
/**
* Get delay test URL.
* @param second Whether to use the second URL.
* @return The delay test URL.
*/
fun getDelayTestUrl(second: Boolean = false): String {
return if (second) {
AppConfig.DelayTestUrl2
} else {
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
?: AppConfig.DelayTestUrl
}
}
/**
* Get the locale.
* @return The locale.
*/
fun getLocale(): Locale {
val langCode =
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code
val language = Language.fromCode(langCode)
return when (language) {
Language.AUTO -> Utils.getSysLocale()
Language.ENGLISH -> Locale.ENGLISH
Language.CHINA -> Locale.CHINA
Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
Language.VIETNAMESE -> Locale("vi")
Language.RUSSIAN -> Locale("ru")
Language.PERSIAN -> Locale("fa")
Language.ARABIC -> Locale("ar")
Language.BANGLA -> Locale("bn")
Language.BAKHTIARI -> Locale("bqi", "IR")
}
}
/**
* Set night mode.
*/
fun setNightMode() {
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) {
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
} }
} }
@@ -1,28 +1,32 @@
package com.v2ray.ang.util package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.os.SystemClock import android.os.SystemClock
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.responseLength import com.v2ray.ang.extension.responseLength
import com.v2ray.ang.util.HttpUtil
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import libv2ray.Libv2ray import libv2ray.Libv2ray
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy
import java.net.Socket import java.net.Socket
import java.net.URL
import java.net.UnknownHostException import java.net.UnknownHostException
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
object SpeedtestUtil { object SpeedtestManager {
private val tcpTestingSockets = ArrayList<Socket?>() private val tcpTestingSockets = ArrayList<Socket?>()
/**
* Measures the TCP connection time to a given URL and port.
*
* @param url The URL to connect to.
* @param port The port to connect to.
* @return The connection time in milliseconds, or -1 if the connection failed.
*/
suspend fun tcping(url: String, port: Int): Long { suspend fun tcping(url: String, port: Int): Long {
var time = -1L var time = -1L
for (k in 0 until 2) { for (k in 0 until 2) {
@@ -37,15 +41,27 @@ object SpeedtestUtil {
return time return time
} }
/**
* Measures the real ping time using the V2Ray library.
*
* @param config The configuration string for the V2Ray library.
* @return The ping time in milliseconds, or -1 if the ping failed.
*/
fun realPing(config: String): Long { fun realPing(config: String): Long {
return try { return try {
Libv2ray.measureOutboundDelay(config, Utils.getDelayTestUrl()) Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl())
} catch (e: Exception) { } catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e") Log.e(AppConfig.TAG, "Failed to measure outbound delay", e)
-1L -1L
} }
} }
/**
* Measures the ping time to a given URL using the system ping command.
*
* @param url The URL to ping.
* @return The ping time in milliseconds as a string, or "-1ms" if the ping failed.
*/
fun ping(url: String): String { fun ping(url: String): String {
try { try {
val command = "/system/bin/ping -c 3 $url" val command = "/system/bin/ping -c 3 $url"
@@ -60,11 +76,18 @@ object SpeedtestUtil {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to ping URL: $url", e)
} }
return "-1ms" return "-1ms"
} }
/**
* Measures the time taken to establish a TCP connection to a given URL and port.
*
* @param url The URL to connect to.
* @param port The port to connect to.
* @return The connection time in milliseconds, or -1 if the connection failed.
*/
fun socketConnectTime(url: String, port: Int): Long { fun socketConnectTime(url: String, port: Int): Long {
try { try {
val socket = Socket() val socket = Socket()
@@ -80,15 +103,18 @@ object SpeedtestUtil {
socket.close() socket.close()
return time return time
} catch (e: UnknownHostException) { } catch (e: UnknownHostException) {
e.printStackTrace() Log.e(AppConfig.TAG, "Unknown host: $url", e)
} catch (e: IOException) { } catch (e: IOException) {
Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e") Log.e(AppConfig.TAG, "socketConnectTime IOException: $e")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
} }
return -1 return -1
} }
/**
* Closes all TCP sockets that are currently being tested.
*/
fun closeAllTcpSockets() { fun closeAllTcpSockets() {
synchronized(this) { synchronized(this) {
tcpTestingSockets.forEach { tcpTestingSockets.forEach {
@@ -98,26 +124,19 @@ object SpeedtestUtil {
} }
} }
/**
* Tests the connection to a given URL and port.
*
* @param context The Context in which the test is running.
* @param port The port to connect to.
* @return A pair containing the elapsed time in milliseconds and the result message.
*/
fun testConnection(context: Context, port: Int): Pair<Long, String> { fun testConnection(context: Context, port: Int): Pair<Long, String> {
var result: String var result: String
var elapsed = -1L var elapsed = -1L
var conn: HttpURLConnection? = null
val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
try { try {
val url = URL(Utils.getDelayTestUrl())
conn = url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress(LOOPBACK, port)
)
) as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val code = conn.responseCode val code = conn.responseCode
elapsed = SystemClock.elapsedRealtime() - start elapsed = SystemClock.elapsedRealtime() - start
@@ -133,20 +152,23 @@ object SpeedtestUtil {
) )
} }
} catch (e: IOException) { } catch (e: IOException) {
// network exception Log.e(AppConfig.TAG, "Connection test IOException", e)
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
result = context.getString(R.string.connection_test_error, e.message) result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) { } catch (e: Exception) {
// library exception, eg sumsung Log.e(AppConfig.TAG, "Connection test Exception", e)
Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e))
result = context.getString(R.string.connection_test_error, e.message) result = context.getString(R.string.connection_test_error, e.message)
} finally { } finally {
conn?.disconnect() conn.disconnect()
} }
return Pair(elapsed, result) return Pair(elapsed, result)
} }
/**
* Gets the version of the V2Ray library.
*
* @return The version of the V2Ray library.
*/
fun getLibVersion(): String { fun getLibVersion(): String {
return Libv2ray.checkVersionX() return Libv2ray.checkVersionX()
} }
@@ -4,7 +4,6 @@ import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.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
@@ -34,10 +33,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
@@ -50,45 +51,84 @@ import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
object V2rayConfigManager { object V2rayConfigManager {
private var initConfigCache: String? = null
/**
* Retrieves the V2ray configuration for the given GUID.
*
* @param context The context of the caller.
* @param guid The unique identifier for the V2ray configuration.
* @return A ConfigResult object containing the configuration details or indicating failure.
*/
fun getV2rayConfig(context: Context, guid: String): ConfigResult { fun getV2rayConfig(context: Context, guid: String): ConfigResult {
try { try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false) val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
if (config.configType == EConfigType.CUSTOM) { return if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false) getV2rayCustomConfig(guid, config)
val domainPort = config.getServerAddressAndPort() } else {
return ConfigResult(true, guid, raw, domainPort) getV2rayNormalConfig(context, guid, config)
} }
val result = getV2rayNonCustomConfig(context, config)
//Log.d(ANG_PACKAGE, result.content)
result.guid = guid
return result
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to get V2ray config", e)
return ConfigResult(false) return ConfigResult(false)
} }
} }
private fun getV2rayNonCustomConfig(context: Context, config: ProfileItem): ConfigResult { /**
* Retrieves the speedtest V2ray configuration for the given GUID.
*
* @param context The context of the caller.
* @param guid The unique identifier for the V2ray configuration.
* @return A ConfigResult object containing the configuration details or indicating failure.
*/
fun getV2rayConfig4Speedtest(context: Context, guid: String): ConfigResult {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
return if (config.configType == EConfigType.CUSTOM) {
getV2rayCustomConfig(guid, config)
} else {
getV2rayNormalConfig4Speedtest(context, guid, config)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e)
return ConfigResult(false)
}
}
/**
* Retrieves the custom V2ray configuration.
*
* @param guid The unique identifier for the V2ray configuration.
* @param config The profile item containing the configuration details.
* @return A ConfigResult object containing the result of the configuration retrieval.
*/
private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult {
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
val domainPort = config.getServerAddressAndPort()
return ConfigResult(true, guid, raw, domainPort)
}
/**
* Retrieves the normal V2ray configuration.
*
* @param context The context in which the function is called.
* @param guid The unique identifier for the V2ray configuration.
* @param config The profile item containing the configuration details.
* @return A ConfigResult object containing the result of the configuration retrieval.
*/
private fun getV2rayNormalConfig(context: Context, guid: String, config: ProfileItem): ConfigResult {
val result = ConfigResult(false) val result = ConfigResult(false)
val address = config.server ?: return result val address = config.server ?: return result
if (!Utils.isIpAddress(address)) { if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) { if (!Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain") Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
return result return result
} }
} }
//取得默认配置 val v2rayConfig = initV2rayConfig(context) ?: return result
val assets = Utils.readTextFromAssets(context, "v2ray_config.json") v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
if (TextUtils.isEmpty(assets)) {
return result
}
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel =
MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.remarks = config.remarks v2rayConfig.remarks = config.remarks
inbounds(v2rayConfig) inbounds(v2rayConfig)
@@ -114,13 +154,78 @@ object V2rayConfigManager {
result.status = true result.status = true
result.content = v2rayConfig.toPrettyPrinting() result.content = v2rayConfig.toPrettyPrinting()
result.domainPort = if (retMore.first) retMore.second else retOut.second result.domainPort = if (retMore.first) retMore.second else retOut.second
result.guid = guid
return result return result
} }
/**
* Retrieves the normal V2ray configuration for speedtest.
*
* @param context The context in which the function is called.
* @param guid The unique identifier for the V2ray configuration.
* @param config The profile item containing the configuration details.
* @return A ConfigResult object containing the result of the configuration retrieval.
*/
private fun getV2rayNormalConfig4Speedtest(context: Context, guid: String, config: ProfileItem): ConfigResult {
val result = ConfigResult(false)
val address = config.server ?: return result
if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
return result
}
}
val v2rayConfig = initV2rayConfig(context) ?: return result
val isPlugin = config.configType == EConfigType.HYSTERIA2
val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.inbounds.clear()
v2rayConfig.routing.rules.clear()
v2rayConfig.dns = null
v2rayConfig.fakedns = null
v2rayConfig.stats = null
v2rayConfig.policy = null
v2rayConfig.outbounds.forEach { key ->
key.mux = null
}
result.status = true
result.content = v2rayConfig.toPrettyPrinting()
result.domainPort = if (retMore.first) retMore.second else retOut.second
result.guid = guid
return result
}
/**
* Initializes V2ray configuration.
*
* This function loads the V2ray configuration from assets or from a cached value.
* It first attempts to use the cached configuration if available, otherwise reads
* the configuration from the "v2ray_config.json" asset file.
*
* @param context Android context used to access application assets
* @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty
*/
private fun initV2rayConfig(context: Context): V2rayConfig? {
val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
if (TextUtils.isEmpty(assets)) {
return null
}
initConfigCache = assets
val config = JsonUtil.fromJson(assets, V2rayConfig::class.java)
return config
}
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) {
@@ -142,16 +247,15 @@ object V2rayConfigManager {
v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns") v2rayConfig.inbounds[0].sniffing?.destOverride?.add("fakedns")
} }
v2rayConfig.inbounds[1].port = httpPort if (Utils.isXray()) {
v2rayConfig.inbounds.removeAt(1)
} else {
val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds[1].port = httpPort
}
// if (httpPort > 0) {
// val httpCopy = v2rayConfig.inbounds[0].copy()
// httpCopy.port = httpPort
// httpCopy.protocol = "http"
// v2rayConfig.inbounds.add(httpCopy)
// }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to configure inbounds", e)
return false return false
} }
return true return true
@@ -214,7 +318,7 @@ object V2rayConfigManager {
routingUserRule(key, v2rayConfig) routingUserRule(key, v2rayConfig)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to configure routing", e)
return false return false
} }
return true return true
@@ -231,7 +335,7 @@ object V2rayConfigManager {
v2rayConfig.routing.rules.add(rule) v2rayConfig.routing.rules.add(rule)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to apply routing user rule", e)
} }
} }
@@ -240,7 +344,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:"))
@@ -261,7 +365,7 @@ object V2rayConfigManager {
val proxyDomain = userRule2Domain(TAG_PROXY) val proxyDomain = userRule2Domain(TAG_PROXY)
val directDomain = userRule2Domain(TAG_DIRECT) val directDomain = userRule2Domain(TAG_DIRECT)
// fakedns with all domains to make it always top priority // fakedns with all domains to make it always top priority
v2rayConfig.dns.servers?.add( v2rayConfig.dns?.servers?.add(
0, 0,
V2rayConfig.DnsBean.ServersBean( V2rayConfig.DnsBean.ServersBean(
address = "fakedns", address = "fakedns",
@@ -270,8 +374,8 @@ object V2rayConfigManager {
) )
} }
// DNS inbound对象 // DNS inbound
val remoteDns = Utils.getRemoteDnsServers() val remoteDns = SettingsManager.getRemoteDnsServers()
if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) { if (v2rayConfig.inbounds.none { e -> e.protocol == "dokodemo-door" && e.tag == "dns-in" }) {
val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean( val dnsInboundSettings = V2rayConfig.InboundBean.InSettingsBean(
address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY, address = if (Utils.isPureIpAddress(remoteDns.first())) remoteDns.first() else AppConfig.DNS_PROXY,
@@ -295,7 +399,7 @@ object V2rayConfigManager {
) )
} }
// DNS outbound对象 // DNS outbound
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) { if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
v2rayConfig.outbounds.add( v2rayConfig.outbounds.add(
V2rayConfig.OutboundBean( V2rayConfig.OutboundBean(
@@ -317,7 +421,7 @@ object V2rayConfigManager {
) )
) )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to configure custom local DNS", e)
return false return false
} }
return true return true
@@ -329,26 +433,26 @@ object V2rayConfigManager {
val servers = ArrayList<Any>() val servers = ArrayList<Any>()
//remote Dns //remote Dns
val remoteDns = Utils.getRemoteDnsServers() val remoteDns = SettingsManager.getRemoteDnsServers()
val proxyDomain = userRule2Domain(TAG_PROXY) val proxyDomain = userRule2Domain(TAG_PROXY)
remoteDns.forEach { remoteDns.forEach {
servers.add(it) servers.add(it)
} }
if (proxyDomain.size > 0) { if (proxyDomain.isNotEmpty()) {
servers.add( servers.add(
V2rayConfig.DnsBean.ServersBean( V2rayConfig.DnsBean.ServersBean(
address = remoteDns.first(), address = remoteDns.first(),
domains = proxyDomain, domains = proxyDomain,
) )
) )
} }
// domestic DNS // domestic DNS
val domesticDns = Utils.getDomesticDnsServers() val domesticDns = SettingsManager.getDomesticDnsServers()
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(),
@@ -370,9 +474,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) {
Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
}
//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 })
} }
@@ -388,7 +506,7 @@ object V2rayConfigManager {
hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
// DNS dns对象 // DNS dns
v2rayConfig.dns = V2rayConfig.DnsBean( v2rayConfig.dns = V2rayConfig.DnsBean(
servers = servers, servers = servers,
hosts = hosts hosts = hosts
@@ -406,7 +524,7 @@ object V2rayConfigManager {
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to configure DNS", e)
return false return false
} }
return true return true
@@ -424,19 +542,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?.first()?.users?.first()?.flow?.isNotEmpty() == true
) {
muxEnabled = false muxEnabled = false
} }
if (muxEnabled == true) { if (muxEnabled == true) {
outbound.mux?.enabled = true outbound.mux?.enabled = true
outbound.mux?.concurrency = outbound.mux?.concurrency = 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
@@ -478,7 +595,7 @@ object V2rayConfigManager {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
return false return false
} }
return true return true
@@ -544,7 +661,7 @@ object V2rayConfigManager {
dialerProxy = TAG_FRAGMENT dialerProxy = TAG_FRAGMENT
) )
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
return false return false
} }
return true return true
@@ -607,7 +724,7 @@ object V2rayConfigManager {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to configure more outbounds", e)
return returnPair return returnPair
} }
@@ -617,6 +734,12 @@ object V2rayConfigManager {
return returnPair return returnPair
} }
/**
* Retrieves the proxy outbound configuration for the given profile item.
*
* @param profileItem The profile item for which to get the proxy outbound configuration.
* @return The proxy outbound configuration as a V2rayConfig.OutboundBean, or null if not found.
*/
fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? { fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
return when (profileItem.configType) { return when (profileItem.configType) {
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem) EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
@@ -0,0 +1,68 @@
package com.v2ray.ang.helper
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class CustomDividerItemDecoration(
private val divider: Drawable,
private val orientation: Int
) : RecyclerView.ItemDecoration() {
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (orientation == RecyclerView.VERTICAL) {
drawVerticalDividers(canvas, parent)
} else {
drawHorizontalDividers(canvas, parent)
}
}
private fun drawVerticalDividers(canvas: Canvas, parent: RecyclerView) {
val left = parent.paddingLeft
val right = parent.width - parent.paddingRight
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(canvas)
}
}
private fun drawHorizontalDividers(canvas: Canvas, parent: RecyclerView) {
val top = parent.paddingTop
val bottom = parent.height - parent.paddingBottom
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val left = child.right + params.rightMargin
val right = left + divider.intrinsicWidth
divider.setBounds(left, top, right, bottom)
divider.draw(canvas)
}
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (orientation == RecyclerView.VERTICAL) {
outRect.set(0, 0, 0, divider.intrinsicHeight)
} else {
outRect.set(0, 0, divider.intrinsicWidth, 0)
}
}
}
@@ -16,7 +16,6 @@
package com.v2ray.ang.helper package com.v2ray.ang.helper
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.graphics.Canvas import android.graphics.Canvas
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@@ -32,10 +32,10 @@ import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.system.Os import android.system.Os
import android.widget.Toast
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.v2ray.ang.AngApplication import com.v2ray.ang.AngApplication
import com.v2ray.ang.extension.listenForPackageChanges import com.v2ray.ang.extension.listenForPackageChanges
import com.v2ray.ang.extension.toast
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@@ -126,7 +126,7 @@ object PluginManager {
if (providers.size > 1) { if (providers.size > 1) {
val message = val message =
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}" "Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show() AngApplication.application.toast(message)
throw IllegalStateException(message) throw IllegalStateException(message)
} }
val provider = providers.single().providerInfo val provider = providers.single().providerInfo
@@ -222,10 +222,10 @@ 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)
null -> null null -> null
else -> error("meta-data $key has invalid type ${value.javaClass}") else -> error("meta-data $key has invalid type ${value.javaClass}")
@@ -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
)!!
} }
@@ -7,12 +7,17 @@ import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
/**
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
* It checks if the context is not null and the action is ACTION_BOOT_COMPLETED.
* If the conditions are met, it starts the V2Ray service.
*
* @param context The Context in which the receiver is running.
* @param intent The Intent being received.
*/
override fun onReceive(context: Context?, intent: Intent?) { 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 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 if (!MmkvManager.decodeStartOnBoot() || MmkvManager.getSelectServer().isNullOrEmpty()) return
//Start v2ray V2RayServiceManager.startVService(context)
V2RayServiceManager.startV2Ray(context)
} }
} }
@@ -5,14 +5,19 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.text.TextUtils import android.text.TextUtils
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils
class TaskerReceiver : BroadcastReceiver() { class TaskerReceiver : BroadcastReceiver() {
/**
* This method is called when the BroadcastReceiver is receiving an Intent broadcast.
* It retrieves the bundle from the intent and checks the switch and guid values.
* Depending on the switch value, it starts or stops the V2Ray service.
*
* @param context The Context in which the receiver is running.
* @param intent The Intent being received.
*/
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
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)
@@ -22,16 +27,15 @@ class TaskerReceiver : BroadcastReceiver() {
return return
} else if (switch) { } else if (switch) {
if (guid == AppConfig.TASKER_DEFAULT_GUID) { if (guid == AppConfig.TASKER_DEFAULT_GUID) {
Utils.startVServiceFromToggle(context) V2RayServiceManager.startVServiceFromToggle(context)
} else { } else {
MmkvManager.setSelectServer(guid) V2RayServiceManager.startVService(context, guid)
V2RayServiceManager.startV2Ray(context)
} }
} else { } else {
Utils.stopVService(context) V2RayServiceManager.stopVService(context)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
} }
} }
} }
@@ -11,18 +11,29 @@ import android.widget.RemoteViews
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils
class WidgetProvider : AppWidgetProvider() { class WidgetProvider : AppWidgetProvider() {
/** /**
* 每次窗口小部件被更新都调用一次该方法 * This method is called every time the widget is updated.
* It updates the widget background based on the V2Ray service running state.
*
* @param context The Context in which the receiver is running.
* @param appWidgetManager The AppWidgetManager instance.
* @param appWidgetIds The appWidgetIds for which an update is needed.
*/ */
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds) super.onUpdate(context, appWidgetManager, appWidgetIds)
updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.v2rayPoint.isRunning) updateWidgetBackground(context, appWidgetManager, appWidgetIds, V2RayServiceManager.isRunning())
} }
/**
* Updates the widget background based on whether the V2Ray service is running.
*
* @param context The Context in which the receiver is running.
* @param appWidgetManager The AppWidgetManager instance.
* @param appWidgetIds The appWidgetIds for which an update is needed.
* @param isRunning Boolean indicating if the V2Ray service is running.
*/
private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) { private fun updateWidgetBackground(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, isRunning: Boolean) {
val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch) val remoteViews = RemoteViews(context.packageName, R.layout.widget_switch)
val intent = Intent(context, WidgetProvider::class.java) val intent = Intent(context, WidgetProvider::class.java)
@@ -52,15 +63,19 @@ class WidgetProvider : AppWidgetProvider() {
} }
/** /**
* 接收窗口小部件发送的广播 * This method is called when the BroadcastReceiver is receiving an Intent broadcast.
* It handles widget click actions and updates the widget background based on the V2Ray service state.
*
* @param context The Context in which the receiver is running.
* @param intent The Intent being received.
*/ */
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent) super.onReceive(context, intent)
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) { if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
if (V2RayServiceManager.v2rayPoint.isRunning) { if (V2RayServiceManager.isRunning()) {
Utils.stopVService(context) V2RayServiceManager.stopVService(context)
} else { } else {
Utils.startVServiceFromToggle(context) V2RayServiceManager.startVServiceFromToggle(context)
} }
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) { } else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
AppWidgetManager.getInstance(context)?.let { manager -> AppWidgetManager.getInstance(context)?.let { manager ->
@@ -0,0 +1,251 @@
package com.v2ray.ang.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.R
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toSpeedString
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.math.min
object NotificationService {
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
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 var lastQueryTime = 0L
private var mBuilder: NotificationCompat.Builder? = null
private var speedNotificationJob: Job? = null
private var mNotificationManager: NotificationManager? = null
/**
* Starts the speed notification.
* @param currentConfig The current profile configuration.
*/
fun startSpeedNotification(currentConfig: ProfileItem?) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
if (speedNotificationJob != null || V2RayServiceManager.isRunning() == false) return
lastQueryTime = System.currentTimeMillis()
var lastZeroSpeed = false
val outboundTags = currentConfig?.getAllOutboundTags()
outboundTags?.remove(TAG_DIRECT)
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
val queryTime = System.currentTimeMillis()
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
var proxyTotal = 0L
val text = StringBuilder()
outboundTags?.forEach {
val up = V2RayServiceManager.queryStats(it, AppConfig.UPLINK)
val down = V2RayServiceManager.queryStats(it, AppConfig.DOWNLINK)
if (up + down > 0) {
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
proxyTotal += up + down
}
}
val directUplink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.UPLINK)
val directDownlink = V2RayServiceManager.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
if (!zeroSpeed || !lastZeroSpeed) {
if (proxyTotal == 0L) {
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
}
appendSpeedString(
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
directDownlink / sinceLastQueryInSeconds
)
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
}
lastZeroSpeed = zeroSpeed
lastQueryTime = queryTime
delay(3000)
}
}
}
/**
* Shows the notification.
* @param currentConfig The current profile configuration.
*/
fun showNotification(currentConfig: ProfileItem?) {
val service = getService() ?: return
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val startMainIntent = Intent(service, MainActivity::class.java)
val contentPendingIntent = PendingIntent.getActivity(service, NOTIFICATION_PENDING_INTENT_CONTENT, startMainIntent, flags)
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
stopV2RayIntent.`package` = ANG_PACKAGE
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
val stopV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent, flags)
val restartV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
restartV2RayIntent.`package` = ANG_PACKAGE
restartV2RayIntent.putExtra("key", AppConfig.MSG_STATE_RESTART)
val restartV2RayPendingIntent = PendingIntent.getBroadcast(service, NOTIFICATION_PENDING_INTENT_RESTART_V2RAY, restartV2RayIntent, flags)
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
mBuilder = NotificationCompat.Builder(service, channelId)
.setSmallIcon(R.drawable.ic_stat_name)
.setContentTitle(currentConfig?.remarks)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true)
.setShowWhen(false)
.setOnlyAlertOnce(true)
.setContentIntent(contentPendingIntent)
.addAction(
R.drawable.ic_delete_24dp,
service.getString(R.string.notification_action_stop_v2ray),
stopV2RayPendingIntent
)
.addAction(
R.drawable.ic_delete_24dp,
service.getString(R.string.title_service_restart),
restartV2RayPendingIntent
)
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
}
/**
* Cancels the notification.
*/
fun cancelNotification() {
val service = getService() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
} else {
service.stopForeground(true)
}
mBuilder = null
speedNotificationJob?.cancel()
speedNotificationJob = null
}
/**
* Stops the speed notification.
* @param currentConfig The current profile configuration.
*/
fun stopSpeedNotification(currentConfig: ProfileItem?) {
speedNotificationJob?.let {
it.cancel()
speedNotificationJob = null
updateNotification(currentConfig?.remarks, 0, 0)
}
}
/**
* Creates a notification channel for Android O and above.
* @return The channel ID.
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channelId = AppConfig.RAY_NG_CHANNEL_ID
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
val chan = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_HIGH
)
chan.lightColor = Color.DKGRAY
chan.importance = NotificationManager.IMPORTANCE_NONE
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
getNotificationManager()?.createNotificationChannel(chan)
return channelId
}
/**
* Updates the notification with the given content text and traffic data.
* @param contentText The content text.
* @param proxyTraffic The proxy traffic.
* @param directTraffic The direct traffic.
*/
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
if (mBuilder != null) {
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
} else if (proxyTraffic > directTraffic) {
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
} else {
mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
}
mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
mBuilder?.setContentText(contentText)
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
}
}
/**
* Gets the notification manager.
* @return The notification manager.
*/
private fun getNotificationManager(): NotificationManager? {
if (mNotificationManager == null) {
val service = getService() ?: return null
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
return mNotificationManager
}
/**
* Appends the speed string to the given text.
* @param text The text to append to.
* @param name The name of the tag.
* @param up The uplink speed.
* @param down The downlink speed.
*/
private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
var n = name ?: "no tag"
n = n.substring(0, min(n.length, 6))
text.append(n)
for (i in n.length..6 step 2) {
text.append("\t")
}
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
}
/**
* Gets the service instance.
* @return The service instance.
*/
private fun getService(): Service? {
return V2RayServiceManager.serviceControl?.get()?.getService()
}
}
@@ -2,7 +2,7 @@ package com.v2ray.ang.service
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -10,8 +10,13 @@ import kotlinx.coroutines.launch
class ProcessService { class ProcessService {
private var process: Process? = null private var process: Process? = null
/**
* Runs a process with the given command.
* @param context The context.
* @param cmd The command to run.
*/
fun runProcess(context: Context, cmd: MutableList<String>) { fun runProcess(context: Context, cmd: MutableList<String>) {
Log.d(ANG_PACKAGE, cmd.toString()) Log.i(AppConfig.TAG, cmd.toString())
try { try {
val proBuilder = ProcessBuilder(cmd) val proBuilder = ProcessBuilder(cmd)
@@ -22,23 +27,26 @@ class ProcessService {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(50L) Thread.sleep(50L)
Log.d(ANG_PACKAGE, "runProcess check") Log.i(AppConfig.TAG, "runProcess check")
process?.waitFor() process?.waitFor()
Log.d(ANG_PACKAGE, "runProcess exited") Log.i(AppConfig.TAG, "runProcess exited")
} }
Log.d(ANG_PACKAGE, process.toString()) Log.i(AppConfig.TAG, process.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, e.toString(), e)
} }
} }
/**
* Stops the running process.
*/
fun stopProcess() { fun stopProcess() {
try { try {
Log.d(ANG_PACKAGE, "runProcess destroy") Log.i(AppConfig.TAG, "runProcess destroy")
process?.destroy() process?.destroy()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, "Failed to destroy process", e)
} }
} }
} }
@@ -1,6 +1,5 @@
package com.v2ray.ang.service package com.v2ray.ang.service
import android.annotation.TargetApi
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -9,6 +8,8 @@ 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 android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -16,9 +17,13 @@ import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
@TargetApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
class QSTileService : TileService() { class QSTileService : TileService() {
/**
* Sets the state of the tile.
* @param state The state to set.
*/
fun setState(state: Int) { fun setState(state: Int) {
if (state == Tile.STATE_INACTIVE) { if (state == Tile.STATE_INACTIVE) {
qsTile?.state = Tile.STATE_INACTIVE qsTile?.state = Tile.STATE_INACTIVE
@@ -26,7 +31,7 @@ class QSTileService : TileService() {
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
} else if (state == Tile.STATE_ACTIVE) { } else if (state == Tile.STATE_ACTIVE) {
qsTile?.state = Tile.STATE_ACTIVE qsTile?.state = Tile.STATE_ACTIVE
qsTile?.label = V2RayServiceManager.currentConfig?.remarks qsTile?.label = V2RayServiceManager.getRunningServerName()
qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name) qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
} }
@@ -37,7 +42,6 @@ class QSTileService : TileService() {
* 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): * 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)`. * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
*/ */
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
@@ -48,6 +52,9 @@ class QSTileService : TileService() {
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
} }
/**
* Called when the tile stops listening.
*/
override fun onStopListening() { override fun onStopListening() {
super.onStopListening() super.onStopListening()
@@ -55,20 +62,23 @@ class QSTileService : TileService() {
applicationContext.unregisterReceiver(mMsgReceive) applicationContext.unregisterReceiver(mMsgReceive)
mMsgReceive = null mMsgReceive = null
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to unregister receiver", e)
} }
} }
/**
* Called when the tile is clicked.
*/
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
when (qsTile.state) { when (qsTile.state) {
Tile.STATE_INACTIVE -> { Tile.STATE_INACTIVE -> {
Utils.startVServiceFromToggle(this) V2RayServiceManager.startVServiceFromToggle(this)
} }
Tile.STATE_ACTIVE -> { Tile.STATE_ACTIVE -> {
Utils.stopVService(this) V2RayServiceManager.stopVService(this)
} }
} }
} }
@@ -3,11 +3,26 @@ package com.v2ray.ang.service
import android.app.Service import android.app.Service
interface ServiceControl { interface ServiceControl {
/**
* Gets the service instance.
* @return The service instance.
*/
fun getService(): Service fun getService(): Service
/**
* Starts the service.
*/
fun startService() fun startService()
/**
* Stops the service.
*/
fun stopService() fun stopService()
/**
* Protects the VPN socket.
* @param socket The socket to protect.
* @return True if the socket is protected, false otherwise.
*/
fun vpnProtect(socket: Int): Boolean fun vpnProtect(socket: Int): Boolean
} }
@@ -19,7 +19,6 @@ import com.v2ray.ang.handler.MmkvManager
object SubscriptionUpdater { object SubscriptionUpdater {
class UpdateTask(context: Context, params: WorkerParameters) : class UpdateTask(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) { CoroutineWorker(context, params) {
@@ -33,9 +32,13 @@ object SubscriptionUpdater {
.setCategory(NotificationCompat.CATEGORY_SERVICE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
/**
* Performs the subscription update work.
* @return The result of the work.
*/
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Log.d(AppConfig.ANG_PACKAGE, "subscription automatic update starting") Log.i(AppConfig.TAG, "subscription automatic update starting")
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate } val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
@@ -53,10 +56,7 @@ object SubscriptionUpdater {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
notificationManager.notify(3, notification.build()) notificationManager.notify(3, notification.build())
Log.d( Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
AppConfig.ANG_PACKAGE,
"subscription automatic update: ---${subItem.remarks}"
)
updateConfigViaSub(Pair(sub.first, subItem)) updateConfigViaSub(Pair(sub.first, subItem))
notification.setContentText("Updating ${subItem.remarks}") notification.setContentText("Updating ${subItem.remarks}")
} }
@@ -6,50 +6,87 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
class V2RayProxyOnlyService : Service(), ServiceControl { class V2RayProxyOnlyService : Service(), ServiceControl {
/**
* Initializes the service.
*/
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
V2RayServiceManager.serviceControl = SoftReference(this) V2RayServiceManager.serviceControl = SoftReference(this)
} }
/**
* Handles the start command for the service.
* @param intent The intent.
* @param flags The flags.
* @param startId The start ID.
* @return The start mode.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
V2RayServiceManager.startV2rayPoint() V2RayServiceManager.startV2rayPoint()
return START_STICKY return START_STICKY
} }
/**
* Destroys the service.
*/
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
V2RayServiceManager.stopV2rayPoint() V2RayServiceManager.stopV2rayPoint()
} }
/**
* Gets the service instance.
* @return The service instance.
*/
override fun getService(): Service { override fun getService(): Service {
return this return this
} }
/**
* Starts the service.
*/
override fun startService() { override fun startService() {
// do nothing // do nothing
} }
/**
* Stops the service.
*/
override fun stopService() { override fun stopService() {
stopSelf() stopSelf()
} }
/**
* Protects the VPN socket.
* @param socket The socket to protect.
* @return True if the socket is protected, false otherwise.
*/
override fun vpnProtect(socket: Int): Boolean { override fun vpnProtect(socket: Int): Boolean {
return true return true
} }
/**
* Binds the service.
* @param intent The intent.
* @return The binder.
*/
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null
} }
/**
* Attaches the base context to the service.
* @param newBase The new base context.
*/
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let { val context = newBase?.let {
MyContextWrapper.wrap(newBase, Utils.getLocale()) MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
} }
super.attachBaseContext(context) super.attachBaseContext(context)
} }
@@ -1,36 +1,25 @@
package com.v2ray.ang.service package com.v2ray.ang.service
import android.app.Notification import android.app.Service
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.Color
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat 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.TAG_DIRECT
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.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.ui.MainActivity
import com.v2ray.ang.util.MessageUtil 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.launch import kotlinx.coroutines.launch
@@ -38,16 +27,12 @@ import libv2ray.Libv2ray
import libv2ray.V2RayPoint import libv2ray.V2RayPoint
import libv2ray.V2RayVPNServiceSupportsSet import libv2ray.V2RayVPNServiceSupportsSet
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
import kotlin.math.min
object V2RayServiceManager { object V2RayServiceManager {
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_PENDING_INTENT_CONTENT = 0
private const val NOTIFICATION_PENDING_INTENT_STOP_V2RAY = 1
private const val NOTIFICATION_ICON_THRESHOLD = 3000
val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) private val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
private val mMsgReceive = ReceiveMessageHandler() private val mMsgReceive = ReceiveMessageHandler()
private var currentConfig: ProfileItem? = null
var serviceControl: SoftReference<ServiceControl>? = null var serviceControl: SoftReference<ServiceControl>? = null
set(value) { set(value) {
@@ -55,18 +40,66 @@ object V2RayServiceManager {
Seq.setContext(value?.get()?.getService()?.applicationContext) Seq.setContext(value?.get()?.getService()?.applicationContext)
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
} }
var currentConfig: ProfileItem? = null
private var lastQueryTime = 0L /**
private var mBuilder: NotificationCompat.Builder? = null * Starts the V2Ray service from a toggle action.
private var mDisposable: Disposable? = null * @param context The context from which the service is started.
private var mNotificationManager: NotificationManager? = null * @return True if the service was started successfully, false otherwise.
*/
fun startVServiceFromToggle(context: Context): Boolean {
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
context.toast(R.string.app_tile_first_use)
return false
}
startContextService(context)
return true
}
fun startV2Ray(context: Context) { /**
* Starts the V2Ray service.
* @param context The context from which the service is started.
* @param guid The GUID of the server configuration to use (optional).
*/
fun startVService(context: Context, guid: String? = null) {
if (guid != null) {
MmkvManager.setSelectServer(guid)
}
startContextService(context)
}
/**
* Stops the V2Ray service.
* @param context The context from which the service is stopped.
*/
fun stopVService(context: Context) {
context.toast(R.string.toast_services_stop)
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
}
/**
* Checks if the V2Ray service is running.
* @return True if the service is running, false otherwise.
*/
fun isRunning() = v2rayPoint.isRunning
/**
* Gets the name of the currently running server.
* @return The name of the running server.
*/
fun getRunningServerName() = currentConfig?.remarks.orEmpty()
/**
* Starts the context service for V2Ray.
* @param context The context from which the service is started.
*/
private fun startContextService(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
@@ -75,7 +108,7 @@ object V2RayServiceManager {
} else { } else {
context.toast(R.string.toast_services_start) context.toast(R.string.toast_services_start)
} }
val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) { val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) {
Intent(context.applicationContext, V2RayVpnService::class.java) Intent(context.applicationContext, V2RayVpnService::class.java)
} else { } else {
Intent(context.applicationContext, V2RayProxyOnlyService::class.java) Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
@@ -87,53 +120,13 @@ object V2RayServiceManager {
} }
} }
private class V2RayCallback : V2RayVPNServiceSupportsSet {
override fun shutdown(): Long {
val serviceControl = serviceControl?.get() ?: return -1
// called by go
return try {
serviceControl.stopService()
0
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
-1
}
}
override fun prepare(): Long {
return 0
}
override fun protect(l: Long): Boolean {
val serviceControl = serviceControl?.get() ?: return true
return serviceControl.vpnProtect(l.toInt())
}
override fun onEmitStatus(l: Long, s: String?): Long {
return 0
}
override fun setup(s: String): Long {
val serviceControl = serviceControl?.get() ?: return -1
return try {
serviceControl.startService()
lastQueryTime = System.currentTimeMillis()
startSpeedNotification()
0
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
-1
}
}
}
/** /**
* 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): * 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)`. * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
* Starts the V2Ray point.
*/ */
fun startV2rayPoint() { fun startV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return val service = getService() ?: return
val guid = MmkvManager.getSelectServer() ?: return val guid = MmkvManager.getSelectServer() ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return val config = MmkvManager.decodeServerConfig(guid) ?: return
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
@@ -150,7 +143,7 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_USER_PRESENT) mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags()) ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
} }
v2rayPoint.configureFileContent = result.content v2rayPoint.configureFileContent = result.content
@@ -160,45 +153,155 @@ object V2RayServiceManager {
try { try {
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, "Failed to start V2Ray loop", e)
} }
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
showNotification() NotificationService.showNotification(currentConfig)
PluginUtil.runPlugin(service, config, result.domainPort) PluginUtil.runPlugin(service, config, result.domainPort)
} else { } else {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
cancelNotification() NotificationService.cancelNotification()
} }
} }
/**
* Stops the V2Ray point.
*/
fun stopV2rayPoint() { fun stopV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return val service = getService() ?: return
if (v2rayPoint.isRunning) { if (v2rayPoint.isRunning) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
v2rayPoint.stopLoop() v2rayPoint.stopLoop()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
} }
} }
} }
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
cancelNotification() NotificationService.cancelNotification()
try { try {
service.unregisterReceiver(mMsgReceive) service.unregisterReceiver(mMsgReceive)
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
} }
PluginUtil.stopPlugin() PluginUtil.stopPlugin()
} }
/**
* Queries the statistics for a given tag and link.
* @param tag The tag to query.
* @param link The link to query.
* @return The statistics value.
*/
fun queryStats(tag: String, link: String): Long {
return v2rayPoint.queryStats(tag, link)
}
/**
* Measures the delay for V2Ray.
*/
private fun measureV2rayDelay() {
CoroutineScope(Dispatchers.IO).launch {
val service = getService() ?: return@launch
var time = -1L
var errstr = ""
if (v2rayPoint.isRunning) {
try {
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
if (time == -1L) {
try {
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl(true))
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
}
}
val result = if (time == -1L) {
service.getString(R.string.connection_test_error, errstr)
} else {
service.getString(R.string.connection_test_available, time)
}
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
}
}
/**
* Gets the current service instance.
* @return The current service instance, or null if not available.
*/
private fun getService(): Service? {
return serviceControl?.get()?.getService()
}
private class V2RayCallback : V2RayVPNServiceSupportsSet {
override fun shutdown(): Long {
val serviceControl = serviceControl?.get() ?: return -1
// called by go
return try {
serviceControl.stopService()
0
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
-1
}
}
override fun prepare(): Long {
return 0
}
override fun protect(l: Long): Boolean {
val serviceControl = serviceControl?.get() ?: return true
return serviceControl.vpnProtect(l.toInt())
}
/**
* Called by Go to emit status.
* @param l The status code.
* @param s The status message.
* @return The status code.
*/
override fun onEmitStatus(l: Long, s: String?): Long {
return 0
}
/**
* Called by Go to set up the service.
* @param s The setup string.
* @return The status code.
*/
override fun setup(s: String): Long {
val serviceControl = serviceControl?.get() ?: return -1
return try {
serviceControl.startService()
NotificationService.startSpeedNotification(currentConfig)
0
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to setup service in callback", e)
-1
}
}
}
private class ReceiveMessageHandler : BroadcastReceiver() { private class ReceiveMessageHandler : BroadcastReceiver() {
/**
* Handles received broadcast messages.
* @param ctx The context in which the receiver is running.
* @param intent The intent being received.
*/
override fun onReceive(ctx: Context?, intent: Intent?) { override fun onReceive(ctx: Context?, intent: Intent?) {
val serviceControl = serviceControl?.get() ?: return val serviceControl = serviceControl?.get() ?: return
when (intent?.getIntExtra("key", 0)) { when (intent?.getIntExtra("key", 0)) {
@@ -219,11 +322,15 @@ object V2RayServiceManager {
} }
AppConfig.MSG_STATE_STOP -> { AppConfig.MSG_STATE_STOP -> {
Log.i(AppConfig.TAG, "Stop Service")
serviceControl.stopService() serviceControl.stopService()
} }
AppConfig.MSG_STATE_RESTART -> { AppConfig.MSG_STATE_RESTART -> {
startV2rayPoint() Log.i(AppConfig.TAG, "Restart Service")
serviceControl.stopService()
Thread.sleep(500L)
startVService(serviceControl.getService())
} }
AppConfig.MSG_MEASURE_DELAY -> { AppConfig.MSG_MEASURE_DELAY -> {
@@ -233,209 +340,15 @@ object V2RayServiceManager {
when (intent?.action) { when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> { Intent.ACTION_SCREEN_OFF -> {
Log.d(ANG_PACKAGE, "SCREEN_OFF, stop querying stats") Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats")
stopSpeedNotification() NotificationService.stopSpeedNotification(currentConfig)
} }
Intent.ACTION_SCREEN_ON -> { Intent.ACTION_SCREEN_ON -> {
Log.d(ANG_PACKAGE, "SCREEN_ON, start querying stats") Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats")
startSpeedNotification() NotificationService.startSpeedNotification(currentConfig)
} }
} }
} }
} }
private fun measureV2rayDelay() {
CoroutineScope(Dispatchers.IO).launch {
val service = serviceControl?.get()?.getService() ?: return@launch
var time = -1L
var errstr = ""
if (v2rayPoint.isRunning) {
try {
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl())
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
if (time == -1L) {
try {
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(true))
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
}
}
val result = if (time == -1L) {
service.getString(R.string.connection_test_error, errstr)
} else {
service.getString(R.string.connection_test_available, time)
}
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
}
}
private fun showNotification() {
val service = serviceControl?.get()?.getService() ?: return
val startMainIntent = Intent(service, MainActivity::class.java)
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
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val stopV2RayIntent = Intent(AppConfig.BROADCAST_ACTION_SERVICE)
stopV2RayIntent.`package` = ANG_PACKAGE
stopV2RayIntent.putExtra("key", AppConfig.MSG_STATE_STOP)
val stopV2RayPendingIntent = PendingIntent.getBroadcast(
service,
NOTIFICATION_PENDING_INTENT_STOP_V2RAY, stopV2RayIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
mBuilder = NotificationCompat.Builder(service, channelId)
.setSmallIcon(R.drawable.ic_stat_name)
.setContentTitle(currentConfig?.remarks)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true)
.setShowWhen(false)
.setOnlyAlertOnce(true)
.setContentIntent(contentPendingIntent)
.addAction(
R.drawable.ic_delete_24dp,
service.getString(R.string.notification_action_stop_v2ray),
stopV2RayPendingIntent
)
//.build()
//mBuilder?.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE) //取消震动,铃声其他都不好使
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channelId = AppConfig.RAY_NG_CHANNEL_ID
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
val chan = NotificationChannel(
channelId,
channelName, NotificationManager.IMPORTANCE_HIGH
)
chan.lightColor = Color.DKGRAY
chan.importance = NotificationManager.IMPORTANCE_NONE
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
getNotificationManager()?.createNotificationChannel(chan)
return channelId
}
fun cancelNotification() {
val service = serviceControl?.get()?.getService() ?: return
service.stopForeground(true)
mBuilder = null
mDisposable?.dispose()
mDisposable = null
}
private fun updateNotification(contentText: String?, proxyTraffic: Long, directTraffic: Long) {
if (mBuilder != null) {
if (proxyTraffic < NOTIFICATION_ICON_THRESHOLD && directTraffic < NOTIFICATION_ICON_THRESHOLD) {
mBuilder?.setSmallIcon(R.drawable.ic_stat_name)
} else if (proxyTraffic > directTraffic) {
mBuilder?.setSmallIcon(R.drawable.ic_stat_proxy)
} else {
mBuilder?.setSmallIcon(R.drawable.ic_stat_direct)
}
mBuilder?.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
mBuilder?.setContentText(contentText) // Emui4.1 need content text even if style is set as BigTextStyle
getNotificationManager()?.notify(NOTIFICATION_ID, mBuilder?.build())
}
}
private fun getNotificationManager(): NotificationManager? {
if (mNotificationManager == null) {
val service = serviceControl?.get()?.getService() ?: return null
mNotificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
return mNotificationManager
}
private fun startSpeedNotification() {
if (mDisposable == null &&
v2rayPoint.isRunning &&
MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) == true
) {
var lastZeroSpeed = false
val outboundTags = currentConfig?.getAllOutboundTags()
outboundTags?.remove(TAG_DIRECT)
mDisposable = Observable.interval(3, java.util.concurrent.TimeUnit.SECONDS)
.subscribe {
val queryTime = System.currentTimeMillis()
val sinceLastQueryInSeconds = (queryTime - lastQueryTime) / 1000.0
var proxyTotal = 0L
val text = StringBuilder()
outboundTags?.forEach {
val up = v2rayPoint.queryStats(it, AppConfig.UPLINK)
val down = v2rayPoint.queryStats(it, AppConfig.DOWNLINK)
if (up + down > 0) {
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
proxyTotal += up + down
}
}
val directUplink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.UPLINK)
val directDownlink = v2rayPoint.queryStats(TAG_DIRECT, AppConfig.DOWNLINK)
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
if (!zeroSpeed || !lastZeroSpeed) {
if (proxyTotal == 0L) {
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
}
appendSpeedString(
text, TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
directDownlink / sinceLastQueryInSeconds
)
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
}
lastZeroSpeed = zeroSpeed
lastQueryTime = queryTime
}
}
}
private fun appendSpeedString(text: StringBuilder, name: String?, up: Double, down: Double) {
var n = name ?: "no tag"
n = n.substring(0, min(n.length, 6))
text.append(n)
for (i in n.length..6 step 2) {
text.append("\t")
}
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
}
private fun stopSpeedNotification() {
mDisposable?.let {
it.dispose() //stop queryStats
mDisposable = null
updateNotification(currentConfig?.remarks, 0, 0)
}
}
} }
@@ -9,10 +9,10 @@ import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.serializable
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.PluginUtil import com.v2ray.ang.util.PluginUtil
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import go.Seq import go.Seq
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -26,12 +26,22 @@ import java.util.concurrent.Executors
class V2RayTestService : Service() { class V2RayTestService : Service() {
private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) } private val realTestScope by lazy { CoroutineScope(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).asCoroutineDispatcher()) }
/**
* Initializes the V2Ray environment.
*/
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Seq.setContext(this) Seq.setContext(this)
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey()) Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
} }
/**
* Handles the start command for the service.
* @param intent The intent.
* @param flags The flags.
* @param startId The start ID.
* @return The start mode.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.getIntExtra("key", 0)) { when (intent?.getIntExtra("key", 0)) {
MSG_MEASURE_CONFIG -> { MSG_MEASURE_CONFIG -> {
@@ -49,10 +59,20 @@ class V2RayTestService : Service() {
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
/**
* Binds the service.
* @param intent The intent.
* @return The binder.
*/
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null
} }
/**
* Starts the real ping test.
* @param guid The GUID of the configuration.
* @return The ping result.
*/
private fun startRealPing(guid: String): Long { private fun startRealPing(guid: String): Long {
val retFailure = -1L val retFailure = -1L
@@ -61,11 +81,11 @@ class V2RayTestService : Service() {
val delay = PluginUtil.realPingHy2(this, config) val delay = PluginUtil.realPingHy2(this, config)
return delay return delay
} else { } else {
val config = V2rayConfigManager.getV2rayConfig(this, guid) val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
if (!config.status) { if (!configResult.status) {
return retFailure return retFailure
} }
return SpeedtestUtil.realPing(config.content) return SpeedtestManager.realPing(configResult.content)
} }
} }
} }
@@ -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,7 +18,6 @@ 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
@@ -34,18 +34,16 @@ 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.14.1"
private const val PRIVATE_VLAN4_ROUTER = "26.26.26.2" private const val PRIVATE_VLAN4_ROUTER = "10.10.14.2"
private const val PRIVATE_VLAN6_CLIENT = "da26:2626::1" private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:14:1"
private const val PRIVATE_VLAN6_ROUTER = "da26:2626::2" private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:14:2"
private const val TUN2SOCKS = "libtun2socks.so" private const val TUN2SOCKS = "libtun2socks.so"
}
}
private lateinit var mInterface: ParcelFileDescriptor private lateinit var mInterface: ParcelFileDescriptor
private var isRunning = false private var isRunning = false
//val fd: Int get() = mInterface.fd
private lateinit var process: Process private lateinit var process: Process
/**destroy /**destroy
@@ -87,7 +85,6 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy) StrictMode.setThreadPolicy(policy)
V2RayServiceManager.serviceControl = SoftReference(this) V2RayServiceManager.serviceControl = SoftReference(this)
@@ -104,15 +101,61 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
V2RayServiceManager.cancelNotification() NotificationService.cancelNotification()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
V2RayServiceManager.startV2rayPoint()
return START_STICKY
//return super.onStartCommand(intent, flags, startId)
}
override fun getService(): Service {
return this
}
override fun startService() {
setup()
}
override fun stopService() {
stopV2Ray(true)
}
override fun vpnProtect(socket: Int): Boolean {
return protect(socket)
}
@RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let {
MyContextWrapper.wrap(newBase, SettingsManager.getLocale())
}
super.attachBaseContext(context)
}
/**
* Sets up the VPN service.
* Prepares the VPN and configures it if preparation is successful.
*/
private fun setup() { private fun setup() {
val prepare = prepare(this) val prepare = prepare(this)
if (prepare != null) { if (prepare != null) {
return return
} }
if (setupVpnService() != true) {
return
}
runTun2socks()
}
/**
* Configures the VPN service.
* @return True if the VPN service was configured successfully, false otherwise.
*/
private fun setupVpnService(): Boolean {
// If the old interface has exactly the same parameters, use it! // If the old interface has exactly the same parameters, use it!
// Configure a builder while parsing the parameters. // Configure a builder while parsing the parameters.
val builder = Builder() val builder = Builder()
@@ -143,7 +186,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
// if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) { // if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
// builder.addDnsServer(PRIVATE_VLAN4_ROUTER) // builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
// } else { // } else {
Utils.getVpnDnsServers() SettingsManager.getVpnDnsServers()
.forEach { .forEach {
if (Utils.isPureIpAddress(it)) { if (Utils.isPureIpAddress(it)) {
builder.addDnsServer(it) builder.addDnsServer(it)
@@ -151,7 +194,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
} }
// } // }
builder.setSession(V2RayServiceManager.currentConfig?.remarks.orEmpty()) builder.setSession(V2RayServiceManager.getRunningServerName())
val selfPackageName = BuildConfig.APPLICATION_ID val selfPackageName = BuildConfig.APPLICATION_ID
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PER_APP_PROXY)) {
@@ -166,7 +209,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}") Log.e(AppConfig.TAG, "Failed to configure app in VPN: ${e.localizedMessage}", e)
} }
} }
} else { } else {
@@ -184,26 +227,34 @@ class V2RayVpnService : VpnService(), ServiceControl {
try { try {
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to request default network", e)
} }
} }
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.
try { try {
mInterface = builder.establish()!! mInterface = builder.establish()!!
isRunning = true isRunning = true
runTun2socks() return true
} catch (e: Exception) { } catch (e: Exception) {
// non-nullable lateinit var // non-nullable lateinit var
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to establish VPN interface", e)
stopV2Ray() stopV2Ray()
} }
return false
} }
/**
* Runs the tun2socks process.
* Starts the tun2socks process with the appropriate parameters.
*/
private fun runTun2socks() { private fun runTun2socks() {
val socksPort = SettingsManager.getSocksPort() val socksPort = SettingsManager.getSocksPort()
val cmd = arrayListOf( val cmd = arrayListOf(
@@ -226,7 +277,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
cmd.add("--dnsgw") cmd.add("--dnsgw")
cmd.add("$LOOPBACK:${localDnsPort}") cmd.add("$LOOPBACK:${localDnsPort}")
} }
Log.d(packageName, cmd.toString()) Log.i(AppConfig.TAG, cmd.toString())
try { try {
val proBuilder = ProcessBuilder(cmd) val proBuilder = ProcessBuilder(cmd)
@@ -235,32 +286,36 @@ class V2RayVpnService : VpnService(), ServiceControl {
.directory(applicationContext.filesDir) .directory(applicationContext.filesDir)
.start() .start()
Thread { Thread {
Log.d(packageName, "$TUN2SOCKS check") Log.i(AppConfig.TAG, "$TUN2SOCKS check")
process.waitFor() process.waitFor()
Log.d(packageName, "$TUN2SOCKS exited") Log.i(AppConfig.TAG, "$TUN2SOCKS exited")
if (isRunning) { if (isRunning) {
Log.d(packageName, "$TUN2SOCKS restart") Log.i(AppConfig.TAG, "$TUN2SOCKS restart")
runTun2socks() runTun2socks()
} }
}.start() }.start()
Log.d(packageName, process.toString()) Log.i(AppConfig.TAG, process.toString())
sendFd() sendFd()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(packageName, e.toString()) Log.e(AppConfig.TAG, "Failed to start tun2socks process", e)
} }
} }
/**
* Sends the file descriptor to the tun2socks process.
* Attempts to send the file descriptor multiple times if necessary.
*/
private fun sendFd() { private fun sendFd() {
val fd = mInterface.fileDescriptor val fd = mInterface.fileDescriptor
val path = File(applicationContext.filesDir, "sock_path").absolutePath val path = File(applicationContext.filesDir, "sock_path").absolutePath
Log.d(packageName, path) Log.i(AppConfig.TAG, path)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
var tries = 0 var tries = 0
while (true) try { while (true) try {
Thread.sleep(50L shl tries) Thread.sleep(50L shl tries)
Log.d(packageName, "sendFd tries: $tries") Log.i(AppConfig.TAG, "sendFd tries: $tries")
LocalSocket().use { localSocket -> LocalSocket().use { localSocket ->
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM)) localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.setFileDescriptorsForSend(arrayOf(fd)) localSocket.setFileDescriptorsForSend(arrayOf(fd))
@@ -268,19 +323,17 @@ class V2RayVpnService : VpnService(), ServiceControl {
} }
break break
} catch (e: Exception) { } catch (e: Exception) {
Log.d(packageName, e.toString()) Log.e(AppConfig.TAG, "Failed to send file descriptor, try: $tries", e)
if (tries > 5) break if (tries > 5) break
tries += 1 tries += 1
} }
} }
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { /**
V2RayServiceManager.startV2rayPoint() * Stops the V2Ray service.
return START_STICKY * @param isForced Whether to force stop the service.
//return super.onStartCommand(intent, flags, startId) */
}
private fun stopV2Ray(isForced: Boolean = true) { private fun stopV2Ray(isForced: Boolean = true) {
// val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "") // val configName = defaultDPreference.getPrefString(PREF_CURR_CONFIG_GUID, "")
// val emptyInfo = VpnNetworkInfo() // val emptyInfo = VpnNetworkInfo()
@@ -296,10 +349,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
} }
try { try {
Log.d(packageName, "tun2socks destroy") Log.i(AppConfig.TAG, "tun2socks destroy")
process.destroy() process.destroy()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(packageName, e.toString()) Log.e(AppConfig.TAG, "Failed to destroy tun2socks process", e)
} }
V2RayServiceManager.stopV2rayPoint() V2RayServiceManager.stopV2rayPoint()
@@ -314,33 +367,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
try { try {
mInterface.close() mInterface.close()
} catch (ignored: Exception) { } catch (e: Exception) {
// ignored Log.e(AppConfig.TAG, "Failed to close VPN interface", e)
} }
} }
} }
override fun getService(): Service {
return this
}
override fun startService() {
setup()
}
override fun stopService() {
stopV2Ray(true)
}
override fun vpnProtect(socket: Int): Boolean {
return protect(socket)
}
@RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) {
val context = newBase?.let {
MyContextWrapper.wrap(newBase, Utils.getLocale())
}
super.attachBaseContext(context)
}
} }
@@ -6,15 +6,17 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
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.extension.toast
import com.v2ray.ang.util.SpeedtestUtil import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil import com.v2ray.ang.util.ZipUtil
import java.io.File import java.io.File
@@ -22,9 +24,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) {
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
}
} 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,12 +48,13 @@ 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) {
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
@@ -49,35 +66,35 @@ 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)
) )
) )
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
binding.layoutRestore.setOnClickListener { binding.layoutRestore.setOnClickListener {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val permission =
Manifest.permission.READ_MEDIA_IMAGES if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
} else { Manifest.permission.READ_MEDIA_IMAGES
Manifest.permission.READ_EXTERNAL_STORAGE } else {
} Manifest.permission.READ_EXTERNAL_STORAGE
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
}
} else
toast(R.string.toast_permission_denied)
} }
if (ContextCompat.checkSelfPermission(this, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED) {
try {
showFileChooser()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
}
} else {
requestPermissionLauncher.launch(permission)
}
} }
binding.layoutSoureCcode.setOnClickListener { binding.layoutSoureCcode.setOnClickListener {
@@ -88,6 +105,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)
} }
@@ -96,12 +123,12 @@ class AboutActivity : BaseActivity() {
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy) Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
} }
"v${BuildConfig.VERSION_NAME} (${SpeedtestUtil.getLibVersion()})".also { "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
binding.tvVersion.text = it binding.tvVersion.text = it
} }
} }
fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> { private fun backupConfiguration(outputZipFilePos: String): Pair<Boolean, String> {
val dateFormated = SimpleDateFormat( val dateFormated = SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss", "yyyy-MM-dd-HH-mm-ss",
Locale.getDefault() Locale.getDefault()
@@ -122,7 +149,7 @@ class AboutActivity : BaseActivity() {
} }
} }
fun restoreConfiguration(zipFile: File): Boolean { private fun restoreConfiguration(zipFile: File): Boolean {
val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}" val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}"
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) { if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
@@ -142,15 +169,15 @@ class AboutActivity : BaseActivity() {
try { try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: android.content.ActivityNotFoundException) { } catch (ex: android.content.ActivityNotFoundException) {
Log.e(AppConfig.ANG_PACKAGE, "File chooser activity not found: ${ex.message}", ex) Log.e(AppConfig.TAG, "File chooser activity not found", ex)
toast(R.string.toast_require_file_manager) toast(R.string.toast_require_file_manager)
} }
} }
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")
@@ -160,15 +187,14 @@ class AboutActivity : BaseActivity() {
} }
} }
if (restoreConfiguration(targetFile)) { if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e) Log.e(AppConfig.TAG, "Error during file restore", e)
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
} }
} }
@@ -6,10 +6,16 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.CustomDividerItemDecoration
import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
abstract class BaseActivity : AppCompatActivity() { abstract class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -34,6 +40,26 @@ abstract class BaseActivity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, Utils.getLocale())) super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, SettingsManager.getLocale()))
}
/**
* Adds a custom divider to a RecyclerView.
*
* @param recyclerView The target RecyclerView to which the divider will be added.
* @param context The context used to access resources.
* @param drawableResId The resource ID of the drawable to be used as the divider.
* @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL).
*/
fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, context: Context?, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) {
// Get the drawable from resources
val drawable = ContextCompat.getDrawable(context!!, drawableResId)
requireNotNull(drawable) { "Drawable resource not found" }
// Create a DividerItemDecoration with the specified orientation
val dividerItemDecoration = CustomDividerItemDecoration(drawable, orientation)
// Add the divider to the RecyclerView
recyclerView.addItemDecoration(dividerItemDecoration)
} }
} }
@@ -1,27 +1,32 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.util.Log
import android.os.Looper
import android.text.method.ScrollingMovementMethod
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
class LogcatActivity : BaseActivity() {
private val binding by lazy { class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
ActivityLogcatBinding.inflate(layoutInflater) private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
}
private var logsetsAll: MutableList<String> = mutableListOf()
var logsets: MutableList<String> = mutableListOf()
private val adapter by lazy { LogcatRecyclerAdapter(this) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -29,63 +34,123 @@ class LogcatActivity : BaseActivity() {
title = getString(R.string.title_logcat) title = getString(R.string.title_logcat)
logcat(false) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = adapter
binding.refreshLayout.setOnRefreshListener(this)
logsets.add(getString(R.string.pull_down_to_refresh))
} }
private fun logcat(shouldFlushLog: Boolean) { private fun getLogcat() {
binding.pbWaiting.visibility = View.VISIBLE
lifecycleScope.launch(Dispatchers.Default) { try {
try { binding.refreshLayout.isRefreshing = true
if (shouldFlushLog) {
val lst = linkedSetOf("logcat", "-c") lifecycleScope.launch(Dispatchers.Default) {
withContext(Dispatchers.IO) { val lst = LinkedHashSet<String>()
val process = Runtime.getRuntime().exec(lst.toTypedArray()) lst.add("logcat")
process.waitFor() lst.add("-d")
} lst.add("-v")
} lst.add("time")
val lst = linkedSetOf( lst.add("-s")
"logcat", "-d", "-v", "time", "-s", lst.add("GoLog,tun2socks,${ANG_PACKAGE},AndroidRuntime,System.err")
"GoLog,tun2socks,$ANG_PACKAGE,AndroidRuntime,System.err"
)
val process = withContext(Dispatchers.IO) { val process = withContext(Dispatchers.IO) {
Runtime.getRuntime().exec(lst.toTypedArray()) Runtime.getRuntime().exec(lst.toTypedArray())
} }
val allText = process.inputStream.bufferedReader().use { it.readText() }
withContext(Dispatchers.Main) { val allText = process.inputStream.bufferedReader().use { it.readLines() }.reversed()
binding.tvLogcat.text = allText launch(Dispatchers.Main) {
binding.tvLogcat.movementMethod = ScrollingMovementMethod() logsetsAll = allText.toMutableList()
binding.pbWaiting.visibility = View.GONE logsets = allText.toMutableList()
Handler(Looper.getMainLooper()).post { binding.svLogcat.fullScroll(View.FOCUS_DOWN) } refreshData()
binding.refreshLayout.isRefreshing = false
} }
} catch (e: IOException) {
withContext(Dispatchers.Main) {
binding.pbWaiting.visibility = View.GONE
toast(R.string.toast_failure)
}
e.printStackTrace()
} }
} catch (e: IOException) {
Log.e(AppConfig.TAG, "Failed to get logcat", e)
} }
} }
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()
refreshData()
}
}
} catch (e: IOException) {
Log.e(AppConfig.TAG, "Failed to clear logcat", e)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_logcat, menu) menuInflater.inflate(R.menu.menu_logcat, menu)
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) return super.onCreateOptionsMenu(menu)
} }
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.copy_all -> { R.id.copy_all -> {
Utils.setClipboard(this, binding.tvLogcat.text.toString()) Utils.setClipboard(this, logsets.joinToString("\n"))
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
true true
} }
R.id.clear_all -> { R.id.clear_all -> {
logcat(true) clearLogcat()
true true
} }
else -> super.onOptionsItemSelected(item) 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()
}
refreshData()
return true
}
override fun onRefresh() {
getLogcat()
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
adapter.notifyDataSetChanged()
}
}
@@ -0,0 +1,44 @@
package com.v2ray.ang.ui
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
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) {
try {
val log = mActivity.logsets[position]
if (log.isEmpty()) {
holder.itemSubSettingBinding.logTag.text = ""
holder.itemSubSettingBinding.logContent.text = ""
} else {
val content = log.split("):", limit = 2)
holder.itemSubSettingBinding.logTag.text = content.first().split("(", limit = 2).first().trim()
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error binding log view data", e)
}
}
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)
}
@@ -1,18 +1,18 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.content.ActivityNotFoundException import android.annotation.SuppressLint
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
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -23,17 +23,17 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
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
import com.v2ray.ang.databinding.ActivityMainBinding import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MigrateManager import com.v2ray.ang.handler.MigrateManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
@@ -41,14 +41,10 @@ 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
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.drakeet.support.toast.ToastCompat
import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
private val binding by lazy { private val binding by lazy {
@@ -81,6 +77,64 @@ 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)
@@ -89,7 +143,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.fab.setOnClickListener { binding.fab.setOnClickListener {
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
Utils.stopVService(this) V2RayServiceManager.stopVService(this)
} else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) { } else if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: VPN) == VPN) {
val intent = VpnService.prepare(this) val intent = VpnService.prepare(this)
if (intent == null) { if (intent == null) {
@@ -111,7 +165,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this) if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) {
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
} else {
binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
}
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
@@ -129,12 +188,10 @@ 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_notification)
}
} }
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
@@ -142,13 +199,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START) binding.drawerLayout.closeDrawer(GravityCompat.START)
} else { } else {
//super.onBackPressed() isEnabled = false
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
isEnabled = true
} }
} }
}) })
} }
@SuppressLint("NotifyDataSetChanged")
private fun setupViewModel() { private fun setupViewModel() {
mainViewModel.updateListAction.observe(this) { index -> mainViewModel.updateListAction.observe(this) { index ->
if (index >= 0) { if (index >= 0) {
@@ -214,23 +273,22 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.tabGroup.isVisible = true binding.tabGroup.isVisible = true
} }
fun startV2Ray() { private fun startV2Ray() {
if (MmkvManager.getSelectServer().isNullOrEmpty()) { if (MmkvManager.getSelectServer().isNullOrEmpty()) {
toast(R.string.title_file_chooser) toast(R.string.title_file_chooser)
return return
} }
V2RayServiceManager.startV2Ray(this) V2RayServiceManager.startVService(this)
} }
fun restartV2Ray() { private fun restartV2Ray() {
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
Utils.stopVService(this) V2RayServiceManager.stopVService(this)
}
lifecycleScope.launch {
delay(500)
startV2Ray()
} }
Observable.timer(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
startV2Ray()
}
} }
public override fun onResume() { public override fun onResume() {
@@ -276,6 +334,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
true true
} }
R.id.import_local -> {
importConfigLocal()
true
}
R.id.import_manually_vmess -> { R.id.import_manually_vmess -> {
importManually(EConfigType.VMESS.value) importManually(EConfigType.VMESS.value)
true true
@@ -316,55 +379,39 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
true true
} }
R.id.import_config_custom_clipboard -> { // R.id.import_config_custom_clipboard -> {
importConfigCustomClipboard() // importConfigCustomClipboard()
true // true
} // }
//
R.id.import_config_custom_local -> { // R.id.import_config_custom_local -> {
importConfigCustomLocal() // importConfigCustomLocal()
true // true
} // }
//
R.id.import_config_custom_url -> { // R.id.import_config_custom_url -> {
importConfigCustomUrlClipboard() // importConfigCustomUrlClipboard()
true // true
} // }
//
R.id.import_config_custom_url_scan -> { // R.id.import_config_custom_url_scan -> {
importQRcode(false) // importQRcode(false)
true // true
} // }
R.id.sub_update -> {
importConfigViaSub()
true
}
R.id.export_all -> { R.id.export_all -> {
binding.pbWaiting.show() exportAll()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.exportAllServer()
launch(Dispatchers.Main) {
if (ret == 0)
toast(R.string.toast_success)
else
toast(R.string.toast_failure)
binding.pbWaiting.hide()
}
}
true true
} }
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
} }
@@ -375,75 +422,31 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
R.id.del_all_config -> { R.id.del_all_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) delAllConfig()
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.removeAllServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
binding.pbWaiting.hide()
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true true
} }
R.id.del_duplicate_config -> { R.id.del_duplicate_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm) delDuplicateConfig()
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeDuplicateServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_duplicate_config_count, ret))
binding.pbWaiting.hide()
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true true
} }
R.id.del_invalid_config -> { R.id.del_invalid_config -> {
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm) delInvalidConfig()
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.removeInvalidServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
binding.pbWaiting.hide()
}
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true true
} }
R.id.sort_by_test_results -> { R.id.sort_by_test_results -> {
binding.pbWaiting.show() sortByTestResults()
lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.sortByTestResults()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
binding.pbWaiting.hide()
}
}
true true
} }
R.id.sub_update -> {
importConfigViaSub()
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -460,38 +463,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) scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
// } catch (e: Exception) { } else {
RxPermissions(this) //scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
if (forConfig)
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
else
scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
} }
// } } else {
pendingAction = Action.IMPORT_QR_CODE_CONFIG//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
*/ */
@@ -501,7 +486,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
val clipboard = Utils.getClipboard(this) val clipboard = Utils.getClipboard(this)
importBatchConfig(clipboard) importBatchConfig(clipboard)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to import config from clipboard", e)
return false return false
} }
return true return true
@@ -517,105 +502,114 @@ 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()
} }
countSub > 0 -> initGroupTab() countSub > 0 -> initGroupTab()
else -> toast(R.string.toast_failure) else -> toastError(R.string.toast_failure)
} }
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to import batch config", e)
} }
} }
} }
private fun importConfigCustomClipboard()
: Boolean {
try {
val configText = Utils.getClipboard(this)
if (TextUtils.isEmpty(configText)) {
toast(R.string.toast_none_data_clipboard)
return false
}
importCustomizeConfig(configText)
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
/** /**
* import config from local config file * import config from local config file
*/ */
private fun importConfigCustomLocal(): Boolean { private fun importConfigLocal(): Boolean {
try { try {
showFileChooser() showFileChooser()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to import config from local file", e)
return false return false
} }
return true return true
} }
private fun importConfigCustomUrlClipboard()
: Boolean { // private fun importConfigCustomClipboard()
try { // : Boolean {
val url = Utils.getClipboard(this) // try {
if (TextUtils.isEmpty(url)) { // val configText = Utils.getClipboard(this)
toast(R.string.toast_none_data_clipboard) // if (TextUtils.isEmpty(configText)) {
return false // toast(R.string.toast_none_data_clipboard)
} // return false
return importConfigCustomUrl(url) // }
} catch (e: Exception) { // importCustomizeConfig(configText)
e.printStackTrace() // return true
return false // } catch (e: Exception) {
} // e.printStackTrace()
} // return false
// }
// }
/**
* import config from local config file
*/
// private fun importConfigCustomLocal(): Boolean {
// try {
// showFileChooser()
// } catch (e: Exception) {
// e.printStackTrace()
// return false
// }
// return true
// }
//
// private fun importConfigCustomUrlClipboard()
// : Boolean {
// try {
// val url = Utils.getClipboard(this)
// if (TextUtils.isEmpty(url)) {
// toast(R.string.toast_none_data_clipboard)
// return false
// }
// return importConfigCustomUrl(url)
// } catch (e: Exception) {
// e.printStackTrace()
// return false
// }
// }
/** /**
* import config from url * import config from url
*/ */
private fun importConfigCustomUrl(url: String?): Boolean { // private fun importConfigCustomUrl(url: String?): Boolean {
try { // try {
if (!Utils.isValidUrl(url)) { // if (!Utils.isValidUrl(url)) {
toast(R.string.toast_invalid_url) // toast(R.string.toast_invalid_url)
return false // return false
} // }
lifecycleScope.launch(Dispatchers.IO) { // lifecycleScope.launch(Dispatchers.IO) {
val configText = try { // val configText = try {
Utils.getUrlContentWithCustomUserAgent(url) // HttpUtil.getUrlContentWithUserAgent(url)
} catch (e: Exception) { // } catch (e: Exception) {
e.printStackTrace() // e.printStackTrace()
"" // ""
} // }
launch(Dispatchers.Main) { // launch(Dispatchers.Main) {
importCustomizeConfig(configText) // importCustomizeConfig(configText)
} // }
} // }
} catch (e: Exception) { // } catch (e: Exception) {
e.printStackTrace() // e.printStackTrace()
return false // return false
} // }
return true // return true
} // }
/** /**
* 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,18 +617,99 @@ 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) toastError(R.string.toast_failure)
} }
//dialog.dismiss()
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
} }
return true return true
} }
private fun exportAll() {
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.exportAllServer()
launch(Dispatchers.Main) {
if (ret > 0)
toast(getString(R.string.title_export_config_count, ret))
else
toastError(R.string.toast_failure)
binding.pbWaiting.hide()
}
}
}
private fun delAllConfig() {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeAllServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_config_count, ret))
binding.pbWaiting.hide()
}
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting
}
.show()
}
private fun delDuplicateConfig() {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeDuplicateServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_duplicate_config_count, ret))
binding.pbWaiting.hide()
}
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting
}
.show()
}
private fun delInvalidConfig() {
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeInvalidServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_config_count, ret))
binding.pbWaiting.hide()
}
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting
}
.show()
}
private fun sortByTestResults() {
binding.pbWaiting.show()
lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.sortByTestResults()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
binding.pbWaiting.hide()
}
}
}
/** /**
* show file chooser * show file chooser
*/ */
@@ -643,17 +718,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,45 +741,43 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} else { } else {
Manifest.permission.READ_EXTERNAL_STORAGE Manifest.permission.READ_EXTERNAL_STORAGE
} }
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
try {
contentResolver.openInputStream(uri).use { input ->
importCustomizeConfig(input?.bufferedReader()?.readText())
}
} catch (e: Exception) {
e.printStackTrace()
}
} else
toast(R.string.toast_permission_denied)
}
}
/** if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
* import customize config try {
*/ contentResolver.openInputStream(uri).use { input ->
private fun importCustomizeConfig(server: String?) { importBatchConfig(input?.bufferedReader()?.readText())
try { }
if (server == null || TextUtils.isEmpty(server)) { } catch (e: Exception) {
toast(R.string.toast_none_data) Log.e(AppConfig.TAG, "Failed to read content from URI", e)
return
} }
if (mainViewModel.appendCustomConfigServer(server)) { } else {
mainViewModel.reloadServerList() requestPermissionLauncher.launch(permission)
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
} catch (e: Exception) {
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
e.printStackTrace()
return
} }
} }
// /**
// * import customize config
// */
// private fun importCustomizeConfig(server: String?) {
// try {
// if (server == null || TextUtils.isEmpty(server)) {
// toast(R.string.toast_none_data)
// return
// }
// if (mainViewModel.appendCustomConfigServer(server)) {
// mainViewModel.reloadServerList()
// toastSuccess(R.string.toast_success)
// } else {
// toastError(R.string.toast_failure)
// }
// //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
// } catch (e: Exception) {
// ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
// e.printStackTrace()
// return
// }
// }
private fun setTestState(content: String?) { private fun setTestState(content: String?) {
binding.tvTestState.text = content binding.tvTestState.text = content
} }
@@ -730,35 +803,21 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Handle navigation view item clicks here. // Handle navigation view item clicks here.
when (item.itemId) { when (item.itemId) {
R.id.sub_setting -> { R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java))
requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java)) R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java))
} R.id.routing_setting -> requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java))
R.id.settings -> startActivity(
Intent(this, SettingsActivity::class.java)
.putExtra("isRunning", mainViewModel.isRunning.value == true)
)
R.id.settings -> { R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
startActivity( R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
Intent(this, SettingsActivity::class.java) R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
.putExtra("isRunning", mainViewModel.isRunning.value == true)
)
}
R.id.routing_setting -> {
requestSubSettingActivity.launch(Intent(this, RoutingSettingActivity::class.java))
}
R.id.promotion -> {
Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
}
R.id.logcat -> {
startActivity(Intent(this, LogcatActivity::class.java))
}
R.id.about -> {
startActivity(Intent(this, AboutActivity::class.java))
}
} }
binding.drawerLayout.closeDrawer(GravityCompat.START) binding.drawerLayout.closeDrawer(GravityCompat.START)
return true return true
} }
} }
@@ -3,11 +3,13 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View 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
@@ -16,16 +18,17 @@ import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
import com.v2ray.ang.databinding.ItemRecyclerMainBinding import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils import kotlinx.coroutines.delay
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import kotlinx.coroutines.launch
import io.reactivex.rxjava3.core.Observable
import java.util.concurrent.TimeUnit
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter { class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
companion object { companion object {
@@ -37,169 +40,273 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
private val share_method: Array<out String> by lazy { private val share_method: Array<out String> by lazy {
mActivity.resources.getStringArray(R.array.share_method) mActivity.resources.getStringArray(R.array.share_method)
} }
private val share_method_more: Array<out String> by lazy {
mActivity.resources.getStringArray(R.array.share_method_more)
}
var isRunning = false var isRunning = false
private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
/**
* Gets the total number of items in the adapter (servers count + footer view)
* @return The total item count
*/
override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1 override fun getItemCount() = mActivity.mainViewModel.serversCache.size + 1
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
if (holder is MainViewHolder) { if (holder is MainViewHolder) {
val guid = mActivity.mainViewModel.serversCache[position].guid val guid = mActivity.mainViewModel.serversCache[position].guid
val profile = mActivity.mainViewModel.serversCache[position].profile val profile = mActivity.mainViewModel.serversCache[position].profile
// //filter val isCustom = profile.configType == EConfigType.CUSTOM
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
// ) {
// holder.itemMainBinding.cardView.visibility = View.GONE
// } else {
// holder.itemMainBinding.cardView.visibility = View.VISIBLE
// }
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
holder.itemMainBinding.tvName.text = profile.remarks
holder.itemView.setBackgroundColor(Color.TRANSPARENT) holder.itemView.setBackgroundColor(Color.TRANSPARENT)
//Name address
holder.itemMainBinding.tvName.text = profile.remarks
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
holder.itemMainBinding.tvType.text = profile.configType.name
//TestResult
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty() holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
if ((aff?.testDelayMillis ?: 0L) < 0L) { if ((aff?.testDelayMillis ?: 0L) < 0L) {
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed)) holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
} else { } else {
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing)) holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
} }
//layoutIndicator
if (guid == MmkvManager.getSelectServer()) { if (guid == MmkvManager.getSelectServer()) {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent) holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
} else { } else {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0) holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
} }
holder.itemMainBinding.tvSubscription.text = MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks ?: ""
var shareOptions = share_method.asList() //subscription remarks
when (profile.configType) { val subRemarks = getSubscriptionRemarks(profile)
EConfigType.CUSTOM -> { holder.itemMainBinding.tvSubscription.text = subRemarks
holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config) holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE
shareOptions = shareOptions.takeLast(1)
//layout
if (doubleColumnDisplay) {
holder.itemMainBinding.layoutShare.visibility = View.GONE
holder.itemMainBinding.layoutEdit.visibility = View.GONE
holder.itemMainBinding.layoutRemove.visibility = View.GONE
holder.itemMainBinding.layoutMore.visibility = View.VISIBLE
//share method
val shareOptions = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
holder.itemMainBinding.layoutMore.setOnClickListener {
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
}
} else {
holder.itemMainBinding.layoutShare.visibility = View.VISIBLE
holder.itemMainBinding.layoutEdit.visibility = View.VISIBLE
holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE
holder.itemMainBinding.layoutMore.visibility = View.GONE
//share method
val shareOptions = if (isCustom) share_method.asList().takeLast(1) else share_method.asList()
holder.itemMainBinding.layoutShare.setOnClickListener {
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
} }
else -> { holder.itemMainBinding.layoutEdit.setOnClickListener {
holder.itemMainBinding.tvType.text = profile.configType.name editServer(guid, profile)
} }
} holder.itemMainBinding.layoutRemove.setOnClickListener {
removeServer(guid, position)
// 隐藏主页服务器地址为xxx:xxx:***/xxx.xxx.xxx.***
val strState = "${
profile.server?.let {
if (it.contains(":"))
it.split(":").take(2).joinToString(":", postfix = ":***")
else
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
}
} : ${profile.serverPort}"
holder.itemMainBinding.tvStatistics.text = strState
holder.itemMainBinding.layoutShare.setOnClickListener {
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
try {
when (i) {
0 -> {
if (profile.configType == EConfigType.CUSTOM) {
shareFullContent(guid)
} else {
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
}
}
1 -> {
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
mActivity.toast(R.string.toast_success)
} else {
mActivity.toast(R.string.toast_failure)
}
}
2 -> shareFullContent(guid)
else -> mActivity.toast("else")
}
} catch (e: Exception) {
e.printStackTrace()
}
}.show()
}
holder.itemMainBinding.layoutEdit.setOnClickListener {
val intent = Intent().putExtra("guid", guid)
.putExtra("isRunning", isRunning)
.putExtra("createConfigType", profile.configType.value)
if (profile.configType == EConfigType.CUSTOM) {
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
} else {
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
}
}
holder.itemMainBinding.layoutRemove.setOnClickListener {
if (guid != MmkvManager.getSelectServer()) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
removeServer(guid, position)
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
} else {
removeServer(guid, position)
}
} else {
application.toast(R.string.toast_action_not_allowed)
} }
} }
holder.itemMainBinding.infoContainer.setOnClickListener { holder.itemMainBinding.infoContainer.setOnClickListener {
val selected = MmkvManager.getSelectServer() setSelectServer(guid)
if (guid != selected) {
MmkvManager.setSelectServer(guid)
if (!TextUtils.isEmpty(selected)) {
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
}
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) {
Utils.stopVService(mActivity)
Observable.timer(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
V2RayServiceManager.startV2Ray(mActivity)
}
}
}
} }
} }
if (holder is FooterViewHolder) { // if (holder is FooterViewHolder) {
//if (activity?.defaultDPreference?.getPrefBoolean(AppConfig.PREF_INAPP_BUY_IS_PREMIUM, false)) { // if (true) {
if (true) { // holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE // } else {
} else { // holder.itemFooterBinding.layoutEdit.setOnClickListener {
holder.itemFooterBinding.layoutEdit.setOnClickListener { // Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}") // }
} // }
// }
}
/**
* Gets the server address information
* Hides part of IP or domain information for privacy protection
* @param profile The server configuration
* @return Formatted address string
*/
private fun getAddress(profile: ProfileItem): String {
// Hide xxx:xxx:***/xxx.xxx.xxx.***
return "${
profile.server?.let {
if (it.contains(":"))
it.split(":").take(2).joinToString(":", postfix = ":***")
else
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
} }
} : ${profile.serverPort}"
}
/**
* Gets the subscription remarks information
* @param profile The server configuration
* @return Subscription remarks string, or empty string if none
*/
private fun getSubscriptionRemarks(profile: ProfileItem): String {
val subRemarks =
if (mActivity.mainViewModel.subscriptionId.isEmpty())
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
else
null
return subRemarks?.toString() ?: ""
}
/**
* Shares server configuration
* Displays a dialog with sharing options and executes the selected action
* @param guid The server unique identifier
* @param profile The server configuration
* @param position The position in the list
* @param shareOptions The list of share options
* @param skip The number of options to skip
*/
private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List<String>, skip: Int) {
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
try {
when (i + skip) {
0 -> showQRCode(guid)
1 -> share2Clipboard(guid)
2 -> shareFullContent(guid)
3 -> editServer(guid, profile)
4 -> removeServer(guid, position)
else -> mActivity.toast("else")
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error when sharing server", e)
}
}.show()
}
/**
* Displays QR code for the server configuration
* @param guid The server unique identifier
*/
private fun showQRCode(guid: String) {
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
}
/**
* Shares server configuration to clipboard
* @param guid The server unique identifier
*/
private fun share2Clipboard(guid: String) {
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
mActivity.toastSuccess(R.string.toast_success)
} else {
mActivity.toastError(R.string.toast_failure)
} }
} }
/**
* Shares full server configuration content to clipboard
* @param guid The server unique identifier
*/
private fun shareFullContent(guid: String) { private fun shareFullContent(guid: String) {
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) { if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
mActivity.toast(R.string.toast_success) mActivity.toastSuccess(R.string.toast_success)
} else { } else {
mActivity.toast(R.string.toast_failure) mActivity.toastError(R.string.toast_failure)
} }
} }
/**
* Edits server configuration
* Opens appropriate editing interface based on configuration type
* @param guid The server unique identifier
* @param profile The server configuration
*/
private fun editServer(guid: String, profile: ProfileItem) {
val intent = Intent().putExtra("guid", guid)
.putExtra("isRunning", isRunning)
.putExtra("createConfigType", profile.configType.value)
if (profile.configType == EConfigType.CUSTOM) {
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
} else {
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
}
}
/**
* Removes server configuration
* Handles confirmation dialog and related checks
* @param guid The server unique identifier
* @param position The position in the list
*/
private fun removeServer(guid: String, position: Int) { private fun removeServer(guid: String, position: Int) {
if (guid != MmkvManager.getSelectServer()) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
removeServerSub(guid, position)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting
}
.show()
} else {
removeServerSub(guid, position)
}
} else {
application.toast(R.string.toast_action_not_allowed)
}
}
/**
* Executes the actual server removal process
* @param guid The server unique identifier
* @param position The position in the list
*/
private fun removeServerSub(guid: String, position: Int) {
mActivity.mainViewModel.removeServer(guid) mActivity.mainViewModel.removeServer(guid)
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size) notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
} }
/**
* Sets the selected server
* Updates UI and restarts service if needed
* @param guid The server unique identifier to select
*/
private fun setSelectServer(guid: String) {
val selected = MmkvManager.getSelectServer()
if (guid != selected) {
MmkvManager.setSelectServer(guid)
if (!TextUtils.isEmpty(selected)) {
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
}
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) {
V2RayServiceManager.stopVService(mActivity)
mActivity.lifecycleScope.launch {
try {
delay(500)
V2RayServiceManager.startVService(mActivity)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e)
}
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
return when (viewType) { return when (viewType) {
VIEW_TYPE_ITEM -> VIEW_TYPE_ITEM ->
@@ -246,4 +353,4 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
override fun onItemDismiss(position: Int) { override fun onItemDismiss(position: Int) {
} }
} }
@@ -1,29 +1,29 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBypassListBinding import com.v2ray.ang.databinding.ActivityBypassListBinding
import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.extension.v2RayApplication import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.AppManagerUtil import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.HttpUtil
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() {
@@ -31,6 +31,7 @@ class PerAppProxyActivity : BaseActivity() {
ActivityBypassListBinding.inflate(layoutInflater) ActivityBypassListBinding.inflate(layoutInflater)
} }
private var adapter: PerAppProxyAdapter? = null private var adapter: PerAppProxyAdapter? = null
private var appsAll: List<AppInfo>? = null private var appsAll: List<AppInfo>? = null
@@ -38,98 +39,43 @@ class PerAppProxyActivity : BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
val dividerItemDecoration = DividerItemDecoration(this, LinearLayoutManager.VERTICAL) title = getString(R.string.per_app_proxy_settings)
binding.recyclerView.addItemDecoration(dividerItemDecoration)
val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET) addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
AppManagerUtil.rxLoadNetworkAppList(this) lifecycleScope.launch {
.subscribeOn(Schedulers.io()) try {
.map { binding.pbWaiting.show()
if (blacklist != null) { val blacklist = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
it.forEach { one -> val apps = withContext(Dispatchers.IO) {
if (blacklist.contains(one.packageName)) { val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
one.isSelected = 1
} else { if (blacklist != null) {
one.isSelected = 0 appsList.forEach { app ->
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
} }
} appsList.sortedWith { 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 }
} }
} } else {
it.sortedWith(comparator)
} 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 { appsAll = apps
// val comparator = object : Comparator<AppInfo> { adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
// val collator = Collator.getInstance()
// 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.hide()
} catch (e: Exception) {
binding.pbWaiting.hide()
Log.e(ANG_PACKAGE, "Error loading apps", e)
} }
/***
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
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 +86,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() {
@@ -199,8 +115,10 @@ class PerAppProxyActivity : BaseActivity() {
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
@SuppressLint("NotifyDataSetChanged")
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.select_all -> adapter?.let { R.id.select_all -> adapter?.let { it ->
val pkgNames = it.apps.map { it.packageName } val pkgNames = it.apps.map { it.packageName }
if (it.blacklist.containsAll(pkgNames)) { if (it.blacklist.containsAll(pkgNames)) {
it.apps.forEach { it.apps.forEach {
@@ -237,13 +155,20 @@ class PerAppProxyActivity : BaseActivity() {
private fun selectProxyApp() { private fun selectProxyApp() {
toast(R.string.msg_downloading_content) toast(R.string.msg_downloading_content)
binding.pbWaiting.show()
val url = AppConfig.androidpackagenamelistUrl val url = AppConfig.androidpackagenamelistUrl
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val content = Utils.getUrlContext(url, 5000) var content = HttpUtil.getUrlContent(url, 5000)
if (content.isNullOrEmpty()) {
val httpPort = SettingsManager.getHttpPort()
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
}
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
Log.d(ANG_PACKAGE, content) Log.i(AppConfig.TAG, content)
selectProxyApp(content, true) selectProxyApp(content, true)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
binding.pbWaiting.hide()
} }
} }
} }
@@ -252,7 +177,7 @@ class PerAppProxyActivity : BaseActivity() {
val content = Utils.getClipboard(applicationContext) val content = Utils.getClipboard(applicationContext)
if (TextUtils.isEmpty(content)) return if (TextUtils.isEmpty(content)) return
selectProxyApp(content, false) selectProxyApp(content, false)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} }
private fun exportProxyApp() { private fun exportProxyApp() {
@@ -262,9 +187,10 @@ class PerAppProxyActivity : BaseActivity() {
lst = lst + System.getProperty("line.separator") + it lst = lst + System.getProperty("line.separator") + it
} }
Utils.setClipboard(applicationContext, lst) Utils.setClipboard(applicationContext, lst)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} }
@SuppressLint("NotifyDataSetChanged")
private fun selectProxyApp(content: String, force: Boolean): Boolean { private fun selectProxyApp(content: String, force: Boolean): Boolean {
try { try {
val proxyApps = if (TextUtils.isEmpty(content)) { val proxyApps = if (TextUtils.isEmpty(content)) {
@@ -277,10 +203,10 @@ class PerAppProxyActivity : BaseActivity() {
adapter?.blacklist?.clear() adapter?.blacklist?.clear()
if (binding.switchBypassApps.isChecked) { if (binding.switchBypassApps.isChecked) {
adapter?.let { adapter?.let { it ->
it.apps.forEach block@{ it.apps.forEach block@{
val packageName = it.packageName val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName) Log.i(AppConfig.TAG, packageName)
if (!inProxyApps(proxyApps, packageName, force)) { if (!inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist?.add(packageName) adapter?.blacklist?.add(packageName)
println(packageName) println(packageName)
@@ -290,10 +216,10 @@ class PerAppProxyActivity : BaseActivity() {
it.notifyDataSetChanged() it.notifyDataSetChanged()
} }
} else { } else {
adapter?.let { adapter?.let { it ->
it.apps.forEach block@{ it.apps.forEach block@{
val packageName = it.packageName val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName) Log.i(AppConfig.TAG, packageName)
if (inProxyApps(proxyApps, packageName, force)) { if (inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist?.add(packageName) adapter?.blacklist?.add(packageName)
println(packageName) println(packageName)
@@ -304,7 +230,7 @@ class PerAppProxyActivity : BaseActivity() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Error selecting proxy app", e)
return false return false
} }
return true return true
@@ -339,7 +265,12 @@ class PerAppProxyActivity : BaseActivity() {
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist) adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
adapter?.notifyDataSetChanged() refreshData()
return true return true
} }
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
adapter?.notifyDataSetChanged()
}
} }
@@ -4,7 +4,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.dto.AppInfo
@@ -35,7 +34,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
val view = View(ctx) val view = View(ctx)
view.layoutParams = ViewGroup.LayoutParams( view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ctx.resources.getDimensionPixelSize(R.dimen.bypass_list_header_height) * 0 0
) )
BaseViewHolder(view) BaseViewHolder(view)
} }
@@ -9,6 +9,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingEditBinding import com.v2ray.ang.databinding.ActivityRoutingEditBinding
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -37,7 +38,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 +61,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() }
@@ -78,7 +79,7 @@ class RoutingEditActivity : BaseActivity() {
} }
SettingsManager.saveRoutingRuleset(position, rulesetItem) SettingsManager.saveRoutingRuleset(position, rulesetItem)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
finish() finish()
return true return true
} }
@@ -95,7 +96,7 @@ class RoutingEditActivity : BaseActivity() {
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -1,8 +1,10 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -12,12 +14,13 @@ import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.tbruyelle.rxpermissions3.RxPermissions
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -40,6 +43,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)
@@ -48,6 +61,7 @@ class RoutingSettingActivity : BaseActivity() {
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
@@ -75,97 +89,74 @@ class RoutingSettingActivity : BaseActivity() {
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.add_rule -> { R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true }
startActivity(Intent(this, RoutingEditActivity::class.java)) R.id.import_predefined_rulesets -> importPredefined().let { true }
true R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true }
} R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true }
R.id.export_rulesets_to_clipboard -> export2Clipboard().let { true }
else -> super.onOptionsItemSelected(item)
}
R.id.user_asset_setting -> { private fun importPredefined() {
startActivity(Intent(this, UserAssetActivity::class.java)) AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
true
}
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 -> try {
try { lifecycleScope.launch(Dispatchers.IO) {
lifecycleScope.launch(Dispatchers.IO) { SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i) launch(Dispatchers.Main) {
launch(Dispatchers.Main) {
refreshData()
toast(R.string.toast_success)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}.show()
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true
}
R.id.import_rulesets_from_clipboard -> {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ ->
val clipboard = try {
Utils.getClipboard(this)
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.toast_failure)
return@setPositiveButton
}
lifecycleScope.launch(Dispatchers.IO) {
val result = SettingsManager.resetRoutingRulesets(clipboard)
withContext(Dispatchers.Main) {
if (result) {
refreshData() refreshData()
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
} }
} }
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do nothing //do nothing
} }
.show() .show()
true }.show()
} }
R.id.import_rulesets_from_qrcode -> { private fun importFromClipboard() {
RxPermissions(this) AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.request(Manifest.permission.CAMERA) .setPositiveButton(android.R.string.ok) { _, _ ->
.subscribe { val clipboard = try {
if (it) Utils.getClipboard(this)
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java)) } catch (e: Exception) {
else Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
toast(R.string.toast_permission_denied) toastError(R.string.toast_failure)
return@setPositiveButton
}
lifecycleScope.launch(Dispatchers.IO) {
val result = SettingsManager.resetRoutingRulesets(clipboard)
withContext(Dispatchers.Main) {
if (result) {
refreshData()
toastSuccess(R.string.toast_success)
} else {
toastError(R.string.toast_failure)
}
}
} }
true
}
R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) {
toast(R.string.toast_failure)
} else {
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
toast(R.string.toast_success)
} }
true .setNegativeButton(android.R.string.cancel) { _, _ ->
} //do nothing
}
.show()
}
else -> super.onOptionsItemSelected(item) private fun export2Clipboard() {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) {
toastError(R.string.toast_failure)
} else {
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
toastSuccess(R.string.toast_success)
}
} }
private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val scanQRcodeForRulesets = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -182,24 +173,24 @@ class RoutingSettingActivity : BaseActivity() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (result) { if (result) {
refreshData() refreshData()
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
//do nothing //do nothing
} }
.show() .show()
return true return true
} }
@SuppressLint("NotifyDataSetChanged")
fun refreshData() { fun refreshData() {
rulesets.clear() rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf()) rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
}
}
@@ -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 {
@@ -4,47 +4,49 @@ 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.extension.toastError
import com.v2ray.ang.extension.toastSuccess
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)
importQRcode() importQRcode()
} }
fun importQRcode(): Boolean { private 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()
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false) val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
if (count + countSub > 0) { if (count + countSub > 0) {
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
} }
finish() finish()
} }
}
}
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.service.V2RayServiceManager import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils
class ScSwitchActivity : BaseActivity() { class ScSwitchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -12,10 +11,10 @@ class ScSwitchActivity : BaseActivity() {
setContentView(R.layout.activity_none) setContentView(R.layout.activity_none)
if (V2RayServiceManager.v2rayPoint.isRunning) { if (V2RayServiceManager.isRunning()) {
Utils.stopVService(this) V2RayServiceManager.stopVService(this)
} else { } else {
Utils.startVServiceFromToggle(this) V2RayServiceManager.startVServiceFromToggle(this)
} }
finish() finish()
} }
@@ -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.util.Log
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.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
@@ -20,7 +22,39 @@ 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) {
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
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,15 +106,12 @@ 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 -> showFileChooser()
if (granted) { } else {
showFileChooser() requestPermissionLauncher.launch(permission)
} else { }
toast(R.string.toast_permission_denied)
}
}
true true
} }
@@ -100,26 +131,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)
}
}
}
}
@@ -14,7 +14,6 @@ import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_PORT import com.v2ray.ang.AppConfig.DEFAULT_PORT
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.REALITY import com.v2ray.ang.AppConfig.REALITY
@@ -28,6 +27,7 @@ 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.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -126,6 +126,8 @@ class ServerActivity : BaseActivity() {
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) } private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) } private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) } private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
private val et_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 et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) } private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
@@ -162,7 +164,7 @@ class ServerActivity : BaseActivity() {
sp_header_type_title?.text = sp_header_type_title?.text =
when (networks[position]) { when (networks[position]) {
NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type) NetworkType.GRPC.type -> getString(R.string.server_lab_mode_type)
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode) NetworkType.XHTTP.type -> getString(R.string.server_lab_xhttp_mode)
else -> getString(R.string.server_lab_head_type) else -> getString(R.string.server_lab_head_type)
}.orEmpty() }.orEmpty()
sp_header_type?.setSelection( sp_header_type?.setSelection(
@@ -170,7 +172,7 @@ class ServerActivity : BaseActivity() {
types, types,
when (networks[position]) { when (networks[position]) {
NetworkType.GRPC.type -> config?.mode NetworkType.GRPC.type -> config?.mode
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> config?.xhttpMode NetworkType.XHTTP.type -> config?.xhttpMode
else -> config?.headerType else -> config?.headerType
}.orEmpty() }.orEmpty()
) )
@@ -198,7 +200,7 @@ class ServerActivity : BaseActivity() {
NetworkType.TCP.type -> R.string.server_lab_request_host_http NetworkType.TCP.type -> R.string.server_lab_request_host_http
NetworkType.WS.type -> R.string.server_lab_request_host_ws NetworkType.WS.type -> R.string.server_lab_request_host_ws
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_request_host_httpupgrade
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp NetworkType.XHTTP.type -> R.string.server_lab_request_host_xhttp
NetworkType.H2.type -> 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
NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc
@@ -213,7 +215,7 @@ class ServerActivity : BaseActivity() {
NetworkType.KCP.type -> R.string.server_lab_path_kcp NetworkType.KCP.type -> R.string.server_lab_path_kcp
NetworkType.WS.type -> R.string.server_lab_path_ws NetworkType.WS.type -> R.string.server_lab_path_ws
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
NetworkType.H2.type -> 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
NetworkType.GRPC.type -> R.string.server_lab_path_grpc NetworkType.GRPC.type -> R.string.server_lab_path_grpc
@@ -223,14 +225,14 @@ class ServerActivity : BaseActivity() {
) )
et_extra?.text = Utils.getEditable( et_extra?.text = Utils.getEditable(
when (networks[position]) { when (networks[position]) {
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> config?.xhttpExtra NetworkType.XHTTP.type -> config?.xhttpExtra
else -> null else -> null
}.orEmpty() }.orEmpty()
) )
layout_extra?.visibility = layout_extra?.visibility =
when (networks[position]) { when (networks[position]) {
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> View.VISIBLE NetworkType.XHTTP.type -> View.VISIBLE
else -> View.GONE else -> View.GONE
} }
} }
@@ -334,6 +336,8 @@ class ServerActivity : BaseActivity() {
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
@@ -350,13 +354,13 @@ class ServerActivity : BaseActivity() {
container_alpn?.visibility = View.VISIBLE container_alpn?.visibility = View.VISIBLE
et_sni?.text = Utils.getEditable(config.sni) et_sni?.text = Utils.getEditable(config.sni)
config.fingerPrint?.let { config.fingerPrint?.let { it ->
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 { it ->
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
@@ -476,9 +480,9 @@ 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.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
finish() finish()
return true return true
} }
@@ -513,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()
} }
} }
@@ -578,7 +584,7 @@ class ServerActivity : BaseActivity() {
grpcModes grpcModes
} }
NetworkType.SPLIT_HTTP.type, NetworkType.XHTTP.type -> { NetworkType.XHTTP.type -> {
xhttpMode xhttpMode
} }
@@ -600,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()
@@ -2,21 +2,22 @@ package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.utils.EditorTheme import com.blacksquircle.ui.editorkit.utils.EditorTheme
import com.blacksquircle.ui.language.json.JsonLanguage import com.blacksquircle.ui.language.json.JsonLanguage
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat
class ServerCustomConfigActivity : BaseActivity() { class ServerCustomConfigActivity : BaseActivity() {
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) } private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
@@ -77,8 +78,8 @@ class ServerCustomConfigActivity : BaseActivity() {
val profileItem = try { val profileItem = try {
CustomFmt.parse(binding.editor.text.toString()) CustomFmt.parse(binding.editor.text.toString())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to parse custom configuration", e)
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show() toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
return false return false
} }
@@ -91,7 +92,7 @@ class ServerCustomConfigActivity : BaseActivity() {
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
finish() finish()
return true return true
} }
@@ -106,7 +107,7 @@ class ServerCustomConfigActivity : BaseActivity() {
MmkvManager.removeServer(editGuid) MmkvManager.removeServer(editGuid)
finish() finish()
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -40,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
@@ -235,6 +238,7 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_SPEED_ENABLED, AppConfig.PREF_SPEED_ENABLED,
AppConfig.PREF_CONFIRM_REMOVE, AppConfig.PREF_CONFIRM_REMOVE,
AppConfig.PREF_START_SCAN_IMMEDIATE, AppConfig.PREF_START_SCAN_IMMEDIATE,
AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
AppConfig.PREF_PREFER_IPV6, AppConfig.PREF_PREFER_IPV6,
AppConfig.PREF_PROXY_SHARING, AppConfig.PREF_PROXY_SHARING,
AppConfig.PREF_ALLOW_INSECURE AppConfig.PREF_ALLOW_INSECURE
@@ -244,6 +248,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 +269,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(
@@ -10,6 +10,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -18,8 +19,8 @@ import kotlinx.coroutines.launch
class SubEditActivity : BaseActivity() { class SubEditActivity : BaseActivity() {
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) } private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null private var del_config: MenuItem? = null
var save_config: MenuItem? = null private var save_config: MenuItem? = null
private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() } private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() }
@@ -37,7 +38,7 @@ class SubEditActivity : BaseActivity() {
} }
/** /**
* bingding seleced server config * binding selected server config
*/ */
private fun bindingServer(subItem: SubscriptionItem): Boolean { private fun bindingServer(subItem: SubscriptionItem): Boolean {
binding.etRemarks.text = Utils.getEditable(subItem.remarks) binding.etRemarks.text = Utils.getEditable(subItem.remarks)
@@ -94,7 +95,7 @@ class SubEditActivity : BaseActivity() {
} }
MmkvManager.encodeSubscription(editSubId, subItem) MmkvManager.encodeSubscription(editSubId, subItem)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
finish() finish()
return true return true
} }
@@ -113,7 +114,7 @@ class SubEditActivity : BaseActivity() {
} }
} }
} }
.setNegativeButton(android.R.string.no) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing // do nothing
} }
.show() .show()
@@ -1,18 +1,18 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.annotation.SuppressLint
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 androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubSettingBinding import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -35,6 +35,7 @@ class SubSettingActivity : BaseActivity() {
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
@@ -58,21 +59,18 @@ class SubSettingActivity : BaseActivity() {
} }
R.id.sub_update -> { R.id.sub_update -> {
val dialog = AlertDialog.Builder(this) binding.pbWaiting.show()
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val count = AngConfigManager.updateConfigViaSubAll() val count = AngConfigManager.updateConfigViaSubAll()
delay(500L) delay(500L)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (count > 0) { if (count > 0) {
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
dialog.dismiss() binding.pbWaiting.hide()
} }
} }
@@ -83,6 +81,7 @@ class SubSettingActivity : BaseActivity() {
} }
@SuppressLint("NotifyDataSetChanged")
fun refreshData() { fun refreshData() {
subscriptions = MmkvManager.decodeSubscriptions() subscriptions = MmkvManager.decodeSubscriptions()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
@@ -3,11 +3,13 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemQrcodeBinding import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
@@ -53,7 +55,10 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
if (TextUtils.isEmpty(subItem.url)) { if (TextUtils.isEmpty(subItem.url)) {
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
} else { } else {
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
holder.itemSubSettingBinding.layoutShare.setOnClickListener { holder.itemSubSettingBinding.layoutShare.setOnClickListener {
AlertDialog.Builder(mActivity) AlertDialog.Builder(mActivity)
.setItems(share_method.asList().toTypedArray()) { _, i -> .setItems(share_method.asList().toTypedArray()) { _, i ->
@@ -78,7 +83,7 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
else -> mActivity.toast("else") else -> mActivity.toast("else")
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Share subscription failed", e)
} }
}.show() }.show()
} }
@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -28,7 +29,7 @@ class TaskerActivity : BaseActivity() {
lstData.add("Default") lstData.add("Default")
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
MmkvManager.decodeServerList()?.forEach { key -> MmkvManager.decodeServerList().forEach { key ->
MmkvManager.decodeServerConfig(key)?.let { config -> MmkvManager.decodeServerConfig(key)?.let { config ->
lstData.add(config.remarks) lstData.add(config.remarks)
lstGuid.add(key) lstGuid.add(key)
@@ -60,7 +61,7 @@ class TaskerActivity : BaseActivity() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e)
} }
} }
@@ -4,10 +4,16 @@ 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.AppConfig
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.extension.toastError
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLDecoder import java.net.URLDecoder
class UrlSchemeActivity : BaseActivity() { class UrlSchemeActivity : BaseActivity() {
@@ -40,7 +46,7 @@ class UrlSchemeActivity : BaseActivity() {
} }
else -> { else -> {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
} }
@@ -49,7 +55,7 @@ class UrlSchemeActivity : BaseActivity() {
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
finish() finish()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Error processing URL scheme", e)
} }
} }
@@ -57,7 +63,7 @@ class UrlSchemeActivity : BaseActivity() {
if (uriString.isNullOrEmpty()) { if (uriString.isNullOrEmpty()) {
return return
} }
Log.d("UrlScheme", uriString) Log.i(AppConfig.TAG, uriString)
var decodedUrl = URLDecoder.decode(uriString, "UTF-8") var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
val uri = Uri.parse(decodedUrl) val uri = Uri.parse(decodedUrl)
@@ -65,12 +71,16 @@ class UrlSchemeActivity : BaseActivity() {
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) { if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
decodedUrl += "#${fragment}" decodedUrl += "#${fragment}"
} }
Log.d("UrlScheme-decodedUrl", decodedUrl) Log.i(AppConfig.TAG, decodedUrl)
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false) lifecycleScope.launch(Dispatchers.IO) {
if (count + countSub > 0) { val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
toast(R.string.import_subscription_success) withContext(Dispatchers.Main) {
} else { if (count + countSub > 0) {
toast(R.string.import_subscription_failure) toast(R.string.import_subscription_success)
} else {
toast(R.string.import_subscription_failure)
}
}
} }
} }
} }
@@ -19,18 +19,18 @@ 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.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubSettingBinding import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toTrafficString import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -38,18 +38,48 @@ import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
import java.text.DateFormat import java.text.DateFormat
import java.util.Date import java.util.Date
class UserAssetActivity : BaseActivity() { class UserAssetActivity : BaseActivity() {
private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) } private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
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)
@@ -58,12 +88,13 @@ class UserAssetActivity : BaseActivity() {
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = UserAssetAdapter() binding.recyclerView.adapter = UserAssetAdapter()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.recyclerView.adapter?.notifyDataSetChanged() refreshData()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -86,30 +117,10 @@ 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 -> private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data val uri = result.data?.data
if (result.resultCode == RESULT_OK && uri != null) { if (result.resultCode == RESULT_OK && uri != null) {
val assetId = Utils.getUuid() val assetId = Utils.getUuid()
@@ -127,7 +138,7 @@ class UserAssetActivity : BaseActivity() {
copyFile(uri) copyFile(uri)
} }
}.onFailure { }.onFailure {
toast(R.string.toast_asset_copy_failed) toastError(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId) MmkvManager.removeAssetUrl(assetId)
} }
} }
@@ -138,8 +149,8 @@ class UserAssetActivity : BaseActivity() {
contentResolver.openInputStream(uri).use { inputStream -> contentResolver.openInputStream(uri).use { inputStream ->
targetFile.outputStream().use { fileOut -> targetFile.outputStream().use { fileOut ->
inputStream?.copyTo(fileOut) inputStream?.copyTo(fileOut)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
binding.recyclerView.adapter?.notifyDataSetChanged() refreshData()
} }
} }
return targetFile.path return targetFile.path
@@ -153,19 +164,12 @@ class UserAssetActivity : BaseActivity() {
}.also { cursor.close() } }.also { cursor.close() }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to get cursor name", e)
null null
} }
private fun importAssetFromQRcode(): Boolean { private fun importAssetFromQRcode(): Boolean {
RxPermissions(this) requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
.request(Manifest.permission.CAMERA)
.subscribe {
if (it)
scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java))
else
toast(R.string.toast_permission_denied)
}
return true return true
} }
@@ -182,65 +186,58 @@ class UserAssetActivity : BaseActivity() {
return false return false
} }
// Send URL to UserAssetUrlActivity for Processing // Send URL to UserAssetUrlActivity for Processing
startActivity(Intent(this, UserAssetUrlActivity::class.java) startActivity(
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)) Intent(this, UserAssetUrlActivity::class.java)
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to import asset from URL", e)
return false return false
} }
return true return true
} }
private fun downloadGeoFiles() { private fun downloadGeoFiles() {
val dialog = AlertDialog.Builder(this) binding.pbWaiting.show()
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
toast(R.string.msg_downloading_content) toast(R.string.msg_downloading_content)
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
var assets = MmkvManager.decodeAssetUrls() var assets = MmkvManager.decodeAssetUrls()
assets = addBuiltInGeoItems(assets) assets = addBuiltInGeoItems(assets)
assets.forEach { var resultCount = 0
//toast(getString(R.string.msg_downloading_content) + it) lifecycleScope.launch(Dispatchers.IO) {
lifecycleScope.launch(Dispatchers.IO) { assets.forEach {
var result = downloadGeo(it.second, 60000, httpPort) try {
if (!result) { var result = downloadGeo(it.second, 15000, httpPort)
result = downloadGeo(it.second, 60000, 0) if (!result) {
} result = downloadGeo(it.second, 15000, 0)
launch(Dispatchers.Main) {
if (result) {
toast(getString(R.string.toast_success) + " " + it.second.remarks)
binding.recyclerView.adapter?.notifyDataSetChanged()
} else {
toast(getString(R.string.toast_failure) + " " + it.second.remarks)
} }
dialog.dismiss() if (result)
resultCount++
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
} }
} }
withContext(Dispatchers.Main) {
if (resultCount > 0) {
toast(getString(R.string.title_update_config_count, resultCount))
refreshData()
} else {
toast(getString(R.string.toast_failure))
}
binding.pbWaiting.hide()
}
} }
} }
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean { private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
val targetTemp = File(extDir, item.remarks + "_temp") val targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks) val target = File(extDir, item.remarks)
var conn: HttpURLConnection? = null Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
//Log.d(AppConfig.ANG_PACKAGE, url)
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
try { try {
conn = if (httpPort == 0) {
URL(item.url).openConnection() as HttpURLConnection
} else {
URL(item.url).openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress(LOOPBACK, httpPort)
)
) as HttpURLConnection
}
conn.connectTimeout = timeout
conn.readTimeout = timeout
val inputStream = conn.inputStream val inputStream = conn.inputStream
val responseCode = conn.responseCode val responseCode = conn.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
@@ -252,10 +249,10 @@ class UserAssetActivity : BaseActivity() {
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, Log.getStackTraceString(e)) Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
return false return false
} finally { } finally {
conn?.disconnect() conn.disconnect()
} }
} }
@@ -279,11 +276,16 @@ class UserAssetActivity : BaseActivity() {
lifecycleScope.launch(Dispatchers.Default) { lifecycleScope.launch(Dispatchers.Default) {
SettingsManager.initAssets(this@UserAssetActivity, assets) SettingsManager.initAssets(this@UserAssetActivity, assets)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.recyclerView.adapter?.notifyDataSetChanged() refreshData()
} }
} }
} }
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
binding.recyclerView.adapter?.notifyDataSetChanged()
}
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() { inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
return UserAssetViewHolder( return UserAssetViewHolder(
@@ -333,7 +335,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()
@@ -349,4 +351,4 @@ class UserAssetActivity : BaseActivity() {
class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
RecyclerView.ViewHolder(itemUserAssetBinding.root) RecyclerView.ViewHolder(itemUserAssetBinding.root)
} }
@@ -2,13 +2,16 @@ package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.io.File import java.io.File
@@ -21,10 +24,10 @@ class UserAssetUrlActivity : BaseActivity() {
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) } private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null private var del_config: MenuItem? = null
var save_config: MenuItem? = null private var save_config: MenuItem? = null
val extDir by lazy { File(Utils.userAssetPath(this)) } private val extDir by lazy { File(Utils.userAssetPath(this)) }
private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() } private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -41,6 +44,7 @@ class UserAssetUrlActivity : BaseActivity() {
binding.etRemarks.setText(assetNameQrcode) binding.etRemarks.setText(assetNameQrcode)
binding.etUrl.setText(assetUrlQrcode) binding.etUrl.setText(assetUrlQrcode)
} }
else -> clearAsset() else -> clearAsset()
} }
} }
@@ -73,7 +77,11 @@ class UserAssetUrlActivity : BaseActivity() {
// remove file associated with the asset // remove file associated with the asset
val file = extDir.resolve(assetItem.remarks) val file = extDir.resolve(assetItem.remarks)
if (file.exists()) { if (file.exists()) {
file.delete() try {
file.delete()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
}
} }
} else { } else {
assetId = Utils.getUuid() assetId = Utils.getUuid()
@@ -101,7 +109,7 @@ class UserAssetUrlActivity : BaseActivity() {
} }
MmkvManager.encodeAsset(assetId, assetItem) MmkvManager.encodeAsset(assetId, assetItem)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
finish() finish()
return true return true
} }
@@ -116,7 +124,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()
@@ -4,36 +4,33 @@ import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.dto.AppInfo
import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object AppManagerUtil { object AppManagerUtil {
private fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> { /**
val packageManager = ctx.packageManager * Load the list of network applications.
val packages = packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) *
val apps = ArrayList<AppInfo>() * @param context The context to use.
* @return A list of AppInfo objects representing the network applications.
*/
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) { for (pkg in packages) {
val applicationInfo = pkg.applicationInfo ?: continue val applicationInfo = pkg.applicationInfo ?: continue
val appName = applicationInfo.loadLabel(packageManager).toString() val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0 val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0) val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo) apps.add(appInfo)
}
return@withContext apps
} }
}
return apps
}
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
Observable.unsafeCreate {
it.onNext(loadNetworkAppList(ctx))
}
// val PackageInfo.hasInternetPermission: Boolean
// get() {
// val permissions = requestedPermissions
// return permissions?.any { it == Manifest.permission.INTERNET } ?: false
// }
}
@@ -0,0 +1,156 @@
package com.v2ray.ang.util
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode
import com.v2ray.ang.util.Utils.urlDecode
import java.io.IOException
import java.net.*
import java.util.*
object HttpUtil {
/**
* Converts a URL string to its ASCII representation.
*
* @param str The URL string to convert.
* @return The ASCII representation of the URL.
*/
fun idnToASCII(str: String): String {
val url = URI(str)
val host = url.host
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
if (host != asciiHost) {
return str.replace(host, asciiHost)
} else {
return str
}
}
/**
* Retrieves the content of a URL as a string.
*
* @param url The URL to fetch content from.
* @param timeout The timeout value in milliseconds.
* @param httpPort The HTTP port to use.
* @return The content of the URL as a string.
*/
fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? {
val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null
try {
return conn.inputStream.bufferedReader().readText()
} catch (_: Exception) {
} finally {
conn.disconnect()
}
return null
}
/**
* Retrieves the content of a URL as a string with a custom User-Agent header.
*
* @param url The URL to fetch content from.
* @param timeout The timeout value in milliseconds.
* @param httpPort The HTTP port to use.
* @return The content of the URL as a string.
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class)
fun getUrlContentWithUserAgent(url: String?, timeout: Int = 15000, httpPort: Int = 0): String {
var currentUrl = url
var redirects = 0
val maxRedirects = 3
while (redirects++ < maxRedirects) {
if (currentUrl == null) continue
val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
conn.connect()
val responseCode = conn.responseCode
when (responseCode) {
in 300..399 -> {
val location = conn.getHeaderField("Location")
conn.disconnect()
if (location.isNullOrEmpty()) {
throw IOException("Redirect location not found")
}
currentUrl = location
continue
}
else -> try {
return conn.inputStream.use { it.bufferedReader().readText() }
} finally {
conn.disconnect()
}
}
}
throw IOException("Too many redirects")
}
/**
* Creates an HttpURLConnection object connected through a proxy.
*
* @param urlStr The target URL address.
* @param port The port of the proxy server.
* @param connectTimeout The connection timeout in milliseconds (default is 15000 ms).
* @param readTimeout The read timeout in milliseconds (default is 15000 ms).
* @param needStream Whether the connection needs to support streaming.
* @return Returns a configured HttpURLConnection object, or null if it fails.
*/
fun createProxyConnection(
urlStr: String,
port: Int,
connectTimeout: Int = 15000,
readTimeout: Int = 15000,
needStream: Boolean = false
): HttpURLConnection? {
var conn: HttpURLConnection? = null
try {
val url = URL(urlStr)
// Create a connection
conn = if (port == 0) {
url.openConnection()
} else {
url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress(LOOPBACK, port)
)
)
} as HttpURLConnection
// Set connection and read timeouts
conn.connectTimeout = connectTimeout
conn.readTimeout = readTimeout
if (!needStream) {
// Set request headers
conn.setRequestProperty("Connection", "close")
// Disable automatic redirects
conn.instanceFollowRedirects = false
// Disable caching
conn.useCaches = false
}
//Add Basic Authorization
url.userInfo?.let {
conn.setRequestProperty(
"Authorization",
"Basic ${encode(urlDecode(it))}"
)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to create proxy connection", e)
// If an exception occurs, close the connection and return null
conn?.disconnect()
return null
}
return conn
}
}
@@ -1,5 +1,6 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.util.Log
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.JsonObject
@@ -8,19 +9,39 @@ 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
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import java.lang.reflect.Type import java.lang.reflect.Type
object JsonUtil { object JsonUtil {
private var gson = Gson() private var gson = Gson()
/**
* Converts an object to its JSON representation.
*
* @param src The object to convert.
* @return The JSON representation of the object.
*/
fun toJson(src: Any?): String { fun toJson(src: Any?): String {
return gson.toJson(src) return gson.toJson(src)
} }
/**
* Parses a JSON string into an object of the specified class.
*
* @param src The JSON string to parse.
* @param cls The class of the object to parse into.
* @return The parsed object.
*/
fun <T> fromJson(src: String, cls: Class<T>): T { fun <T> fromJson(src: String, cls: Class<T>): T {
return gson.fromJson(src, cls) return gson.fromJson(src, cls)
} }
/**
* Converts an object to its pretty-printed JSON representation.
*
* @param src The object to convert.
* @return The pretty-printed JSON representation of the object, or null if the object is null.
*/
fun toJsonPretty(src: Any?): String? { fun toJsonPretty(src: Any?): String? {
if (src == null) if (src == null)
return null return null
@@ -39,13 +60,19 @@ object JsonUtil {
return gsonPre.toJson(src) return gsonPre.toJson(src)
} }
/**
* Parses a JSON string into a JsonObject.
*
* @param src The JSON string to parse.
* @return The parsed JsonObject, or null if parsing fails.
*/
fun parseString(src: String?): JsonObject? { fun parseString(src: String?): JsonObject? {
if (src == null) if (src == null)
return null return null
try { try {
return JsonParser.parseString(src).getAsJsonObject() return JsonParser.parseString(src).getAsJsonObject()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to parse JSON string", e)
return null return null
} }
} }
@@ -3,21 +3,43 @@ package com.v2ray.ang.util
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.service.V2RayTestService import com.v2ray.ang.service.V2RayTestService
import java.io.Serializable import java.io.Serializable
object MessageUtil { object MessageUtil {
/**
* Sends a message to the service.
*
* @param ctx The context.
* @param what The message identifier.
* @param content The message content.
*/
fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) { fun sendMsg2Service(ctx: Context, what: Int, content: Serializable) {
sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content) sendMsg(ctx, AppConfig.BROADCAST_ACTION_SERVICE, what, content)
} }
/**
* Sends a message to the UI.
*
* @param ctx The context.
* @param what The message identifier.
* @param content The message content.
*/
fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) { fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content) sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
} }
/**
* Sends a message to the test service.
*
* @param ctx The context.
* @param what The message identifier.
* @param content The message content.
*/
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) { fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
try { try {
val intent = Intent() val intent = Intent()
@@ -26,10 +48,18 @@ object MessageUtil {
intent.putExtra("content", content) intent.putExtra("content", content)
ctx.startService(intent) ctx.startService(intent)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to send message to test service", e)
} }
} }
/**
* Sends a message with the specified action.
*
* @param ctx The context.
* @param action The action string.
* @param what The message identifier.
* @param content The message content.
*/
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) { private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
try { try {
val intent = Intent() val intent = Intent()
@@ -39,7 +69,7 @@ object MessageUtil {
intent.putExtra("content", content) intent.putExtra("content", content)
ctx.sendBroadcast(intent) ctx.sendBroadcast(intent)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to send message with action: $action", e)
} }
} }
} }
@@ -11,12 +11,18 @@ import java.util.Locale
open class MyContextWrapper(base: Context?) : ContextWrapper(base) { open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
companion object { companion object {
/**
* Wraps the context with a new locale.
*
* @param context The original context.
* @param newLocale The new locale to set.
* @return A ContextWrapper with the new locale.
*/
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
fun wrap(context: Context, newLocale: Locale?): ContextWrapper { fun wrap(context: Context, newLocale: Locale?): ContextWrapper {
var mContext = context var mContext = context
val res: Resources = mContext.resources val res: Resources = mContext.resources
val configuration: Configuration = res.configuration val configuration: Configuration = res.configuration
//注意 Android 7.0 前后的不同处理方法
mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
configuration.setLocale(newLocale) configuration.setLocale(newLocale)
val localeList = LocaleList(newLocale) val localeList = LocaleList(newLocale)
@@ -3,42 +3,65 @@ package com.v2ray.ang.util
import android.content.Context import android.content.Context
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig
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.fmt.Hysteria2Fmt import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.service.ProcessService import com.v2ray.ang.service.ProcessService
import java.io.File import java.io.File
object PluginUtil { object PluginUtil {
//private const val HYSTERIA2 = "hysteria2-plugin"
private const val HYSTERIA2 = "libhysteria2.so" private const val HYSTERIA2 = "libhysteria2.so"
private const val TAG = ANG_PACKAGE
private val procService: ProcessService by lazy { private val procService: ProcessService by lazy {
ProcessService() ProcessService()
} }
// fun initPlugin(name: String): PluginManager.InitResult { /**
// return PluginManager.init(name)!! * Run the plugin based on the provided configuration.
// } *
* @param context The context to use.
* @param config The profile configuration.
* @param domainPort The domain and port information.
*/
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) { fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
Log.d(TAG, "runPlugin") Log.i(AppConfig.TAG, "Starting plugin execution")
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) { if (config == null) {
val configFile = genConfigHy2(context, config, domainPort) ?: return Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
val cmd = genCmdHy2(context, configFile) return
}
try {
if (config.configType == EConfigType.HYSTERIA2) {
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
val configFile = genConfigHy2(context, config, domainPort) ?: return
val cmd = genCmdHy2(context, configFile)
procService.runProcess(context, cmd) procService.runProcess(context, cmd)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error running plugin", e)
} }
} }
/**
* Stop the running plugin.
*/
fun stopPlugin() { fun stopPlugin() {
stopHy2() stopHy2()
} }
/**
* Perform a real ping using Hysteria2.
*
* @param context The context to use.
* @param config The profile configuration.
* @return The ping delay in milliseconds, or -1 if it fails.
*/
fun realPingHy2(context: Context, config: ProfileItem?): Long { fun realPingHy2(context: Context, config: ProfileItem?): Long {
Log.d(TAG, "realPingHy2") Log.i(AppConfig.TAG, "realPingHy2")
val retFailure = -1L val retFailure = -1L
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) { if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
@@ -49,7 +72,7 @@ object PluginUtil {
val proc = ProcessService() val proc = ProcessService()
proc.runProcess(context, cmd) proc.runProcess(context, cmd)
Thread.sleep(1000L) Thread.sleep(1000L)
val delay = SpeedtestUtil.testConnection(context, socksPort) val delay = SpeedtestManager.testConnection(context, socksPort)
proc.stopProcess() proc.stopProcess()
return delay.first return delay.first
@@ -57,27 +80,41 @@ object PluginUtil {
return retFailure return retFailure
} }
/**
* Generate the configuration file for Hysteria2.
*
* @param context The context to use.
* @param config The profile configuration.
* @param domainPort The domain and port information.
* @return The generated configuration file.
*/
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? { private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
Log.d(TAG, "runPlugin $HYSTERIA2") Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
val socksPort = domainPort?.split(":")?.last() val socksPort = domainPort?.split(":")?.last()
.let { if (it.isNullOrEmpty()) return null else it.toInt() } .let { if (it.isNullOrEmpty()) return null else it.toInt() }
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json") val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
Log.d(TAG, "runPlugin ${configFile.absolutePath}") Log.i(AppConfig.TAG, "runPlugin ${configFile.absolutePath}")
configFile.parentFile?.mkdirs() configFile.parentFile?.mkdirs()
configFile.writeText(JsonUtil.toJson(hy2Config)) configFile.writeText(JsonUtil.toJson(hy2Config))
Log.d(TAG, JsonUtil.toJson(hy2Config)) Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config))
return configFile return configFile
} }
/**
* Generate the command to run Hysteria2.
*
* @param context The context to use.
* @param configFile The configuration file.
* @return The command to run Hysteria2.
*/
private fun genCmdHy2(context: Context, configFile: File): MutableList<String> { private fun genCmdHy2(context: Context, configFile: File): MutableList<String> {
return mutableListOf( return mutableListOf(
File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath, File(context.applicationInfo.nativeLibraryDir, HYSTERIA2).absolutePath,
//initPlugin(HYSTERIA2).path,
"--disable-update-check", "--disable-update-check",
"--config", "--config",
configFile.absolutePath, configFile.absolutePath,
@@ -87,12 +124,15 @@ object PluginUtil {
) )
} }
/**
* Stop the Hysteria2 process.
*/
private fun stopHy2() { private fun stopHy2() {
try { try {
Log.d(TAG, "$HYSTERIA2 destroy") Log.i(AppConfig.TAG, "$HYSTERIA2 destroy")
procService?.stopProcess() procService?.stopProcess()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, e.toString()) Log.e(AppConfig.TAG, "Failed to stop Hysteria2 process", e)
} }
} }
} }
@@ -14,13 +14,17 @@ import com.google.zxing.qrcode.QRCodeWriter
import java.util.EnumMap import java.util.EnumMap
/** /**
* 描述:解析二维码图片 * QR code decoder utility.
*/ */
object QRCodeDecoder { object QRCodeDecoder {
val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java) val HINTS: MutableMap<DecodeHintType, Any?> = EnumMap(DecodeHintType::class.java)
/** /**
* create qrcode using zxing * Creates a QR code bitmap from the given text.
*
* @param text The text to encode in the QR code.
* @param size The size of the QR code bitmap.
* @return The generated QR code bitmap, or null if an error occurs.
*/ */
fun createQRCode(text: String, size: Int = 800): Bitmap? { fun createQRCode(text: String, size: Int = 800): Bitmap? {
return runCatching { return runCatching {
@@ -35,22 +39,21 @@ object QRCodeDecoder {
}.getOrNull() }.getOrNull()
} }
/** /**
* 同步解析本地图片二维码。该方法是耗时操作,请在子线程中调用。 * Decodes a QR code from a local image file. This method is time-consuming and should be called in a background thread.
* *
* @param picturePath 要解析的二维码图片本地路径 * @param picturePath The local path of the image file to decode.
* @return 返回二维码图片里的内容 或 null * @return The content of the QR code, or null if decoding fails.
*/ */
fun syncDecodeQRCode(picturePath: String): String? { fun syncDecodeQRCode(picturePath: String): String? {
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath)) return syncDecodeQRCode(getDecodeAbleBitmap(picturePath))
} }
/** /**
* 同步解析bitmap二维码。该方法是耗时操作,请在子线程中调用。 * Decodes a QR code from a bitmap. This method is time-consuming and should be called in a background thread.
* *
* @param bitmap 要解析的二维码图片 * @param bitmap The bitmap to decode.
* @return 返回二维码图片里的内容 或 null * @return The content of the QR code, or null if decoding fails.
*/ */
fun syncDecodeQRCode(bitmap: Bitmap?): String? { fun syncDecodeQRCode(bitmap: Bitmap?): String? {
return bitmap?.let { return bitmap?.let {
@@ -70,12 +73,11 @@ object QRCodeDecoder {
} }
} }
/** /**
* 将本地图片文件转换成可解码二维码的 Bitmap。为了避免图片太大,这里对图片进行了压缩。感谢 https://github.com/devilsen 提的 PR * Converts a local image file to a bitmap that can be decoded as a QR code. The image is compressed to avoid being too large.
* *
* @param picturePath 本地图片文件路径 * @param picturePath The local path of the image file.
* @return * @return The decoded bitmap, or null if an error occurs.
*/ */
private fun getDecodeAbleBitmap(picturePath: String): Bitmap? { private fun getDecodeAbleBitmap(picturePath: String): Bitmap? {
return try { return try {
@@ -4,10 +4,8 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration.UI_MODE_NIGHT_MASK import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.LocaleList import android.os.LocaleList
import android.provider.Settings import android.provider.Settings
@@ -16,72 +14,77 @@ import android.util.Base64
import android.util.Log import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.Language
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.service.V2RayServiceManager
import java.io.IOException import java.io.IOException
import java.net.* import java.net.ServerSocket
import java.util.* import java.net.URLDecoder
import java.net.URLEncoder
import java.util.Locale
import java.util.UUID
object Utils { object Utils {
private val IPV4_REGEX =
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$")
private val IPV6_REGEX = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
/** /**
* convert string to editalbe for kotlin * Convert string to editable for Kotlin.
* *
* @param text * @param text The string to convert.
* @return * @return An Editable instance containing the text.
*/ */
fun getEditable(text: String?): Editable { fun getEditable(text: String?): Editable {
return Editable.Factory.getInstance().newEditable(text.orEmpty()) return Editable.Factory.getInstance().newEditable(text.orEmpty())
} }
/** /**
* find value in array position * Find the position of a value in an array.
*
* @param array The array to search.
* @param value The value to find.
* @return The index of the value in the array, or -1 if not found.
*/ */
fun arrayFind(array: Array<out String>, value: String): Int { fun arrayFind(array: Array<out String>, value: String): Int {
for (i in array.indices) { return array.indexOf(value)
if (array[i] == value) {
return i
}
}
return -1
} }
/** /**
* parseInt * Parse a string to an integer with a default value.
*
* @param str The string to parse.
* @param default The default value if parsing fails.
* @return The parsed integer, or the default value if parsing fails.
*/ */
fun parseInt(str: String): Int { fun parseInt(str: String?, default: Int = 0): Int {
return parseInt(str, 0)
}
fun parseInt(str: String?, default: Int): Int {
return str?.toIntOrNull() ?: default return str?.toIntOrNull() ?: default
} }
/** /**
* get text from clipboard * Get text from the clipboard.
*
* @param context The context to use.
* @return The text from the clipboard, or an empty string if an error occurs.
*/ */
fun getClipboard(context: Context): String { fun getClipboard(context: Context): String {
return try { return try {
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cmb.primaryClip?.getItemAt(0)?.text.toString() cmb.primaryClip?.getItemAt(0)?.text.toString()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
"" ""
} }
} }
/** /**
* set text to clipboard * Set text to the clipboard.
*
* @param context The context to use.
* @param content The text to set to the clipboard.
*/ */
fun setClipboard(context: Context, content: String) { fun setClipboard(context: Context, content: String) {
try { try {
@@ -89,385 +92,353 @@ object Utils {
val clipData = ClipData.newPlainText(null, content) val clipData = ClipData.newPlainText(null, content)
cmb.setPrimaryClip(clipData) cmb.setPrimaryClip(clipData)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to set clipboard content", e)
} }
} }
/** /**
* base64 decode * Decode a base64 encoded string.
*
* @param text The base64 encoded string.
* @return The decoded string, or an empty string if decoding fails.
*/ */
fun decode(text: String?): String { fun decode(text: String?): String {
return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty() return tryDecodeBase64(text) ?: text?.trimEnd('=')?.let { tryDecodeBase64(it) }.orEmpty()
} }
/**
* Try to decode a base64 encoded string.
*
* @param text The base64 encoded string.
* @return The decoded string, or null if decoding fails.
*/
fun tryDecodeBase64(text: String?): String? { fun tryDecodeBase64(text: String?): String? {
if (text.isNullOrEmpty()) return null
try { try {
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8) return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
Log.i(ANG_PACKAGE, "Parse base64 standard failed $e") Log.e(AppConfig.TAG, "Failed to decode standard base64", e)
} }
try { try {
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8) return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e") Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e)
} }
return null return null
} }
/** /**
* base64 encode * Encode a string to base64.
*
* @param text The string to encode.
* @return The base64 encoded string, or an empty string if encoding fails.
*/ */
fun encode(text: String): String { fun encode(text: String): String {
return try { return try {
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to encode text to base64", e)
"" ""
} }
} }
/** /**
* get remote dns servers from preference * Check if a string is a valid IP address.
*/ *
fun getRemoteDnsServers(): List<String> { * @param value The string to check.
val remoteDns = * @return True if the string is a valid IP address, false otherwise.
MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS) ?: AppConfig.DNS_PROXY
val ret = remoteDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) {
return listOf(AppConfig.DNS_PROXY)
}
return ret
}
fun getVpnDnsServers(): List<String> {
val vpnDns = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_DNS) ?: AppConfig.DNS_VPN
return vpnDns.split(",").filter { isPureIpAddress(it) }
// allow empty, in that case dns will use system default
}
/**
* get remote dns servers from preference
*/
fun getDomesticDnsServers(): List<String> {
val domesticDns =
MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS) ?: AppConfig.DNS_DIRECT
val ret = domesticDns.split(",").filter { isPureIpAddress(it) || isCoreDNSAddress(it) }
if (ret.isEmpty()) {
return listOf(AppConfig.DNS_DIRECT)
}
return ret
}
/**
* is ip address
*/ */
fun isIpAddress(value: String?): Boolean { fun isIpAddress(value: String?): Boolean {
if (value.isNullOrEmpty()) return false
try { try {
if (value.isNullOrEmpty()) { var addr = value.trim()
return false if (addr.isEmpty()) return false
}
var addr = value
if (addr.isEmpty() || addr.isBlank()) {
return false
}
//CIDR //CIDR
if (addr.indexOf("/") > 0) { if (addr.contains("/")) {
val arr = addr.split("/") val arr = addr.split("/")
if (arr.count() == 2 && Integer.parseInt(arr[1]) > -1) { if (arr.size == 2 && arr[1].toIntOrNull() != null && arr[1].toInt() > -1) {
addr = arr[0] addr = arr[0]
} }
} }
// "::ffff:192.168.173.22" // Handle IPv4-mapped IPv6 addresses
// "[::ffff:192.168.173.22]:80"
if (addr.startsWith("::ffff:") && '.' in addr) { if (addr.startsWith("::ffff:") && '.' in addr) {
addr = addr.drop(7) addr = addr.drop(7)
} else if (addr.startsWith("[::ffff:") && '.' in addr) { } else if (addr.startsWith("[::ffff:") && '.' in addr) {
addr = addr.drop(8).replace("]", "") addr = addr.drop(8).replace("]", "")
} }
// addr = addr.toLowerCase() val octets = addr.split('.')
val octets = addr.split('.').toTypedArray()
if (octets.size == 4) { if (octets.size == 4) {
if (octets[3].indexOf(":") > 0) { if (octets[3].contains(":")) {
addr = addr.substring(0, addr.indexOf(":")) addr = addr.substring(0, addr.indexOf(":"))
} }
return isIpv4Address(addr) return isIpv4Address(addr)
} }
// Ipv6addr [2001:abc::123]:8080
return isIpv6Address(addr) return isIpv6Address(addr)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to validate IP address", e)
return false return false
} }
} }
/**
* Check if a string is a pure IP address (IPv4 or IPv6).
*
* @param value The string to check.
* @return True if the string is a pure IP address, false otherwise.
*/
fun isPureIpAddress(value: String): Boolean { fun isPureIpAddress(value: String): Boolean {
return isIpv4Address(value) || isIpv6Address(value) return isIpv4Address(value) || isIpv6Address(value)
} }
fun isIpv4Address(value: String): Boolean { /**
val regV4 = * Check if a string is a valid IPv4 address.
Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") *
return regV4.matches(value) * @param value The string to check.
} * @return True if the string is a valid IPv4 address, false otherwise.
*/
fun isIpv6Address(value: String): Boolean { private fun isIpv4Address(value: String): Boolean {
var addr = value return IPV4_REGEX.matches(value)
if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) {
addr = addr.drop(1)
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
}
val regV6 =
Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
return regV6.matches(addr)
}
private fun isCoreDNSAddress(s: String): Boolean {
return s.startsWith("https") || s.startsWith("tcp") || s.startsWith("quic") || s == "localhost"
} }
/** /**
* is valid url * Check if a string is a valid IPv6 address.
*
* @param value The string to check.
* @return True if the string is a valid IPv6 address, false otherwise.
*/
private fun isIpv6Address(value: String): Boolean {
var addr = value
if (addr.startsWith("[") && addr.endsWith("]")) {
addr = addr.drop(1).dropLast(1)
}
return IPV6_REGEX.matches(addr)
}
/**
* Check if a string is a CoreDNS address.
*
* @param s The string to check.
* @return True if the string is a CoreDNS address, false otherwise.
*/
fun isCoreDNSAddress(s: String): Boolean {
return s.startsWith("https") ||
s.startsWith("tcp") ||
s.startsWith("quic") ||
s == "localhost"
}
/**
* Check if a string is a valid URL.
*
* @param value The string to check.
* @return True if the string is a valid URL, false otherwise.
*/ */
fun isValidUrl(value: String?): Boolean { fun isValidUrl(value: String?): Boolean {
try { if (value.isNullOrEmpty()) return false
if (value.isNullOrEmpty()) {
return false return try {
} Patterns.WEB_URL.matcher(value).matches() ||
if (Patterns.WEB_URL.matcher(value).matches() Patterns.DOMAIN_NAME.matcher(value).matches() ||
|| Patterns.DOMAIN_NAME.matcher(value).matches() URLUtil.isValidUrl(value)
|| URLUtil.isValidUrl(value)
) {
return true
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to validate URL", e)
return false false
} }
return false
}
fun startVServiceFromToggle(context: Context): Boolean {
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
context.toast(R.string.app_tile_first_use)
return false
}
V2RayServiceManager.startV2Ray(context)
return true
} }
/** /**
* stopVService * Open a URI in a browser.
*
* @param context The context to use.
* @param uriString The URI string to open.
*/ */
fun stopVService(context: Context) {
context.toast(R.string.toast_services_stop)
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
}
fun openUri(context: Context, uriString: String) { fun openUri(context: Context, uriString: String) {
val uri = Uri.parse(uriString) try {
context.startActivity(Intent(Intent.ACTION_VIEW, uri)) val uri = uriString.toUri()
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to open URI", e)
}
} }
/** /**
* uuid * Generate a UUID.
*
* @return A UUID string without dashes.
*/ */
fun getUuid(): String { fun getUuid(): String {
return try { return try {
UUID.randomUUID().toString().replace("-", "") UUID.randomUUID().toString().replace("-", "")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to generate UUID", e)
"" ""
} }
} }
/**
* Decode a URL-encoded string.
*
* @param url The URL-encoded string.
* @return The decoded string, or the original string if decoding fails.
*/
fun urlDecode(url: String): String { fun urlDecode(url: String): String {
return try { return try {
URLDecoder.decode(url, Charsets.UTF_8.toString()) URLDecoder.decode(url, Charsets.UTF_8.toString())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to decode URL", e)
url url
} }
} }
/**
* Encode a string to URL-encoded format.
*
* @param url The string to encode.
* @return The URL-encoded string, or the original string if encoding fails.
*/
fun urlEncode(url: String): String { fun urlEncode(url: String): String {
return try { return try {
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20") URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to encode URL", e)
url url
} }
} }
/** /**
* readTextFromAssets * Read text from an asset file.
*
* @param context The context to use.
* @param fileName The name of the asset file.
* @return The content of the asset file as a string.
*/ */
fun readTextFromAssets(context: Context?, fileName: String): String { fun readTextFromAssets(context: Context?, fileName: String): String {
if (context == null) { if (context == null) return ""
return ""
}
val content = context.assets.open(fileName).bufferedReader().use {
it.readText()
}
return content
}
fun userAssetPath(context: Context?): String { return try {
if (context == null) context.assets.open(fileName).use { inputStream ->
return "" inputStream.bufferedReader().use { reader ->
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS) reader.readText()
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath }
return extDir.absolutePath }
}
fun backupPath(context: Context?): String {
if (context == null)
return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS)
?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
return extDir.absolutePath
}
fun getDeviceIdForXUDPBaseKey(): String {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
}
fun getUrlContext(url: String, timeout: Int): String {
var result: String
var conn: HttpURLConnection? = null
try {
conn = URL(url).openConnection() as HttpURLConnection
conn.connectTimeout = timeout
conn.readTimeout = timeout
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
//val code = conn.responseCode
result = conn.inputStream.bufferedReader().readText()
} catch (e: Exception) { } catch (e: Exception) {
result = "" Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
} finally { ""
conn?.disconnect()
}
return result
}
@Throws(IOException::class)
fun getUrlContentWithCustomUserAgent(
urlStr: String?,
timeout: Int = 30000,
httpPort: Int = 0
): String {
val url = URL(urlStr)
val conn = if (httpPort == 0) {
url.openConnection()
} else {
url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress(LOOPBACK, httpPort)
)
)
}
conn.connectTimeout = timeout
conn.readTimeout = timeout
conn.setRequestProperty("Connection", "close")
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
url.userInfo?.let {
conn.setRequestProperty(
"Authorization",
"Basic ${encode(urlDecode(it))}"
)
}
conn.useCaches = false
return conn.inputStream.use {
it.bufferedReader().readText()
} }
} }
/**
* Get the path to the user asset directory.
*
* @param context The context to use.
* @return The path to the user asset directory.
*/
fun userAssetPath(context: Context?): String {
if (context == null) return ""
return try {
context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get user asset path", e)
""
}
}
/**
* Get the path to the backup directory.
*
* @param context The context to use.
* @return The path to the backup directory.
*/
fun backupPath(context: Context?): String {
if (context == null) return ""
return try {
context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get backup path", e)
""
}
}
/**
* Get the device ID for XUDP base key.
*
* @return The device ID for XUDP base key.
*/
fun getDeviceIdForXUDPBaseKey(): String {
return try {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to generate device ID", e)
""
}
}
/**
* Get the dark mode status.
*
* @param context The context to use.
* @return True if dark mode is enabled, false otherwise.
*/
fun getDarkModeStatus(context: Context): Boolean { fun getDarkModeStatus(context: Context): Boolean {
return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO return context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK != UI_MODE_NIGHT_NO
} }
/**
fun setNightMode() { * Get the IPv6 address in a formatted string.
when (MmkvManager.decodeSettingsString(AppConfig.PREF_UI_MODE_NIGHT, "0")) { *
"0" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) * @param address The IPv6 address.
"1" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) * @return The formatted IPv6 address, or the original address if not valid.
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) */
}
}
fun getIpv6Address(address: String?): String { fun getIpv6Address(address: String?): String {
if (address == null) { if (address.isNullOrEmpty()) return ""
return ""
}
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) { return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
String.format("[%s]", address) "[$address]"
} else { } else {
address address
} }
} }
fun getLocale(): Locale { /**
val langCode = * Get the system locale.
MmkvManager.decodeSettingsString(AppConfig.PREF_LANGUAGE) ?: Language.AUTO.code *
val language = Language.fromCode(langCode) * @return The system locale.
*/
return when (language) { fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Language.AUTO -> getSysLocale()
Language.ENGLISH -> Locale.ENGLISH
Language.CHINA -> Locale.CHINA
Language.TRADITIONAL_CHINESE -> Locale.TRADITIONAL_CHINESE
Language.VIETNAMESE -> Locale("vi")
Language.RUSSIAN -> Locale("ru")
Language.PERSIAN -> Locale("fa")
Language.BANGLA -> Locale("bn")
Language.BAKHTIARI -> Locale("bqi", "IR")
}
}
private fun getSysLocale(): Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LocaleList.getDefault()[0] LocaleList.getDefault()[0]
} else { } else {
Locale.getDefault() Locale.getDefault()
} }
/**
* Fix illegal characters in a URL.
*
* @param str The URL string.
* @return The URL string with illegal characters replaced.
*/
fun fixIllegalUrl(str: String): String { fun fixIllegalUrl(str: String): String {
return str return str.replace(" ", "%20")
.replace(" ", "%20")
.replace("|", "%7C") .replace("|", "%7C")
} }
fun removeWhiteSpace(str: String?): String? { /**
return str?.replace(" ", "") * Find a free port from a list of ports.
} *
* @param ports The list of ports to check.
fun idnToASCII(str: String): String { * @return The first free port found.
val url = URL(str) * @throws IOException If no free port is found.
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file) */
.toExternalForm()
}
fun isTv(context: Context): Boolean =
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
fun getDelayTestUrl(second: Boolean = false): String {
return if (second) {
AppConfig.DelayTestUrl2
} else {
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
?: AppConfig.DelayTestUrl
}
}
fun findFreePort(ports: List<Int>): Int { fun findFreePort(ports: List<Int>): Int {
for (port in ports) { for (port in ports) {
try { try {
@@ -481,22 +452,41 @@ object Utils {
throw IOException("no free port found") throw IOException("no free port found")
} }
/**
* Check if a string is a valid subscription URL.
*
* @param value The string to check.
* @return True if the string is a valid subscription URL, false otherwise.
*/
fun isValidSubUrl(value: String?): Boolean { fun isValidSubUrl(value: String?): Boolean {
try { if (value.isNullOrEmpty()) return false
if (value.isNullOrEmpty()) return false
if (URLUtil.isHttpsUrl(value)) return true return try {
if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true URLUtil.isHttpsUrl(value) ||
(URLUtil.isHttpUrl(value) && value.contains(LOOPBACK))
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
false
} }
return false
} }
/**
* Get the receiver flags based on the Android version.
*
* @return The receiver flags.
*/
fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { fun receiverFlags(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.RECEIVER_EXPORTED ContextCompat.RECEIVER_EXPORTED
} else { } else {
ContextCompat.RECEIVER_NOT_EXPORTED ContextCompat.RECEIVER_NOT_EXPORTED
} }
/**
* Check if the package is Xray.
*
* @return True if the package is Xray, false otherwise.
*/
fun isXray(): Boolean = ANG_PACKAGE.startsWith("com.v2ray.ang")
} }
@@ -1,5 +1,7 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.util.Log
import com.v2ray.ang.AppConfig
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@@ -13,6 +15,14 @@ import java.util.zip.ZipOutputStream
object ZipUtil { object ZipUtil {
private const val BUFFER_SIZE = 4096 private const val BUFFER_SIZE = 4096
/**
* Zip the contents of a folder.
*
* @param folderPath The path to the folder to zip.
* @param outputZipFilePath The path to the output zip file.
* @return True if the operation is successful, false otherwise.
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class) @Throws(IOException::class)
fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean { fun zipFromFolder(folderPath: String, outputZipFilePath: String): Boolean {
val buffer = ByteArray(BUFFER_SIZE) val buffer = ByteArray(BUFFER_SIZE)
@@ -53,12 +63,20 @@ object ZipUtil {
zos.closeEntry() zos.closeEntry()
zos.close() zos.close()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to zip folder", e)
return false return false
} }
return true return true
} }
/**
* Unzip the contents of a zip file to a folder.
*
* @param zipFile The zip file to unzip.
* @param destDirectory The destination directory.
* @return True if the operation is successful, false otherwise.
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class) @Throws(IOException::class)
fun unzipToFolder(zipFile: File, destDirectory: String): Boolean { fun unzipToFolder(zipFile: File, destDirectory: String): Boolean {
File(destDirectory).run { File(destDirectory).run {
@@ -72,10 +90,8 @@ object ZipUtil {
zip.getInputStream(entry).use { input -> zip.getInputStream(entry).use { input ->
val filePath = destDirectory + File.separator + entry.name val filePath = destDirectory + File.separator + entry.name
if (!entry.isDirectory) { if (!entry.isDirectory) {
// if the entry is a file, extracts it
extractFile(input, filePath) extractFile(input, filePath)
} else { } else {
// if the entry is a directory, make the directory
val dir = File(filePath) val dir = File(filePath)
dir.mkdir() dir.mkdir()
} }
@@ -83,12 +99,19 @@ object ZipUtil {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to unzip file", e)
return false return false
} }
return true return true
} }
/**
* Extract a file from an input stream.
*
* @param inputStream The input stream to read from.
* @param destFilePath The destination file path.
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class) @Throws(IOException::class)
private fun extractFile(inputStream: InputStream, destFilePath: String) { private fun extractFile(inputStream: InputStream, destFilePath: String) {
val bos = BufferedOutputStream(FileOutputStream(destFilePath)) val bos = BufferedOutputStream(FileOutputStream(destFilePath))

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