Compare commits

...

99 Commits

Author SHA1 Message Date
2dust
3773962b64 up 1.10.2 2025-05-07 10:47:50 +08:00
2dust
be0a2506ce Update AndroidLibXrayLite 2025-05-07 10:44:19 +08:00
2dust
7f9cb8dfdd Check upgrade function is visible 2025-05-07 10:14:14 +08:00
2dust
71a5b6e480 Update AndroidLibXrayLite 2025-05-04 17:49:02 +08:00
2dust
02e53ced50 Update AndroidLibXrayLite 2025-04-30 14:49:02 +08:00
2dust
42c27a5e7e Update hysteria 2025-04-30 14:48:59 +08:00
2dust
af04bbcf87 up 1.10.1 2025-04-30 14:35:48 +08:00
2dust
9bedfe8a7b Bug fix
https://github.com/2dust/v2rayNG/issues/4555
2025-04-30 08:31:51 +08:00
2dust
2fdf684ee7 Fix
https://github.com/2dust/v2rayNG/issues/4548
2025-04-28 15:30:16 +08:00
2dust
5b79951da7 up 1.10.0 2025-04-24 10:33:23 +08:00
2dust
06aa680d45 Update libs.versions.toml 2025-04-24 10:26:03 +08:00
Hossein Abaspanah
cdb9b1811c Update Luri Bakhtiari translation (#4535) 2025-04-24 10:11:20 +08:00
solokot
0fc1f2f5d3 Update Russian translation (#4534) 2025-04-24 10:11:10 +08:00
Pk-web6936
ef1bb3dd34 Update Persian translation (#4533) 2025-04-24 10:11:02 +08:00
2dust
1bca321d3f Temporarily add option to allow insecure HTTP subscription address
https://github.com/2dust/v2rayNG/issues/4526
2025-04-23 10:05:49 +08:00
solokot
247e2b3ba3 Update Russian translation (#4532) 2025-04-22 10:36:23 +08:00
2dust
41fd2b0cfb Fix 2025-04-22 10:36:10 +08:00
Hossein Abaspanah
72da42ee40 Update Luri Bakhtiari translation (#4524) 2025-04-20 09:33:01 +08:00
AmirHossein Abdolmotallebi
c130d55e8f Update V2rayConfig.kt (#4522) 2025-04-20 09:32:52 +08:00
Pk-web6936
5ae84f7eac Update Persian translate (#4518)
https://github.com/2dust/v2rayNG/pull/4507
2025-04-20 09:31:50 +08:00
patterniha
df5ea251e1 move Prefer_IPv6 settings (#4507) 2025-04-19 15:35:20 +08:00
2dust
8890d9f004 Organize and optimize the code of V2rayConfigManager 2025-04-19 11:38:02 +08:00
2dust
4fcb3f9d06 Refactor ConfigResult 2025-04-19 10:24:15 +08:00
2dust
5bf7c98cd3 Refactor outbound related code 2025-04-18 20:02:55 +08:00
2dust
46bc1a49df Refactor reference code with libv2ray remove protect 2025-04-18 17:20:21 +08:00
2dust
21175f41ec Update AndroidLibXrayLite 2025-04-18 17:19:40 +08:00
DHR60
864c63987e Adds domain strategy option to sockopt (#4511)
* Adds domain strategy option to sockopt

* Simplifies sockopt handling in V2Ray config
2025-04-18 16:49:51 +08:00
DHR60
4ac0547e22 Resolves hostnames in config (#4508)
* Removes IP resolution and resolves in config

* Resolves hostnames to multiple IPs for DNS

* Improves custom config handling
2025-04-18 14:12:14 +08:00
2dust
12a9ee262c Revert "Update gradle.properties"
This reverts commit 56e33e6cdd.
2025-04-18 10:24:54 +08:00
2dust
cfa9c19c94 Clean code 2025-04-18 10:22:44 +08:00
2dust
56e33e6cdd Update gradle.properties 2025-04-18 10:22:17 +08:00
2dust
02421072c1 Merge branch 'master' of https://github.com/2dust/v2rayNG 2025-04-17 16:41:04 +08:00
2dust
b862a0dc65 Update AndroidLibXrayLite 2025-04-17 16:40:53 +08:00
Pk-web6936
1f25d6a000 Update dependencies (#4504)
* Update libs.versions.toml

* Update libs.versions.toml
2025-04-17 16:29:56 +08:00
2dust
e1def0616a Refactor the Outbound transport and tls in the configuration file 2025-04-17 15:23:57 +08:00
2dust
83fd6efc17 Resolve remote host names in the configuration file to IP addresses 2025-04-17 14:08:45 +08:00
2dust
f0c0e2e83a Refactor reference code with libv2ray 2025-04-17 10:41:30 +08:00
Pk-web6936
6ca3eb769e Update dependencies (#4485)
* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml

* Update libs.versions.toml
2025-04-16 20:06:44 +08:00
2dust
963d24ab66 Optimize and improve
38193b5621
2025-04-16 15:09:49 +08:00
2dust
cfd81441fa Update libs.versions.toml 2025-04-15 20:44:03 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
4084ae2938 Updating PRIVATE_IP_LIST (#4498) 2025-04-15 19:21:25 +08:00
kore kas nadar
3f9bc098ec Update Luri Bakhtiari translation (#4497) 2025-04-15 19:21:17 +08:00
Pk-web6936
9cb28ed969 Update Persian translate (#4495)
38193b5621
2025-04-15 14:14:13 +08:00
DHR60
773ddc5373 Fix wg chain proxy (#4496)
* Fix handle null streamSettings in WireGuard chained proxy

* Allows null preshared key for WireGuard

* remove WIREGUARD_LOCAL_ADDRESS_V6

* Considers WireGuard outbound for domain port
2025-04-14 20:42:02 +08:00
2dust
38193b5621 Added IP display in connection test
https://github.com/2dust/v2rayNG/issues/4489
2025-04-14 14:15:25 +08:00
solokot
358713a2a3 Update Russian translation (#4484) 2025-04-11 09:19:02 +08:00
2dust
5b9f24c1f0 up 1.9.46 2025-04-09 14:41:11 +08:00
2dust
c47c2c3666 Code clean 2025-04-09 14:35:05 +08:00
2dust
49f7c3e7d7 Added tips for per app proxy 2025-04-09 14:34:27 +08:00
2dust
423e5de2c6 Fix
https://github.com/2dust/v2rayNG/issues/4473
2025-04-09 11:55:43 +08:00
kore kas nadar
3e3387e63e Update Luri Bakhtiari translation (#4477) 2025-04-09 10:50:47 +08:00
solokot
debddace8b Update Russian translation (#4476) 2025-04-09 10:50:33 +08:00
Pk-web6936
160b412e0a Update Persian translate (#4475) 2025-04-09 10:50:23 +08:00
2dust
0f3e0a0ea2 Optimize and improve RoutingSettingActivity 2025-04-09 10:47:35 +08:00
2dust
c4cf90e807 Optimize and improve UserAssetActivity 2025-04-09 10:46:21 +08:00
2dust
5db46e81b7 Bug fix
https://github.com/2dust/v2rayNG/issues/4474
2025-04-09 10:08:56 +08:00
2dust
1ef80a3a96 Refactor AppConfig const 2025-04-08 21:05:34 +08:00
2dust
a46d9d0d2a Fix tools:context 2025-04-08 21:03:29 +08:00
2dust
7b80536e1e Added GEO files sources settings in asset settings
https://github.com/2dust/v2rayNG/issues/4440
2025-04-08 19:31:26 +08:00
2dust
5733ecf20e Add some unit test 2025-04-07 17:06:29 +08:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
eae33b61cf Restoring some IP ranges removed in b4c833b (#4469) 2025-04-07 16:55:59 +08:00
2dust
9e55b525f1 Bug fix
https://github.com/2dust/v2rayNG/issues/4468
2025-04-07 16:15:06 +08:00
2dust
678b3cb505 Allow private IP to use HTTP protocol
https://github.com/2dust/v2rayNG/issues/4460
2025-04-07 16:00:56 +08:00
2dust
b4c833b039 Refactoring of private IP lists 2025-04-06 19:42:01 +08:00
2dust
597bd021b8 Bug fix 2025-04-06 14:22:47 +08:00
2dust
ba03118a43 Code clean 2025-04-06 14:22:30 +08:00
kore kas nadar
82148408b0 Improved Luri Bakhtiari Translation (#4465)
* Improved Luri Bakhtiari Translation

* Improved Luri Bakhtiari Translation
2025-04-06 14:09:49 +08:00
kore kas nadar
042900e065 Update Luri Bakhtiari translation (#4463) 2025-04-06 10:19:20 +08:00
kore kas nadar
874fccc351 Update Luri Bakhtiari translation (#4455) 2025-04-04 10:35:39 +08:00
2dust
14f36872e7 If it is the Google Play version, the update check will not be displayed within 2 days after update. 2025-04-03 17:30:29 +08:00
solokot
3b6ad3052a Update Russian translation (#4451) 2025-04-03 15:39:55 +08:00
Pk-web6936
194fc6b6ed Update Persian Translation (#4449)
* Update Persian Translation

* Update strings.xml

* Update strings.xml
2025-04-03 15:39:47 +08:00
Pk-web6936
0275ad54ac Delete Unnecessary () around expression (#4447)
* Delete Unnecessary () around expression

Delete Unnecessary () around expression

* Delete Unnecessary () around expression

Delete Unnecessary () around expression

* Delete Unnecessary () around expression

Delete Unnecessary () around expression

* Delete Unnecessary () around expression

Delete Unnecessary () around expression

* Delete Unnecessary () around expression

Delete Unnecessary () around expression

* Delete Unnecessary () around expression

Delete Unnecessary () around expression

* Delete Unnecessary () around expression

Delete Unnecessary () around expression
2025-04-03 10:20:59 +08:00
2dust
7ca4044467 Added check for updates 2025-04-01 17:24:59 +08:00
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
139 changed files with 2503 additions and 1580 deletions

View File

@@ -13,4 +13,4 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Validate Fastlane Supply Metadata - name: Validate Fastlane Supply Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.0.0 uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0

60
.gitignore vendored
View File

@@ -1,6 +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 *.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

View File

@@ -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
V2rayNG/.gitignore vendored
View File

@@ -1,10 +0,0 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
*.apk
signing.properties
*.aar

View File

@@ -1,2 +0,0 @@
/build
/google-services.json

View File

@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 21 minSdk = 21
targetSdk = 35 targetSdk = 35
versionCode = 642 versionCode = 652
versionName = "1.9.42" versionName = "1.10.2"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -82,8 +82,9 @@ android {
val isFdroid = variant.productFlavors.any { it.name == "fdroid" } val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
if (isFdroid) { if (isFdroid) {
val versionCodes = val versionCodes =
mapOf("armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0 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 }
@@ -148,7 +149,7 @@ dependencies {
// 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)

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,10 +51,10 @@
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"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="m"> tools:targetApi="m">
<activity <activity
@@ -213,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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -97,7 +97,7 @@
} }
], ],
"routing": { "routing": {
"domainStrategy": "IPIfNonMatch", "domainStrategy": "AsIs",
"rules": [] "rules": []
}, },
"dns": { "dns": {

View File

@@ -39,5 +39,9 @@ class AngApplication : MultiDexApplication() {
WorkManager.initialize(this, workManagerConfiguration) WorkManager.initialize(this, workManagerConfiguration)
SettingsManager.initRoutingRulesets(this) SettingsManager.initRoutingRulesets(this)
es.dmoral.toasty.Toasty.Config.getInstance()
.setGravity(android.view.Gravity.BOTTOM, 0, 200)
.apply()
} }
} }

View File

@@ -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"
@@ -56,13 +57,15 @@ object AppConfig {
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"
const val PREF_IS_BOOTED = "pref_is_booted" const val PREF_IS_BOOTED = "pref_is_booted"
const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release"
const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources"
/** Cache keys. */ /** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter" const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */ /** Protocol identifiers. */
const val PROTOCOL_FREEDOM: String = "freedom" const val PROTOCOL_FREEDOM = "freedom"
/** Broadcast actions. */ /** Broadcast actions. */
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service" const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
@@ -87,19 +90,20 @@ object AppConfig {
const val DOWNLINK = "downlink" const val DOWNLINK = "downlink"
/** URLs for various resources. */ /** URLs for various resources. */
const val androidpackagenamelistUrl = const val GITHUB_URL = "https://github.com"
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt" const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
const val v2rayCustomRoutingListUrl = const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/" const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG" const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
const val v2rayNGIssues = "$v2rayNGUrl/issues" const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode" const val APP_ISSUES_URL = "$APP_URL/issues"
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md" const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/" const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val TgChannelUrl = "https://t.me/github_2dust" const val TG_CHANNEL_URL = "https://t.me/github_2dust"
const val DelayTestUrl = "https://www.gstatic.com/generate_204" const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
const val DelayTestUrl2 = "https://www.google.com/generate_204" const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
const val IP_API_Url = "https://api.ip.sb/geoip"
/** DNS server addresses. */ /** DNS server addresses. */
const val DNS_PROXY = "1.1.1.1" const val DNS_PROXY = "1.1.1.1"
@@ -169,14 +173,6 @@ 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_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
const val DEFAULT_PORT = 443 const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto" const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8 const val DEFAULT_LEVEL = 8
@@ -185,4 +181,62 @@ object AppConfig {
const val REALITY = "reality" const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http" const val HEADER_TYPE_HTTP = "http"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
//minimum list https://serverfault.com/a/304791
val BYPASS_PRIVATE_IP_LIST = arrayListOf(
"0.0.0.0/5",
"8.0.0.0/7",
"11.0.0.0/8",
"12.0.0.0/6",
"16.0.0.0/4",
"32.0.0.0/3",
"64.0.0.0/2",
"128.0.0.0/3",
"160.0.0.0/5",
"168.0.0.0/6",
"172.0.0.0/12",
"172.32.0.0/11",
"172.64.0.0/10",
"172.128.0.0/9",
"173.0.0.0/8",
"174.0.0.0/7",
"176.0.0.0/4",
"192.0.0.0/9",
"192.128.0.0/11",
"192.160.0.0/13",
"192.169.0.0/16",
"192.170.0.0/15",
"192.172.0.0/14",
"192.176.0.0/12",
"192.192.0.0/10",
"193.0.0.0/8",
"194.0.0.0/7",
"196.0.0.0/6",
"200.0.0.0/5",
"208.0.0.0/4",
"240.0.0.0/4"
)
val PRIVATE_IP_LIST = arrayListOf(
"0.0.0.0/8",
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"224.0.0.0/4"
)
val GEO_FILES_SOURCES = arrayListOf(
"Loyalsoldier/v2ray-rules-dat",
"runetfreedom/russia-v2ray-rules-dat",
"Chocolate4U/Iran-v2ray-rules"
)
} }

View File

@@ -4,5 +4,6 @@ data class AssetUrlItem(
var remarks: String = "", var remarks: String = "",
var url: String = "", var url: String = "",
val addedTime: Long = System.currentTimeMillis(), val addedTime: Long = System.currentTimeMillis(),
var lastUpdated: Long = -1 var lastUpdated: Long = -1,
var locked: Boolean? = false,
) )

View File

@@ -0,0 +1,10 @@
package com.v2ray.ang.dto
data class CheckUpdateResult(
val hasUpdate: Boolean,
val latestVersion: String? = null,
val releaseNotes: String? = null,
val downloadUrl: String? = null,
val error: String? = null,
val isPreRelease: Boolean = false
)

View File

@@ -4,6 +4,6 @@ data class ConfigResult(
var status: Boolean, var status: Boolean,
var guid: String? = null, var guid: String? = null,
var content: String = "", var content: String = "",
var domainPort: String? = null, var socksPort: Int? = null,
) )

View File

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

View File

@@ -0,0 +1,23 @@
package com.v2ray.ang.dto
import com.google.gson.annotations.SerializedName
data class GitHubRelease(
@SerializedName("tag_name")
val tagName: String,
@SerializedName("body")
val body: String,
@SerializedName("assets")
val assets: List<Asset>,
@SerializedName("prerelease")
val prerelease: Boolean = false,
@SerializedName("published_at")
val publishedAt: String = ""
) {
data class Asset(
@SerializedName("name")
val name: String,
@SerializedName("browser_download_url")
val browserDownloadUrl: String
)
}

View File

@@ -0,0 +1,11 @@
package com.v2ray.ang.dto
data class IPAPIInfo(
var ip: String? = null,
var city: String? = null,
var region: String? = null,
var region_code: String? = null,
var country: String? = null,
var country_name: String? = null,
var country_code: String? = null
)

View File

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

View File

@@ -8,6 +8,7 @@ enum class NetworkType(val type: String) {
XHTTP("xhttp"), XHTTP("xhttp"),
HTTP("http"), HTTP("http"),
H2("h2"), H2("h2"),
//QUIC("quic"), //QUIC("quic"),
GRPC("grpc"); GRPC("grpc");

View File

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

View File

@@ -11,5 +11,6 @@ data class SubscriptionItem(
var prevProfile: String? = null, var prevProfile: String? = null,
var nextProfile: String? = null, var nextProfile: String? = null,
var filter: String? = null, var filter: String? = null,
var allowInsecureUrl: Boolean = false,
) )

View File

@@ -1,28 +1,17 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
import android.text.TextUtils
import com.google.gson.GsonBuilder
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
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.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
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.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
data class V2rayConfig( data class V2rayConfig(
var remarks: String? = null, var remarks: String? = null,
var stats: Any? = null, var stats: Any? = null,
val log: LogBean, val log: LogBean,
var policy: PolicyBean?, var policy: PolicyBean? = null,
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,
@@ -34,9 +23,9 @@ data class V2rayConfig(
) { ) {
data class LogBean( data class LogBean(
val access: String, val access: String? = null,
val error: String, val error: String? = null,
var loglevel: String?, var loglevel: String? = null,
val dnsLog: Boolean? = null val dnsLog: Boolean? = null
) )
@@ -46,7 +35,7 @@ data class V2rayConfig(
var protocol: String, var protocol: String,
var listen: String? = null, var listen: String? = null,
val settings: Any? = null, val settings: Any? = null,
val sniffing: SniffingBean?, val sniffing: SniffingBean? = null,
val streamSettings: Any? = null, val streamSettings: Any? = null,
val allocate: Any? = null val allocate: Any? = null
) { ) {
@@ -77,50 +66,6 @@ data class V2rayConfig(
val sendThrough: String? = null, val sendThrough: String? = null,
var mux: MuxBean? = MuxBean(false) var mux: MuxBean? = MuxBean(false)
) { ) {
companion object {
fun create(configType: EConfigType): OutboundBean? {
return when (configType) {
EConfigType.VMESS,
EConfigType.VLESS ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
vnext = listOf(
VnextBean(
users = listOf(UsersBean())
)
)
),
streamSettings = StreamSettingsBean()
)
EConfigType.SHADOWSOCKS,
EConfigType.SOCKS,
EConfigType.HTTP,
EConfigType.TROJAN,
EConfigType.HYSTERIA2 ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
servers = listOf(ServersBean())
),
streamSettings = StreamSettingsBean()
)
EConfigType.WIREGUARD ->
return OutboundBean(
protocol = configType.name.lowercase(),
settings = OutSettingsBean(
secretKey = "",
peers = listOf(WireGuardBean())
)
)
EConfigType.CUSTOM -> null
}
}
}
data class OutSettingsBean( data class OutSettingsBean(
var vnext: List<VnextBean>? = null, var vnext: List<VnextBean>? = null,
var fragment: FragmentBean? = null, var fragment: FragmentBean? = null,
@@ -197,7 +142,7 @@ data class V2rayConfig(
data class WireGuardBean( data class WireGuardBean(
var publicKey: String = "", var publicKey: String = "",
var preSharedKey: String = "", var preSharedKey: String? = null,
var endpoint: String = "" var endpoint: String = ""
) )
} }
@@ -261,7 +206,8 @@ data class V2rayConfig(
) { ) {
data class HeaderBean( data class HeaderBean(
var type: String = "none", var type: String = "none",
var domain: String? = null) var domain: String? = null
)
} }
data class WsSettingsBean( data class WsSettingsBean(
@@ -298,7 +244,8 @@ data class V2rayConfig(
var tcpFastOpen: Boolean? = null, var tcpFastOpen: Boolean? = null,
var tproxy: String? = null, var tproxy: String? = null,
var mark: Int? = null, var mark: Int? = null,
var dialerProxy: String? = null var dialerProxy: String? = null,
var domainStrategy: String? = null
) )
data class TlsSettingsBean( data class TlsSettingsBean(
@@ -348,139 +295,6 @@ data class V2rayConfig(
) )
} }
fun populateTransportSettings(
transport: String,
headerType: String?,
host: String?,
path: String?,
seed: String?,
quicSecurity: String?,
key: String?,
mode: String?,
serviceName: String?,
authority: String?
): String? {
var sni: String? = null
network = if (transport.isEmpty()) NetworkType.TCP.type else transport
when (network) {
NetworkType.TCP.type -> {
val tcpSetting = TcpSettingsBean()
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
tcpSetting.header.request = requestObj
sni = requestObj.headers.Host?.getOrNull(0)
}
} else {
tcpSetting.header.type = "none"
sni = host
}
tcpSettings = tcpSetting
}
NetworkType.KCP.type -> {
val kcpsetting = KcpSettingsBean()
kcpsetting.header.type = headerType ?: "none"
if (seed.isNullOrEmpty()) {
kcpsetting.seed = null
} else {
kcpsetting.seed = seed
}
if (host.isNullOrEmpty()) {
kcpsetting.header.domain = null
} else {
kcpsetting.header.domain = host
}
kcpSettings = kcpsetting
}
NetworkType.WS.type -> {
val wssetting = WsSettingsBean()
wssetting.headers.Host = host.orEmpty()
sni = host
wssetting.path = path ?: "/"
wsSettings = wssetting
}
NetworkType.HTTP_UPGRADE.type -> {
val httpupgradeSetting = HttpupgradeSettingsBean()
httpupgradeSetting.host = host.orEmpty()
sni = host
httpupgradeSetting.path = path ?: "/"
httpupgradeSettings = httpupgradeSetting
}
NetworkType.XHTTP.type -> {
val xhttpSetting = XhttpSettingsBean()
xhttpSetting.host = host.orEmpty()
sni = host
xhttpSetting.path = path ?: "/"
xhttpSettings = xhttpSetting
}
NetworkType.H2.type, NetworkType.HTTP.type -> {
network = NetworkType.H2.type
val h2Setting = HttpSettingsBean()
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
sni = h2Setting.host.getOrNull(0)
h2Setting.path = path ?: "/"
httpSettings = h2Setting
}
// "quic" -> {
// val quicsetting = QuicSettingBean()
// quicsetting.security = quicSecurity ?: "none"
// quicsetting.key = key.orEmpty()
// quicsetting.header.type = headerType ?: "none"
// quicSettings = quicsetting
// }
NetworkType.GRPC.type -> {
val grpcSetting = GrpcSettingsBean()
grpcSetting.multiMode = mode == "multi"
grpcSetting.serviceName = serviceName.orEmpty()
grpcSetting.authority = authority.orEmpty()
grpcSetting.idle_timeout = 60
grpcSetting.health_check_timeout = 20
sni = authority
grpcSettings = grpcSetting
}
}
return sni
}
fun populateTlsSettings(
streamSecurity: String,
allowInsecure: Boolean,
sni: String?,
fingerprint: String?,
alpns: String?,
publicKey: String?,
shortId: String?,
spiderX: String?
) {
security = if (streamSecurity.isEmpty()) null else streamSecurity
if (security == null) return
val tlsSetting = TlsSettingsBean(
allowInsecure = allowInsecure,
serverName = if (sni.isNullOrEmpty()) null else sni,
fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
shortId = if (shortId.isNullOrEmpty()) null else shortId,
spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
)
if (security == AppConfig.TLS) {
tlsSettings = tlsSetting
realitySettings = null
} else if (security == AppConfig.REALITY) {
tlsSettings = null
realitySettings = tlsSetting
}
}
} }
data class MuxBean( data class MuxBean(
@@ -646,6 +460,18 @@ data class V2rayConfig(
} }
return null return null
} }
fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean {
val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also {
streamSettings = it
}
val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also {
stream.sockopt = it
}
return sockopt
}
} }
data class DnsBean( data class DnsBean(
@@ -722,15 +548,9 @@ data class V2rayConfig(
return null return null
} }
fun toPrettyPrinting(): String { fun getAllProxyOutbound(): List<OutboundBean> {
return GsonBuilder() return outbounds.filter { outbound ->
.setPrettyPrinting() EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
.disableHtmlEscaping() }
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
object : TypeToken<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
)
.create()
.toJson(this)
} }
} }

View File

@@ -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
@@ -23,7 +23,7 @@ val Context.v2RayApplication: AngApplication?
* @param message The resource ID of the message to show. * @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()
} }
/** /**
@@ -32,9 +32,46 @@ fun Context.toast(message: Int) {
* @param message The text of the message to show. * @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. * Puts a key-value pair into the JSONObject.
* *
@@ -159,4 +196,17 @@ inline fun <reified T : Serializable> Intent.serializable(key: String): T? = whe
* *
* @return True if the CharSequence is not null and not empty, false otherwise. * @return True if the CharSequence is not null and not empty, false otherwise.
*/ */
fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty()) fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty()
fun String.concatUrl(vararg paths: String): String {
val builder = StringBuilder(this.trimEnd('/'))
paths.forEach { path ->
val trimmedPath = path.trim('/')
if (trimmedPath.isNotEmpty()) {
builder.append('/').append(trimmedPath)
}
}
return builder.toString()
}

View File

@@ -18,9 +18,9 @@ open class FmtBase {
*/ */
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(
separator = "&", separator = "&",
transform = { it.first + "=" + Utils.urlEncode(it.second) })) transform = { it.first + "=" + Utils.urlEncode(it.second) })
else "" else ""
val url = String.format( val url = String.format(
@@ -120,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.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() }
@@ -148,4 +148,7 @@ open class FmtBase {
return dicQuery return dicQuery
} }
}
}

View File

@@ -4,6 +4,7 @@ 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 com.v2ray.ang.handler.V2rayConfigManager
object HttpFmt : FmtBase() { object HttpFmt : FmtBase() {
/** /**
@@ -13,7 +14,7 @@ object HttpFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails * @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 = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
outboundBean?.settings?.servers?.first()?.let { server -> outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty() server.address = profileItem.server.orEmpty()

View File

@@ -9,6 +9,7 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -24,7 +25,7 @@ object Hysteria2Fmt : FmtBase() {
val config = ProfileItem.create(EConfigType.HYSTERIA2) val config = ProfileItem.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str)) val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.password = uri.userInfo config.password = uri.userInfo
@@ -144,7 +145,7 @@ object Hysteria2Fmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails * @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 = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
return outboundBean return outboundBean
} }
} }

View File

@@ -1,10 +1,13 @@
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
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -33,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
if (uri.port <= 0) return null if (uri.port <= 0) return null
if (uri.userInfo.isNullOrEmpty()) return null if (uri.userInfo.isNullOrEmpty()) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
@@ -82,7 +85,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)
@@ -129,7 +132,7 @@ object ShadowsocksFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails * @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 = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.servers?.first()?.let { server -> outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty() server.address = profileItem.server.orEmpty()
@@ -138,29 +141,13 @@ object ShadowsocksFmt : FmtBase() {
server.method = profileItem.method server.method = profileItem.method
} }
val sni = outboundBean?.streamSettings?.populateTransportSettings( val sni = outboundBean?.streamSettings?.let {
profileItem.network.orEmpty(), V2rayConfigManager.populateTransportSettings(it, profileItem)
profileItem.headerType, }
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings( outboundBean?.streamSettings?.let {
profileItem.security.orEmpty(), V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
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
} }

View File

@@ -5,6 +5,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -22,7 +23,7 @@ object SocksFmt : FmtBase() {
if (uri.idnHost.isEmpty()) return null if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null if (uri.port <= 0) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
@@ -60,7 +61,7 @@ object SocksFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails * @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 = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
outboundBean?.settings?.servers?.first()?.let { server -> outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty() server.address = profileItem.server.orEmpty()

View File

@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -22,7 +23,7 @@ object TrojanFmt : FmtBase() {
val config = ProfileItem.create(EConfigType.TROJAN) val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str)) val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.password = uri.userInfo config.password = uri.userInfo
@@ -60,7 +61,7 @@ object TrojanFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails * @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 = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
outboundBean?.settings?.servers?.first()?.let { server -> outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty() server.address = profileItem.server.orEmpty()
@@ -69,29 +70,13 @@ object TrojanFmt : FmtBase() {
server.flow = profileItem.flow server.flow = profileItem.flow
} }
val sni = outboundBean?.streamSettings?.populateTransportSettings( val sni = outboundBean?.streamSettings?.let {
profileItem.network.orEmpty(), V2rayConfigManager.populateTransportSettings(it, profileItem)
profileItem.headerType, }
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings( outboundBean?.streamSettings?.let {
profileItem.security.orEmpty(), V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
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
} }

View File

@@ -6,7 +6,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri) val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.password = uri.userInfo config.password = uri.userInfo
@@ -57,7 +57,7 @@ object VlessFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails * @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 = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
outboundBean?.settings?.vnext?.first()?.let { vnext -> outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty() vnext.address = profileItem.server.orEmpty()
@@ -67,31 +67,13 @@ object VlessFmt : FmtBase() {
vnext.users[0].flow = profileItem.flow vnext.users[0].flow = profileItem.flow
} }
val sni = outboundBean?.streamSettings?.populateTransportSettings( val sni = outboundBean?.streamSettings?.let {
profileItem.network.orEmpty(), V2rayConfigManager.populateTransportSettings(it, profileItem)
profileItem.headerType, }
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
outboundBean?.streamSettings?.populateTlsSettings( outboundBean?.streamSettings?.let {
profileItem.security.orEmpty(), V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
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
} }

View File

@@ -11,6 +11,7 @@ import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
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
@@ -33,7 +34,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)
@@ -43,7 +44,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
} }
@@ -73,6 +74,7 @@ object VmessFmt : FmtBase() {
config.serviceName = vmessQRCode.path config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host config.authority = vmessQRCode.host
} }
else -> {} else -> {}
} }
@@ -119,6 +121,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 -> {}
} }
@@ -148,7 +151,7 @@ object VmessFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri) val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.password = uri.userInfo config.password = uri.userInfo
@@ -166,7 +169,7 @@ object VmessFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails * @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 = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
outboundBean?.settings?.vnext?.first()?.let { vnext -> outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty() vnext.address = profileItem.server.orEmpty()
@@ -175,29 +178,13 @@ object VmessFmt : FmtBase() {
vnext.users[0].security = profileItem.method vnext.users[0].security = profileItem.method
} }
val sni = outboundBean?.streamSettings?.populateTransportSettings( val sni = outboundBean?.streamSettings?.let {
profileItem.network.orEmpty(), V2rayConfigManager.populateTransportSettings(it, profileItem)
profileItem.headerType, }
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.populateTlsSettings( outboundBean?.streamSettings?.let {
profileItem.security.orEmpty(), V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
profileItem.insecure == true, }
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
null,
null,
null
)
return outboundBean return outboundBean
} }

View File

@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -24,16 +25,16 @@ object WireguardFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri) val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()) config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo.orEmpty() config.secretKey = uri.userInfo.orEmpty()
config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4) config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.publicKey = queryParam["publickey"].orEmpty() config.publicKey = queryParam["publickey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"].orEmpty() config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.reserved = (queryParam["reserved"] ?: "0,0,0") config.reserved = queryParam["reserved"] ?: "0,0,0"
return config return config
} }
@@ -83,7 +84,7 @@ object WireguardFmt : FmtBase() {
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4 config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU) config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.publicKey = peerParams["publickey"].orEmpty() config.publicKey = peerParams["publickey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"].orEmpty() config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
val endpoint = peerParams["endpoint"].orEmpty() val endpoint = peerParams["endpoint"].orEmpty()
val endpointParts = endpoint.split(":", limit = 2) val endpointParts = endpoint.split(":", limit = 2)
if (endpointParts.size == 2) { if (endpointParts.size == 2) {
@@ -105,18 +106,18 @@ object WireguardFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails * @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 = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard -> outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",") wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.firstOrNull()?.let { peer -> wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty() peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey.orEmpty() peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() }
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}" peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
} }
wireguard.mtu = profileItem.mtu wireguard.mtu = profileItem.mtu
wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() } wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
} }
return outboundBean return outboundBean

View File

@@ -7,7 +7,9 @@ import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2 import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.* import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.fmt.Hysteria2Fmt import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.fmt.ShadowsocksFmt import com.v2ray.ang.fmt.ShadowsocksFmt
@@ -42,7 +44,7 @@ object AngConfigManager {
Utils.setClipboard(context, conf) Utils.setClipboard(context, conf)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to share config to clipboard", e)
return -1 return -1
} }
return 0 return 0
@@ -71,7 +73,7 @@ object AngConfigManager {
} }
return sb.lines().count() return sb.lines().count()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
return -1 return -1
} }
} }
@@ -91,7 +93,7 @@ object AngConfigManager {
return QRCodeDecoder.createQRCode(conf) return QRCodeDecoder.createQRCode(conf)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to share config as QR code", e)
return null return null
} }
} }
@@ -120,7 +122,7 @@ object AngConfigManager {
return -1 return -1
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
return -1 return -1
} }
return 0 return 0
@@ -148,7 +150,7 @@ object AngConfigManager {
EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config) EConfigType.HYSTERIA2 -> Hysteria2Fmt.toUri(config)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
return "" return ""
} }
} }
@@ -203,7 +205,7 @@ object AngConfigManager {
} }
return count return count
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to parse batch subscription", e)
} }
return 0 return 0
} }
@@ -251,7 +253,7 @@ object AngConfigManager {
} }
return count return count
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to parse batch config", e)
} }
return 0 return 0
} }
@@ -287,7 +289,7 @@ object AngConfigManager {
return count return count
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
} }
try { try {
@@ -298,7 +300,7 @@ object AngConfigManager {
MmkvManager.encodeServerRaw(key, server) MmkvManager.encodeServerRaw(key, server)
return 1 return 1
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
} }
return 0 return 0
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) { } else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
@@ -308,7 +310,7 @@ object AngConfigManager {
MmkvManager.encodeServerRaw(key, server) MmkvManager.encodeServerRaw(key, server)
return 1 return 1
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
} }
return 0 return 0
} else { } else {
@@ -372,7 +374,7 @@ 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
@@ -390,7 +392,7 @@ 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
@@ -417,21 +419,25 @@ object AngConfigManager {
if (!Utils.isValidUrl(url)) { if (!Utils.isValidUrl(url)) {
return 0 return 0
} }
Log.d(AppConfig.ANG_PACKAGE, url) if (!it.second.allowInsecureUrl) {
if (!Utils.isValidSubUrl(url)) {
return 0
}
}
Log.i(AppConfig.TAG, url)
var configText = try { var configText = try {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, 15000, 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 {
HttpUtil.getUrlContentWithUserAgent(url) HttpUtil.getUrlContentWithUserAgent(url)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
"" ""
} }
} }
@@ -440,7 +446,7 @@ 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
} }
} }

View File

@@ -3,7 +3,6 @@ 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
@@ -26,7 +25,7 @@ object MigrateManager {
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
@@ -43,9 +42,9 @@ 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
} }
@@ -172,7 +171,7 @@ object MigrateManager {
outbound.settings?.let { wireguard -> outbound.settings?.let { wireguard ->
config.secretKey = wireguard.secretKey config.secretKey = wireguard.secretKey
config.localAddress = (wireguard.address as List<*>).joinToString(",").removeWhiteSpace().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 = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString() config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString()

View File

@@ -571,7 +571,7 @@ object MmkvManager {
* @param startOnBoot Whether to start on boot. * @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)
} }
/** /**

View File

@@ -84,7 +84,7 @@ 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
} }
} }
@@ -167,11 +167,11 @@ object SettingsManager {
} }
val guid = MmkvManager.getSelectServer() ?: return false val guid = MmkvManager.getSelectServer() ?: return false
val config = MmkvManager.decodeServerConfig(guid) ?: return false val config = decodeServerConfig(guid) ?: return false
if (config.configType == EConfigType.CUSTOM) { if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) ?: return false val raw = MmkvManager.decodeServerRaw(guid) ?: return false
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java) val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }?.any { val exist = v2rayConfig.routing.rules.filter { 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
} }
return exist == true return exist == true
@@ -216,7 +216,7 @@ object SettingsManager {
* @return The ProfileItem. * @return The ProfileItem.
*/ */
fun getServerViaRemarks(remarks: String?): ProfileItem? { fun getServerViaRemarks(remarks: String?): ProfileItem? {
if (remarks == null) { if (remarks.isNullOrEmpty()) {
return null return null
} }
val serverList = decodeServerList() val serverList = decodeServerList()
@@ -242,7 +242,7 @@ object SettingsManager {
* @return The HTTP port. * @return The HTTP port.
*/ */
fun getHttpPort(): Int { fun getHttpPort(): Int {
return getSocksPort() + (if (Utils.isXray()) 0 else 1) return getSocksPort() + if (Utils.isXray()) 0 else 1
} }
/** /**
@@ -265,10 +265,7 @@ 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)
@@ -319,10 +316,10 @@ object SettingsManager {
*/ */
fun getDelayTestUrl(second: Boolean = false): String { fun getDelayTestUrl(second: Boolean = false): String {
return if (second) { return if (second) {
AppConfig.DelayTestUrl2 AppConfig.DELAY_TEST_URL2
} else { } else {
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
?: AppConfig.DelayTestUrl ?: AppConfig.DELAY_TEST_URL
} }
} }
@@ -343,6 +340,7 @@ object SettingsManager {
Language.VIETNAMESE -> Locale("vi") Language.VIETNAMESE -> Locale("vi")
Language.RUSSIAN -> Locale("ru") Language.RUSSIAN -> Locale("ru")
Language.PERSIAN -> Locale("fa") Language.PERSIAN -> Locale("fa")
Language.ARABIC -> Locale("ar")
Language.BANGLA -> Locale("bn") Language.BANGLA -> Locale("bn")
Language.BAKHTIARI -> Locale("bqi", "IR") Language.BAKHTIARI -> Locale("bqi", "IR")
} }

View File

@@ -6,8 +6,10 @@ 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.R import com.v2ray.ang.R
import com.v2ray.ang.dto.IPAPIInfo
import com.v2ray.ang.extension.responseLength import com.v2ray.ang.extension.responseLength
import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import libv2ray.Libv2ray import libv2ray.Libv2ray
import java.io.IOException import java.io.IOException
@@ -51,7 +53,7 @@ object SpeedtestManager {
return try { return try {
Libv2ray.measureOutboundDelay(config, SettingsManager.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
} }
} }
@@ -76,7 +78,7 @@ object SpeedtestManager {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to ping URL: $url", e)
} }
return "-1ms" return "-1ms"
} }
@@ -103,11 +105,11 @@ object SpeedtestManager {
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
} }
@@ -152,18 +154,26 @@ object SpeedtestManager {
) )
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e)) Log.e(AppConfig.TAG, "Connection test IOException", 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) {
Log.d(AppConfig.ANG_PACKAGE, "testConnection Exception: " + Log.getStackTraceString(e)) Log.e(AppConfig.TAG, "Connection test Exception", 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)
} }
fun getRemoteIPInfo(): String? {
val httpPort = SettingsManager.getHttpPort()
var content = HttpUtil.getUrlContent(AppConfig.IP_API_Url, 5000, httpPort) ?: return null
var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
return "(${ipInfo.country_code}) ${ipInfo.ip}"
}
/** /**
* Gets the version of the V2Ray library. * Gets the version of the V2Ray library.
* *

View File

@@ -0,0 +1,112 @@
package com.v2ray.ang.handler
import android.content.Context
import android.os.Build
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.dto.CheckUpdateResult
import com.v2ray.ang.dto.GitHubRelease
import com.v2ray.ang.extension.concatUrl
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
object UpdateCheckerManager {
suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
try {
val url = if (includePreRelease) {
AppConfig.APP_API_URL
} else {
AppConfig.APP_API_URL.concatUrl("latest")
}
var response = HttpUtil.getUrlContent(url, 5000)
if (response.isNullOrEmpty()) {
val httpPort = SettingsManager.getHttpPort()
response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response")
}
val latestRelease = if (includePreRelease) {
JsonUtil.fromJson(response, Array<GitHubRelease>::class.java)
.firstOrNull()
?: throw IllegalStateException("No pre-release found")
} else {
JsonUtil.fromJson(response, GitHubRelease::class.java)
}
val latestVersion = latestRelease.tagName.removePrefix("v")
Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})")
return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) {
val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0])
CheckUpdateResult(
hasUpdate = true,
latestVersion = latestVersion,
releaseNotes = latestRelease.body,
downloadUrl = downloadUrl,
isPreRelease = latestRelease.prerelease
)
} else {
CheckUpdateResult(hasUpdate = false)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
return@withContext CheckUpdateResult(hasUpdate = false, error = e.message)
}
}
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
try {
val httpPort = SettingsManager.getHttpPort()
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
?: throw IllegalStateException("Failed to create connection")
try {
val apkFile = File(context.cacheDir, "update.apk")
Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
FileOutputStream(apkFile).use { outputStream ->
connection.inputStream.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
Log.i(AppConfig.TAG, "APK download completed")
return@withContext apkFile
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
return@withContext null
} finally {
try {
connection.disconnect()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
return@withContext null
}
}
private fun compareVersions(version1: String, version2: String): Int {
val v1 = version1.split(".")
val v2 = version2.split(".")
for (i in 0 until maxOf(v1.size, v2.size)) {
val num1 = if (i < v1.size) v1[i].toInt() else 0
val num2 = if (i < v2.size) v2[i].toInt() else 0
if (num1 != num2) return num1 - num2
}
return 0
}
private fun getDownloadUrl(release: GitHubRelease, abi: String): String {
return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl
?: release.assets.firstOrNull()?.browserDownloadUrl
?: throw IllegalStateException("No compatible APK found")
}
}

View File

@@ -107,7 +107,7 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
addUpdateListener { animation -> addUpdateListener { animation ->
val value = animation.animatedValue as Float val value = animation.animatedValue as Float
viewHolder.itemView.translationX = value viewHolder.itemView.translationX = value
viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD)) viewHolder.itemView.alpha = 1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD)
} }
interpolator = DecelerateInterpolator() interpolator = DecelerateInterpolator()
duration = ANIMATION_DURATION duration = ANIMATION_DURATION
@@ -144,4 +144,4 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
private const val SWIPE_THRESHOLD = 0.25f private const val SWIPE_THRESHOLD = 0.25f
private const val ANIMATION_DURATION: Long = 200 private const val ANIMATION_DURATION: Long = 200
} }
} }

View File

@@ -35,7 +35,7 @@ class TaskerReceiver : BroadcastReceiver() {
V2RayServiceManager.stopVService(context) V2RayServiceManager.stopVService(context)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
} }
} }
} }

View File

@@ -158,6 +158,7 @@ object NotificationService {
mBuilder = null mBuilder = null
speedNotificationJob?.cancel() speedNotificationJob?.cancel()
speedNotificationJob = null speedNotificationJob = null
mNotificationManager = null
} }
/** /**

View File

@@ -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
@@ -16,7 +16,7 @@ class ProcessService {
* @param cmd The command to run. * @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)
@@ -27,14 +27,14 @@ 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)
} }
} }
@@ -43,10 +43,10 @@ class ProcessService {
*/ */
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)
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
@@ -61,7 +62,7 @@ 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)
} }
} }

View File

@@ -38,7 +38,7 @@ object SubscriptionUpdater {
*/ */
@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 }
@@ -56,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}")
} }

View File

@@ -27,7 +27,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
* @return The start mode. * @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.startCoreLoop()
return START_STICKY return START_STICKY
} }
@@ -36,7 +36,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
*/ */
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
V2RayServiceManager.stopV2rayPoint() V2RayServiceManager.stopCoreLoop()
} }
/** /**

View File

@@ -9,13 +9,13 @@ import android.os.Build
import android.util.Log import android.util.Log
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.R import com.v2ray.ang.R
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.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
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
@@ -24,14 +24,14 @@ import go.Seq
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import libv2ray.CoreCallbackHandler
import libv2ray.CoreController
import libv2ray.Libv2ray import libv2ray.Libv2ray
import libv2ray.V2RayPoint
import libv2ray.V2RayVPNServiceSupportsSet
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
object V2RayServiceManager { object V2RayServiceManager {
private val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback())
private val mMsgReceive = ReceiveMessageHandler() private val mMsgReceive = ReceiveMessageHandler()
private var currentConfig: ProfileItem? = null private var currentConfig: ProfileItem? = null
@@ -39,7 +39,7 @@ object V2RayServiceManager {
set(value) { set(value) {
field = value field = value
Seq.setContext(value?.get()?.getService()?.applicationContext) Seq.setContext(value?.get()?.getService()?.applicationContext)
Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey()) Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
} }
/** /**
@@ -81,7 +81,7 @@ object V2RayServiceManager {
* Checks if the V2Ray service is running. * Checks if the V2Ray service is running.
* @return True if the service is running, false otherwise. * @return True if the service is running, false otherwise.
*/ */
fun isRunning() = v2rayPoint.isRunning fun isRunning() = coreController.isRunning
/** /**
* Gets the name of the currently running server. * Gets the name of the currently running server.
@@ -91,10 +91,13 @@ object V2RayServiceManager {
/** /**
* Starts the context service for V2Ray. * Starts the context service for V2Ray.
* Chooses between VPN service or Proxy-only service based on user settings.
* @param context The context from which the service is started. * @param context The context from which the service is started.
*/ */
private fun startContextService(context: Context) { private fun startContextService(context: Context) {
if (v2rayPoint.isRunning) return if (coreController.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 (config.configType != EConfigType.CUSTOM if (config.configType != EConfigType.CUSTOM
@@ -124,18 +127,19 @@ object V2RayServiceManager {
/** /**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int): * 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. * Starts the V2Ray core service.
*/ */
fun startV2rayPoint() { fun startCoreLoop(): Boolean {
val service = getService() ?: return if (coreController.isRunning) {
val guid = MmkvManager.getSelectServer() ?: return return false
val config = MmkvManager.decodeServerConfig(guid) ?: return
if (v2rayPoint.isRunning) {
return
} }
val service = getService() ?: return false
val guid = MmkvManager.getSelectServer() ?: return false
val config = MmkvManager.decodeServerConfig(guid) ?: return false
val result = V2rayConfigManager.getV2rayConfig(service, guid) val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status) if (!result.status)
return return false
try { try {
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE) val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
@@ -144,42 +148,52 @@ 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)
return false
} }
v2rayPoint.configureFileContent = result.content
v2rayPoint.domainName = result.domainPort
currentConfig = config currentConfig = config
try { try {
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) coreController.startLoop(result.content)
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, "Failed to start Core loop", e)
return false
} }
if (v2rayPoint.isRunning) { if (coreController.isRunning == false) {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
NotificationService.showNotification(currentConfig)
PluginUtil.runPlugin(service, config, result.domainPort)
} else {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
NotificationService.cancelNotification() NotificationService.cancelNotification()
return false
} }
try {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
NotificationService.showNotification(currentConfig)
NotificationService.startSpeedNotification(currentConfig)
PluginUtil.runPlugin(service, config, result.socksPort)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to startup service", e)
return false
}
return true
} }
/** /**
* Stops the V2Ray point. * Stops the V2Ray core service.
* Unregisters broadcast receivers, stops notifications, and shuts down plugins.
* @return True if the core was stopped successfully, false otherwise.
*/ */
fun stopV2rayPoint() { fun stopCoreLoop(): Boolean {
val service = getService() ?: return val service = getService() ?: return false
if (v2rayPoint.isRunning) { if (coreController.isRunning) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
v2rayPoint.stopLoop() coreController.stopLoop()
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
} }
} }
} }
@@ -190,9 +204,11 @@ object V2RayServiceManager {
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()
return true
} }
/** /**
@@ -202,40 +218,52 @@ object V2RayServiceManager {
* @return The statistics value. * @return The statistics value.
*/ */
fun queryStats(tag: String, link: String): Long { fun queryStats(tag: String, link: String): Long {
return v2rayPoint.queryStats(tag, link) return coreController.queryStats(tag, link)
} }
/** /**
* Measures the delay for V2Ray. * Measures the connection delay for the current V2Ray configuration.
* Tests with primary URL first, then falls back to alternative URL if needed.
* Also fetches remote IP information if the delay test was successful.
*/ */
private fun measureV2rayDelay() { private fun measureV2rayDelay() {
if (coreController.isRunning == false) {
return
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val service = getService() ?: return@launch val service = getService() ?: return@launch
var time = -1L var time = -1L
var errstr = "" var errorStr = ""
if (v2rayPoint.isRunning) {
try { try {
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl()) time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e") Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
errstr = e.message?.substringAfter("\":") ?: "empty message" errorStr = e.message?.substringAfter("\":") ?: "empty message"
}
if (time == -1L) {
try {
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl(true))
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
}
} }
val result = if (time == -1L) { if (time == -1L) {
service.getString(R.string.connection_test_error, errstr) try {
} else { time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
service.getString(R.string.connection_test_available, time) } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message"
}
} }
val result = if (time >= 0) {
service.getString(R.string.connection_test_available, time)
} else {
service.getString(R.string.connection_test_error, errorStr)
}
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result) MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
// Only fetch IP info if the delay test was successful
if (time >= 0) {
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
}
}
} }
} }
@@ -247,59 +275,53 @@ object V2RayServiceManager {
return serviceControl?.get()?.getService() return serviceControl?.get()?.getService()
} }
private class V2RayCallback : V2RayVPNServiceSupportsSet { /**
* Core callback handler implementation for handling V2Ray core events.
* Handles startup, shutdown, socket protection, and status emission.
*/
private class CoreCallback : CoreCallbackHandler {
/**
* Called when V2Ray core starts up.
* @return 0 for success, any other value for failure.
*/
override fun startup(): Long {
return 0
}
/**
* Called when V2Ray core shuts down.
* @return 0 for success, any other value for failure.
*/
override fun shutdown(): Long { override fun shutdown(): Long {
val serviceControl = serviceControl?.get() ?: return -1 val serviceControl = serviceControl?.get() ?: return -1
// called by go
return try { return try {
serviceControl.stopService() serviceControl.stopService()
0 0
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString()) Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
-1 -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. * Called when V2Ray core emits status information.
* @param l The status code. * @param l Status code.
* @param s The status message. * @param s Status message.
* @return The status code. * @return Always returns 0.
*/ */
override fun onEmitStatus(l: Long, s: String?): Long { override fun onEmitStatus(l: Long, s: String?): Long {
return 0 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.d(ANG_PACKAGE, e.toString())
-1
}
}
} }
/**
* Broadcast receiver for handling messages sent to the service.
* Handles registration, service control, and screen events.
*/
private class ReceiveMessageHandler : BroadcastReceiver() { private class ReceiveMessageHandler : BroadcastReceiver() {
/** /**
* Handles received broadcast messages. * Handles received broadcast messages.
* Processes service control messages and screen state changes.
* @param ctx The context in which the receiver is running. * @param ctx The context in which the receiver is running.
* @param intent The intent being received. * @param intent The intent being received.
*/ */
@@ -307,7 +329,7 @@ object V2RayServiceManager {
val serviceControl = serviceControl?.get() ?: return val serviceControl = serviceControl?.get() ?: return
when (intent?.getIntExtra("key", 0)) { when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_REGISTER_CLIENT -> { AppConfig.MSG_REGISTER_CLIENT -> {
if (v2rayPoint.isRunning) { if (coreController.isRunning) {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "") MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
} else { } else {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "") MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
@@ -323,12 +345,12 @@ object V2RayServiceManager {
} }
AppConfig.MSG_STATE_STOP -> { AppConfig.MSG_STATE_STOP -> {
Log.d(ANG_PACKAGE, "Stop Service") Log.i(AppConfig.TAG, "Stop Service")
serviceControl.stopService() serviceControl.stopService()
} }
AppConfig.MSG_STATE_RESTART -> { AppConfig.MSG_STATE_RESTART -> {
Log.d(ANG_PACKAGE, "Restart Service") Log.i(AppConfig.TAG, "Restart Service")
serviceControl.stopService() serviceControl.stopService()
Thread.sleep(500L) Thread.sleep(500L)
startVService(serviceControl.getService()) startVService(serviceControl.getService())
@@ -341,12 +363,12 @@ 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")
NotificationService.stopSpeedNotification(currentConfig) 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")
NotificationService.startSpeedNotification(currentConfig) NotificationService.startSpeedNotification(currentConfig)
} }
} }

View File

@@ -32,7 +32,7 @@ class V2RayTestService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Seq.setContext(this) Seq.setContext(this)
Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey()) Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
} }
/** /**
@@ -81,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 SpeedtestManager.realPing(config.content) return SpeedtestManager.realPing(configResult.content)
} }
} }
} }

View File

@@ -18,10 +18,8 @@ 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.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.MyContextWrapper import com.v2ray.ang.util.MyContextWrapper
@@ -40,6 +38,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:14:1" private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:14:1"
private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:14: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
@@ -105,7 +104,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
V2RayServiceManager.startV2rayPoint() if (V2RayServiceManager.startCoreLoop()) {
startService()
}
return START_STICKY return START_STICKY
//return super.onStartCommand(intent, flags, startId) //return super.onStartCommand(intent, flags, startId)
} }
@@ -166,7 +167,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER) //builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
val bypassLan = SettingsManager.routingRulesetsBypassLan() val bypassLan = SettingsManager.routingRulesetsBypassLan()
if (bypassLan) { if (bypassLan) {
resources.getStringArray(R.array.bypass_private_ip_address).forEach { AppConfig.BYPASS_PRIVATE_IP_LIST.forEach {
val addr = it.split('/') val addr = it.split('/')
builder.addRoute(addr[0], addr[1].toInt()) builder.addRoute(addr[0], addr[1].toInt())
} }
@@ -209,7 +210,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 {
@@ -227,7 +228,7 @@ 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)
} }
} }
@@ -245,7 +246,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
return true 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 return false
@@ -256,6 +257,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
* Starts the tun2socks process with the appropriate parameters. * Starts the tun2socks process with the appropriate parameters.
*/ */
private fun runTun2socks() { private fun runTun2socks() {
Log.i(AppConfig.TAG, "Start run $TUN2SOCKS")
val socksPort = SettingsManager.getSocksPort() val socksPort = SettingsManager.getSocksPort()
val cmd = arrayListOf( val cmd = arrayListOf(
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath, File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
@@ -277,7 +279,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)
@@ -286,19 +288,19 @@ 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, "$TUN2SOCKS process info : ${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)
} }
} }
@@ -309,13 +311,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
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, "LocalSocket path : $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, "LocalSocket 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))
@@ -323,7 +325,7 @@ 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
} }
@@ -349,13 +351,13 @@ 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.stopCoreLoop()
if (isForced) { if (isForced) {
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped //stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
@@ -367,8 +369,8 @@ 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)
} }
} }
} }

View File

@@ -5,18 +5,28 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
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.dto.CheckUpdateResult
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.SpeedtestManager import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.handler.UpdateCheckerManager
import com.v2ray.ang.util.AppManagerUtil
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 kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@@ -32,7 +42,7 @@ class AboutActivity : BaseActivity() {
try { try {
showFileChooser() showFileChooser()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to show file chooser", e)
} }
} else { } else {
toast(R.string.toast_permission_denied) toast(R.string.toast_permission_denied)
@@ -50,9 +60,9 @@ class AboutActivity : BaseActivity() {
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)
} }
} }
@@ -72,7 +82,7 @@ class AboutActivity : BaseActivity() {
) )
) )
} else { } else {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
@@ -88,23 +98,40 @@ class AboutActivity : BaseActivity() {
try { try {
showFileChooser() showFileChooser()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to show file chooser", e)
} }
} else { } else {
requestPermissionLauncher.launch(permission) requestPermissionLauncher.launch(permission)
} }
} }
//If it is the Google Play version, not be displayed within 1 days after update
// if (Utils.isGoogleFlavor()) {
// val lastUpdateTime = AppManagerUtil.getLastUpdateTime(this)
// val currentTime = System.currentTimeMillis()
// if ((currentTime - lastUpdateTime) < 1 * 24 * 60 * 60 * 1000L) {
// binding.layoutCheckUpdate.visibility = View.GONE
// }
// }
binding.layoutCheckUpdate.setOnClickListener {
checkForUpdates(binding.checkPreRelease.isChecked)
}
binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
}
binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)
binding.layoutSoureCcode.setOnClickListener { binding.layoutSoureCcode.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGUrl) Utils.openUri(this, AppConfig.APP_URL)
} }
binding.layoutFeedback.setOnClickListener { binding.layoutFeedback.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGIssues) Utils.openUri(this, AppConfig.APP_ISSUES_URL)
} }
binding.layoutOssLicenses.setOnClickListener { binding.layoutOssLicenses.setOnClickListener {
val webView = android.webkit.WebView(this); val webView = android.webkit.WebView(this)
webView.loadUrl("file:///android_asset/open_source_licenses.html") webView.loadUrl("file:///android_asset/open_source_licenses.html")
android.app.AlertDialog.Builder(this) android.app.AlertDialog.Builder(this)
.setTitle("Open source licenses") .setTitle("Open source licenses")
@@ -114,11 +141,11 @@ class AboutActivity : BaseActivity() {
} }
binding.layoutTgChannel.setOnClickListener { binding.layoutTgChannel.setOnClickListener {
Utils.openUri(this, AppConfig.TgChannelUrl) Utils.openUri(this, AppConfig.TG_CHANNEL_URL)
} }
binding.layoutPrivacyPolicy.setOnClickListener { binding.layoutPrivacyPolicy.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy) Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY)
} }
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also { "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
@@ -126,7 +153,7 @@ class AboutActivity : BaseActivity() {
} }
} }
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()
@@ -147,7 +174,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)) {
@@ -167,7 +194,7 @@ 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)
} }
} }
@@ -185,14 +212,38 @@ 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)
} }
} }
} }
private fun checkForUpdates(includePreRelease: Boolean) {
lifecycleScope.launch {
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
if (result.hasUpdate) {
showUpdateDialog(result)
} else {
toast(R.string.update_already_latest_version)
}
}
}
private fun showUpdateDialog(result: CheckUpdateResult) {
AlertDialog.Builder(this)
.setTitle(getString(R.string.update_new_version_found, result.latestVersion))
.setMessage(result.releaseNotes)
.setPositiveButton(R.string.update_now) { _, _ ->
result.downloadUrl?.let {
Utils.openUri(this, it)
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
} }

View File

@@ -1,16 +1,19 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.annotation.SuppressLint
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.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 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
@@ -21,7 +24,7 @@ import java.io.IOException
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) } private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
var logsetsAll: MutableList<String> = mutableListOf() private var logsetsAll: MutableList<String> = mutableListOf()
var logsets: MutableList<String> = mutableListOf() var logsets: MutableList<String> = mutableListOf()
private val adapter by lazy { LogcatRecyclerAdapter(this) } private val adapter by lazy { LogcatRecyclerAdapter(this) }
@@ -62,12 +65,12 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
logsetsAll = allText.toMutableList() logsetsAll = allText.toMutableList()
logsets = allText.toMutableList() logsets = allText.toMutableList()
adapter.notifyDataSetChanged() refreshData()
binding.refreshLayout.isRefreshing = false binding.refreshLayout.isRefreshing = false
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to get logcat", e)
} }
} }
@@ -84,11 +87,11 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
logsetsAll.clear() logsetsAll.clear()
logsets.clear() logsets.clear()
adapter.notifyDataSetChanged() refreshData()
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to clear logcat", e)
} }
} }
@@ -118,7 +121,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
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, logsets.joinToString("\n")) Utils.setClipboard(this, logsets.joinToString("\n"))
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
true true
} }
@@ -138,11 +141,16 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
logsetsAll.filter { it.contains(key) }.toMutableList() logsetsAll.filter { it.contains(key) }.toMutableList()
} }
adapter?.notifyDataSetChanged() refreshData()
return true return true
} }
override fun onRefresh() { override fun onRefresh() {
getLogcat() getLogcat()
} }
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
adapter.notifyDataSetChanged()
}
} }

View File

@@ -1,13 +1,16 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() { class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
private var mActivity: LogcatActivity = activity private var mActivity: LogcatActivity = activity
override fun getItemCount() = mActivity.logsets.size override fun getItemCount() = mActivity.logsets.size
override fun onBindViewHolder(holder: MainViewHolder, position: Int) { override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
@@ -22,7 +25,7 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else "" holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Error binding log view data", e)
} }
} }

View File

@@ -1,6 +1,7 @@
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.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.ColorStateList import android.content.res.ColorStateList
@@ -8,6 +9,7 @@ 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.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
@@ -31,6 +33,7 @@ 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
@@ -84,9 +87,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
Action.IMPORT_QR_CODE_CONFIG -> Action.IMPORT_QR_CODE_CONFIG ->
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
// Action.IMPORT_QR_CODE_URL ->
// scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
Action.READ_CONTENT_FROM_URI -> Action.READ_CONTENT_FROM_URI ->
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*" type = "*/*"
@@ -107,8 +107,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
enum class Action { enum class Action {
NONE, NONE,
IMPORT_QR_CODE_CONFIG, IMPORT_QR_CODE_CONFIG,
//IMPORT_QR_CODE_URL,
READ_CONTENT_FROM_URI, READ_CONTENT_FROM_URI,
POST_NOTIFICATIONS POST_NOTIFICATIONS
} }
@@ -126,12 +124,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
// 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)
@@ -204,6 +196,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}) })
} }
@SuppressLint("NotifyDataSetChanged")
private fun setupViewModel() { private fun setupViewModel() {
mainViewModel.updateListAction.observe(this) { index -> mainViewModel.updateListAction.observe(this) { index ->
if (index >= 0) { if (index >= 0) {
@@ -269,7 +262,7 @@ 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
@@ -277,7 +270,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
V2RayServiceManager.startVService(this) V2RayServiceManager.startVService(this)
} }
fun restartV2Ray() { private fun restartV2Ray() {
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this) V2RayServiceManager.stopVService(this)
} }
@@ -321,7 +314,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.import_qrcode -> { R.id.import_qrcode -> {
importQRcode(true) importQRcode()
true true
} }
@@ -375,26 +368,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
true true
} }
// R.id.import_config_custom_clipboard -> {
// importConfigCustomClipboard()
// true
// }
//
// R.id.import_config_custom_local -> {
// importConfigCustomLocal()
// true
// }
//
// R.id.import_config_custom_url -> {
// importConfigCustomUrlClipboard()
// true
// }
//
// R.id.import_config_custom_url_scan -> {
// importQRcode(false)
// true
// }
R.id.export_all -> { R.id.export_all -> {
exportAll() exportAll()
true true
@@ -458,16 +431,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/** /**
* import config from qrcode * import config from qrcode
*/ */
private fun importQRcode(forConfig: Boolean): Boolean { private fun importQRcode(): Boolean {
val permission = Manifest.permission.CAMERA val permission = Manifest.permission.CAMERA
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
if (forConfig) { scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
} else {
//scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
}
} else { } else {
pendingAction = Action.IMPORT_QR_CODE_CONFIG//if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL pendingAction = Action.IMPORT_QR_CODE_CONFIG
requestPermissionLauncher.launch(permission) requestPermissionLauncher.launch(permission)
} }
return true return true
@@ -482,7 +451,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
@@ -503,102 +472,34 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
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)
} }
} }
} }
/**
* import config from local config file
*/
private fun importConfigLocal(): 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 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
*/
// 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
*/
// private fun importConfigCustomUrl(url: String?): Boolean {
// try {
// if (!Utils.isValidUrl(url)) {
// toast(R.string.toast_invalid_url)
// return false
// }
// lifecycleScope.launch(Dispatchers.IO) {
// val configText = try {
// HttpUtil.getUrlContentWithUserAgent(url)
// } catch (e: Exception) {
// e.printStackTrace()
// ""
// }
// launch(Dispatchers.Main) {
// importCustomizeConfig(configText)
// }
// }
// } catch (e: Exception) {
// e.printStackTrace()
// return false
// }
// return true
// }
/** /**
* import config from sub * import config from sub
*/ */
@@ -613,7 +514,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
toast(getString(R.string.title_update_config_count, count)) 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)
} }
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
@@ -629,7 +530,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
if (ret > 0) if (ret > 0)
toast(getString(R.string.title_export_config_count, ret)) toast(getString(R.string.title_export_config_count, ret))
else else
toast(R.string.toast_failure) toastError(R.string.toast_failure)
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
} }
@@ -741,36 +642,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
importBatchConfig(input?.bufferedReader()?.readText()) importBatchConfig(input?.bufferedReader()?.readText())
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to read content from URI", e)
} }
} else { } else {
requestPermissionLauncher.launch(permission) requestPermissionLauncher.launch(permission)
} }
} }
// /**
// * 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()
// 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
// }
// }
private fun setTestState(content: String?) { private fun setTestState(content: String?) {
binding.tvTestState.text = content binding.tvTestState.text = content
} }
@@ -797,14 +675,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
// Handle navigation view item clicks here. // Handle navigation view item clicks here.
when (item.itemId) { when (item.itemId) {
R.id.sub_setting -> requestSubSettingActivity.launch(Intent(this, SubSettingActivity::class.java)) R.id.sub_setting -> 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( R.id.settings -> startActivity(
Intent(this, SettingsActivity::class.java) Intent(this, SettingsActivity::class.java)
.putExtra("isRunning", mainViewModel.isRunning.value == true) .putExtra("isRunning", mainViewModel.isRunning.value == true)
) )
R.id.per_app_proxy_settings -> startActivity(Intent(this, PerAppProxyActivity::class.java)) R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
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.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
R.id.about -> startActivity(Intent(this, AboutActivity::class.java)) R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
} }

View File

@@ -3,6 +3,7 @@ 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
@@ -19,11 +20,14 @@ 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.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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -43,6 +47,10 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
var isRunning = false var isRunning = false
private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, 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) {
@@ -128,6 +136,12 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
// } // }
} }
/**
* 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 { private fun getAddress(profile: ProfileItem): String {
// Hide xxx:xxx:***/xxx.xxx.xxx.*** // Hide xxx:xxx:***/xxx.xxx.xxx.***
return "${ return "${
@@ -140,6 +154,11 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
} : ${profile.serverPort}" } : ${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 { private fun getSubscriptionRemarks(profile: ProfileItem): String {
val subRemarks = val subRemarks =
if (mActivity.mainViewModel.subscriptionId.isEmpty()) if (mActivity.mainViewModel.subscriptionId.isEmpty())
@@ -149,6 +168,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
return subRemarks?.toString() ?: "" 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) { private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List<String>, skip: Int) {
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i -> AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
try { try {
@@ -161,33 +189,56 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
else -> mActivity.toast("else") else -> mActivity.toast("else")
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Error when sharing server", e)
} }
}.show() }.show()
} }
/**
* Displays QR code for the server configuration
* @param guid The server unique identifier
*/
private fun showQRCode(guid: String) { private fun showQRCode(guid: String) {
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity)) val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid)) ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
AlertDialog.Builder(mActivity).setView(ivBinding.root).show() AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
} }
/**
* Shares server configuration to clipboard
* @param guid The server unique identifier
*/
private fun share2Clipboard(guid: String) { private fun share2Clipboard(guid: String) {
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) { if (AngConfigManager.share2Clipboard(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)
} }
} }
/**
* 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) { mActivity.lifecycleScope.launch(Dispatchers.IO) {
mActivity.toast(R.string.toast_success) val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
} else { launch(Dispatchers.Main) {
mActivity.toast(R.string.toast_failure) if (result == 0) {
mActivity.toastSuccess(R.string.toast_success)
} else {
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) { private fun editServer(guid: String, profile: ProfileItem) {
val intent = Intent().putExtra("guid", guid) val intent = Intent().putExtra("guid", guid)
.putExtra("isRunning", isRunning) .putExtra("isRunning", isRunning)
@@ -199,6 +250,12 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
} }
} }
/**
* 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 (guid != MmkvManager.getSelectServer()) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
@@ -218,12 +275,22 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
} }
} }
/**
* 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) { 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) { private fun setSelectServer(guid: String) {
val selected = MmkvManager.getSelectServer() val selected = MmkvManager.getSelectServer()
if (guid != selected) { if (guid != selected) {
@@ -239,7 +306,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
delay(500) delay(500)
V2RayServiceManager.startVService(mActivity) V2RayServiceManager.startVService(mActivity)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e)
} }
} }
} }

View File

@@ -1,10 +1,12 @@
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.widget.Toast
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
@@ -13,21 +15,21 @@ 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.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.HttpUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import es.dmoral.toasty.Toasty
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.text.Collator import java.text.Collator
class PerAppProxyActivity : BaseActivity() { class PerAppProxyActivity : BaseActivity() {
private val binding by lazy { private val binding by lazy { 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
@@ -51,13 +53,13 @@ class PerAppProxyActivity : BaseActivity() {
appsList.forEach { app -> appsList.forEach { app ->
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0 app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
} }
appsList.sortedWith(Comparator { p1, p2 -> appsList.sortedWith { 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 { } else {
val collator = Collator.getInstance() val collator = Collator.getInstance()
appsList.sortedWith(compareBy(collator) { it.appName }) appsList.sortedWith(compareBy(collator) { it.appName })
@@ -83,6 +85,10 @@ 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)
binding.layoutSwitchBypassAppsTips.setOnClickListener {
Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show()
}
} }
override fun onPause() { override fun onPause() {
@@ -112,8 +118,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 {
@@ -152,7 +160,7 @@ class PerAppProxyActivity : BaseActivity() {
toast(R.string.msg_downloading_content) toast(R.string.msg_downloading_content)
binding.pbWaiting.show() binding.pbWaiting.show()
val url = AppConfig.androidpackagenamelistUrl val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000) var content = HttpUtil.getUrlContent(url, 5000)
if (content.isNullOrEmpty()) { if (content.isNullOrEmpty()) {
@@ -160,9 +168,9 @@ class PerAppProxyActivity : BaseActivity() {
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: "" 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() binding.pbWaiting.hide()
} }
} }
@@ -172,7 +180,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() {
@@ -182,9 +190,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)) {
@@ -197,10 +206,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)
@@ -210,10 +219,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)
@@ -224,7 +233,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
@@ -259,7 +268,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()
}
} }

View File

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

View File

@@ -1,12 +1,12 @@
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.widget.AdapterView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -17,6 +17,8 @@ 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
@@ -63,15 +65,9 @@ class RoutingSettingActivity : BaseActivity() {
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter)) mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView) mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "") binding.tvDomainStrategySummary.text = getDomainStrategy()
found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) } binding.layoutDomainStrategy.setOnClickListener {
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { setDomainStrategy()
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position])
}
} }
} }
@@ -87,7 +83,6 @@ class RoutingSettingActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true } R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true }
R.id.user_asset_setting -> startActivity(Intent(this, UserAssetActivity::class.java)).let { true }
R.id.import_predefined_rulesets -> importPredefined().let { true } R.id.import_predefined_rulesets -> importPredefined().let { true }
R.id.import_rulesets_from_clipboard -> importFromClipboard().let { 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.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true }
@@ -95,6 +90,22 @@ class RoutingSettingActivity : BaseActivity() {
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
private fun getDomainStrategy(): String {
return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first()
}
private fun setDomainStrategy() {
android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i ->
try {
val value = routing_domain_strategy[i]
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
binding.tvDomainStrategySummary.text = value
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to set domain strategy", e)
}
}.show()
}
private fun importPredefined() { private fun importPredefined() {
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i -> AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip) AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
@@ -104,11 +115,11 @@ class RoutingSettingActivity : BaseActivity() {
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i) SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
refreshData() refreshData()
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
} }
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
@@ -124,8 +135,8 @@ class RoutingSettingActivity : BaseActivity() {
val clipboard = try { val clipboard = try {
Utils.getClipboard(this) Utils.getClipboard(this)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
toast(R.string.toast_failure) toastError(R.string.toast_failure)
return@setPositiveButton return@setPositiveButton
} }
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@@ -133,9 +144,9 @@ 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)
} }
} }
} }
@@ -149,10 +160,10 @@ class RoutingSettingActivity : BaseActivity() {
private fun export2Clipboard() { private fun export2Clipboard() {
val rulesetList = MmkvManager.decodeRoutingRulesets() val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) { if (rulesetList.isNullOrEmpty()) {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} else { } else {
Utils.setClipboard(this, JsonUtil.toJson(rulesetList)) Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
} }
} }
@@ -170,9 +181,9 @@ 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)
} }
} }
} }
@@ -184,6 +195,7 @@ class RoutingSettingActivity : BaseActivity() {
return true return true
} }
@SuppressLint("NotifyDataSetChanged")
fun refreshData() { fun refreshData() {
rulesets.clear() rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf()) rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())

View File

@@ -6,6 +6,8 @@ import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
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() {
@@ -27,7 +29,7 @@ class ScScannerActivity : BaseActivity() {
importQRcode() importQRcode()
} }
fun importQRcode(): Boolean { private fun importQRcode(): Boolean {
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
return true return true
} }
@@ -38,9 +40,9 @@ class ScScannerActivity : BaseActivity() {
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))

View File

@@ -6,6 +6,7 @@ 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
@@ -21,6 +22,7 @@ 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()) { private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data val uri = it.data?.data
@@ -37,7 +39,7 @@ class ScannerActivity : BaseActivity() {
finished(text) finished(text)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
toast(R.string.toast_decoding_failed) toast(R.string.toast_decoding_failed)
} }
} }

View File

@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -14,13 +13,11 @@ 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
import com.v2ray.ang.AppConfig.TLS import com.v2ray.ang.AppConfig.TLS
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.EConfigType
@@ -28,6 +25,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
@@ -328,7 +326,7 @@ class ServerActivity : BaseActivity() {
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty()) et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0") et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
et_local_address?.text = Utils.getEditable( et_local_address?.text = Utils.getEditable(
config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6" config.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4
) )
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU) et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
} else if (config.configType == EConfigType.HYSTERIA2) { } else if (config.configType == EConfigType.HYSTERIA2) {
@@ -354,11 +352,11 @@ 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)
utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) } 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)
alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) } alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) }
} }
@@ -421,7 +419,7 @@ class ServerActivity : BaseActivity() {
et_public_key?.text = null et_public_key?.text = null
et_reserved1?.text = Utils.getEditable("0,0,0") et_reserved1?.text = Utils.getEditable("0,0,0")
et_local_address?.text = et_local_address?.text =
Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}") Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4)
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU) et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
return true return true
} }
@@ -480,9 +478,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
} }

View File

@@ -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,7 +78,7 @@ 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)
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}") 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
} }

View File

@@ -161,7 +161,7 @@ class SettingsActivity : BaseActivity() {
} }
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.DELAY_TEST_URL else nval
true true
} }
mode?.setOnPreferenceChangeListener { _, newValue -> mode?.setOnPreferenceChangeListener { _, newValue ->
@@ -202,7 +202,7 @@ class SettingsActivity : BaseActivity() {
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) 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.DELAY_TEST_URL)
initSharedPreference() initSharedPreference()
} }
@@ -364,6 +364,6 @@ class SettingsActivity : BaseActivity() {
} }
fun onModeHelpClicked(view: View) { fun onModeHelpClicked(view: View) {
Utils.openUri(this, AppConfig.v2rayNGWikiMode) Utils.openUri(this, AppConfig.APP_WIKI_MODE)
} }
} }

View File

@@ -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)
@@ -45,6 +46,7 @@ class SubEditActivity : BaseActivity() {
binding.etFilter.text = Utils.getEditable(subItem.filter) binding.etFilter.text = Utils.getEditable(subItem.filter)
binding.chkEnable.isChecked = subItem.enabled binding.chkEnable.isChecked = subItem.enabled
binding.autoUpdateCheck.isChecked = subItem.autoUpdate binding.autoUpdateCheck.isChecked = subItem.autoUpdate
binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile) binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile) binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
return true return true
@@ -76,6 +78,7 @@ class SubEditActivity : BaseActivity() {
subItem.autoUpdate = binding.autoUpdateCheck.isChecked subItem.autoUpdate = binding.autoUpdateCheck.isChecked
subItem.prevProfile = binding.etPreProfile.text.toString() subItem.prevProfile = binding.etPreProfile.text.toString()
subItem.nextProfile = binding.etNextProfile.text.toString() subItem.nextProfile = binding.etNextProfile.text.toString()
subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked
if (TextUtils.isEmpty(subItem.remarks)) { if (TextUtils.isEmpty(subItem.remarks)) {
toast(R.string.sub_setting_remarks) toast(R.string.sub_setting_remarks)
@@ -89,12 +92,14 @@ class SubEditActivity : BaseActivity() {
if (!Utils.isValidSubUrl(subItem.url)) { if (!Utils.isValidSubUrl(subItem.url)) {
toast(R.string.toast_insecure_url_protocol) toast(R.string.toast_insecure_url_protocol)
//return false if (!subItem.allowInsecureUrl) {
return false
}
} }
} }
MmkvManager.encodeSubscription(editSubId, subItem) MmkvManager.encodeSubscription(editSubId, subItem)
toast(R.string.toast_success) toastSuccess(R.string.toast_success)
finish() finish()
return true return true
} }

View File

@@ -1,5 +1,6 @@
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
@@ -10,7 +11,8 @@ 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.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
@@ -64,9 +66,9 @@ class SubSettingActivity : BaseActivity() {
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)
} }
binding.pbWaiting.hide() binding.pbWaiting.hide()
} }
@@ -79,6 +81,7 @@ class SubSettingActivity : BaseActivity() {
} }
@SuppressLint("NotifyDataSetChanged")
fun refreshData() { fun refreshData() {
subscriptions = MmkvManager.decodeSubscriptions() subscriptions = MmkvManager.decodeSubscriptions()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()

View File

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

View File

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

View File

@@ -5,9 +5,11 @@ 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 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -44,7 +46,7 @@ class UrlSchemeActivity : BaseActivity() {
} }
else -> { else -> {
toast(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
} }
@@ -53,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)
} }
} }
@@ -61,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)
@@ -69,7 +71,7 @@ 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)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false) val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@@ -21,11 +21,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
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.ActivitySubSettingBinding import com.v2ray.ang.databinding.ActivityUserAssetBinding
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.concatUrl
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.HttpUtil
@@ -40,7 +43,7 @@ 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 { ActivityUserAssetBinding.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")
@@ -87,11 +90,16 @@ class UserAssetActivity : BaseActivity() {
binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.layoutManager = LinearLayoutManager(this)
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider) addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = UserAssetAdapter() binding.recyclerView.adapter = UserAssetAdapter()
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
binding.layoutGeoFilesSources.setOnClickListener {
setGeoFilesSources()
}
} }
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 {
@@ -108,6 +116,22 @@ class UserAssetActivity : BaseActivity() {
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
private fun getGeoFilesSources(): String {
return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first()
}
private fun setGeoFilesSources() {
AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i ->
try {
val value = AppConfig.GEO_FILES_SOURCES[i]
MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
binding.tvGeoFilesSourcesSummary.text = value
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to set geo files sources", e)
}
}.show()
}
private fun showFileChooser() { private fun showFileChooser() {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES Manifest.permission.READ_MEDIA_IMAGES
@@ -117,7 +141,7 @@ class UserAssetActivity : BaseActivity() {
requestStoragePermissionLauncher.launch(permission) requestStoragePermissionLauncher.launch(permission)
} }
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()
@@ -135,7 +159,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)
} }
} }
@@ -146,8 +170,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
@@ -161,7 +185,7 @@ 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
} }
@@ -188,7 +212,7 @@ class UserAssetActivity : BaseActivity() {
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url) .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
@@ -205,17 +229,21 @@ class UserAssetActivity : BaseActivity() {
var resultCount = 0 var resultCount = 0
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
assets.forEach { assets.forEach {
var result = downloadGeo(it.second, 15000, httpPort) try {
if (!result) { var result = downloadGeo(it.second, 15000, httpPort)
result = downloadGeo(it.second, 15000, 0) if (!result) {
result = downloadGeo(it.second, 15000, 0)
}
if (result)
resultCount++
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to download geo file: ${it.second.remarks}", e)
} }
if (result)
resultCount++
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (resultCount > 0) { if (resultCount > 0) {
toast(getString(R.string.title_update_config_count, resultCount)) toast(getString(R.string.title_update_config_count, resultCount))
binding.recyclerView.adapter?.notifyDataSetChanged() refreshData()
} else { } else {
toast(getString(R.string.toast_failure)) toast(getString(R.string.toast_failure))
} }
@@ -227,7 +255,7 @@ class UserAssetActivity : BaseActivity() {
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)
//Log.d(AppConfig.ANG_PACKAGE, url) Log.i(AppConfig.TAG, "Downloading geo file: ${item.remarks} from ${item.url}")
val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false val conn = HttpUtil.createProxyConnection(item.url, httpPort, timeout, timeout, needStream = true) ?: return false
try { try {
@@ -242,10 +270,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()
} }
} }
@@ -257,7 +285,8 @@ class UserAssetActivity : BaseActivity() {
list.add( list.add(
Utils.getUuid() to AssetUrlItem( Utils.getUuid() to AssetUrlItem(
it, it,
AppConfig.GeoUrl + it String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
locked = true
) )
) )
} }
@@ -269,11 +298,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(
@@ -303,7 +337,7 @@ class UserAssetActivity : BaseActivity() {
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found) holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
} }
if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) { if (item.second.locked == true) {
holder.itemUserAssetBinding.layoutEdit.visibility = GONE holder.itemUserAssetBinding.layoutEdit.visibility = GONE
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE //holder.itemUserAssetBinding.layoutRemove.visibility = GONE
} else { } else {

View File

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

View File

@@ -25,7 +25,7 @@ object AppManagerUtil {
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)
@@ -33,4 +33,8 @@ object AppManagerUtil {
return@withContext apps return@withContext apps
} }
}
fun getLastUpdateTime(context: Context): Long =
context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
}

View File

@@ -1,12 +1,19 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode import com.v2ray.ang.util.Utils.encode
import com.v2ray.ang.util.Utils.urlDecode import com.v2ray.ang.util.Utils.urlDecode
import java.io.IOException import java.io.IOException
import java.net.* import java.net.HttpURLConnection
import java.util.* import java.net.IDN
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URL
object HttpUtil { object HttpUtil {
@@ -17,7 +24,7 @@ object HttpUtil {
* @return The ASCII representation of the URL. * @return The ASCII representation of the URL.
*/ */
fun idnToASCII(str: String): String { fun idnToASCII(str: String): String {
val url = URI(str) val url = URL(str)
val host = url.host val host = url.host
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED) val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
if (host != asciiHost) { if (host != asciiHost) {
@@ -27,6 +34,45 @@ object HttpUtil {
} }
} }
/**
* Resolves a hostname to an IP address, returns original input if it's already an IP
*
* @param host The hostname or IP address to resolve
* @param ipv6Preferred Whether to prefer IPv6 addresses, defaults to false
* @return The resolved IP address or the original input (if it's already an IP or resolution fails)
*/
fun resolveHostToIP(host: String, ipv6Preferred: Boolean = false): List<String>? {
try {
// If it's already an IP address, return it as a list
if (Utils.isPureIpAddress(host)) {
return null
}
// Get all IP addresses
val addresses = InetAddress.getAllByName(host)
if (addresses.isEmpty()) {
return null
}
// Sort addresses based on preference
val sortedAddresses = if (ipv6Preferred) {
addresses.sortedWith(compareByDescending { it is Inet6Address })
} else {
addresses.sortedWith(compareBy { it is Inet6Address })
}
val ipList = sortedAddresses.mapNotNull { it.hostAddress }
Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}")
return ipList
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to resolve host to IP", e)
return null
}
}
/** /**
* Retrieves the content of a URL as a string. * Retrieves the content of a URL as a string.
* *
@@ -142,7 +188,7 @@ object HttpUtil {
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to create proxy connection", e)
// If an exception occurs, close the connection and return null // If an exception occurs, close the connection and return null
conn?.disconnect() conn?.disconnect()
return null return null

View File

@@ -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,6 +9,7 @@ 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 {
@@ -70,7 +72,7 @@ object JsonUtil {
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
} }
} }

View File

@@ -3,12 +3,14 @@ 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. * Sends a message to the service.
* *
@@ -46,7 +48,7 @@ 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)
} }
} }
@@ -67,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)
} }
} }
} }

View File

@@ -3,7 +3,7 @@ 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
@@ -13,7 +13,7 @@ import java.io.File
object PluginUtil { object PluginUtil {
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()
} }
@@ -23,16 +23,26 @@ object PluginUtil {
* *
* @param context The context to use. * @param context The context to use.
* @param config The profile configuration. * @param config The profile configuration.
* @param domainPort The domain and port information. * @param socksPort The port information.
*/ */
fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) { fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
Log.d(TAG, "runPlugin") Log.i(AppConfig.TAG, "Starting plugin execution")
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) { if (config == null || socksPort == null) {
val configFile = genConfigHy2(context, config, domainPort) ?: return Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
val cmd = genCmdHy2(context, configFile) return
}
procService.runProcess(context, cmd) try {
if (config.configType == EConfigType.HYSTERIA2) {
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
val configFile = genConfigHy2(context, config, socksPort) ?: return
val cmd = genCmdHy2(context, configFile)
procService.runProcess(context, cmd)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error running plugin", e)
} }
} }
@@ -51,12 +61,12 @@ object PluginUtil {
* @return The ping delay in milliseconds, or -1 if it fails. * @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) {
val socksPort = Utils.findFreePort(listOf(0)) val socksPort = Utils.findFreePort(listOf(0))
val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure
val cmd = genCmdHy2(context, configFile) val cmd = genCmdHy2(context, configFile)
val proc = ProcessService() val proc = ProcessService()
@@ -75,22 +85,20 @@ object PluginUtil {
* *
* @param context The context to use. * @param context The context to use.
* @param config The profile configuration. * @param config The profile configuration.
* @param domainPort The domain and port information. * @param socksPort The port information.
* @return The generated configuration file. * @return The generated configuration file.
*/ */
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? { private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? {
Log.d(TAG, "runPlugin $HYSTERIA2") Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
val socksPort = domainPort?.split(":")?.last()
.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
} }
@@ -119,10 +127,10 @@ object PluginUtil {
*/ */
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)
} }
} }
} }

View File

@@ -17,10 +17,12 @@ import android.webkit.URLUtil
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri 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.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import java.io.IOException import java.io.IOException
import java.net.InetAddress
import java.net.ServerSocket import java.net.ServerSocket
import java.net.URI
import java.net.URLDecoder import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Locale import java.util.Locale
@@ -28,6 +30,10 @@ 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 editable for Kotlin. * Convert string to editable for Kotlin.
* *
@@ -46,22 +52,7 @@ object Utils {
* @return The index of the value in the array, or -1 if not found. * @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
}
/**
* Parse a string to an integer.
*
* @param str The string to parse.
* @return The parsed integer, or 0 if parsing fails.
*/
fun parseInt(str: String): Int {
return parseInt(str, 0)
} }
/** /**
@@ -71,7 +62,7 @@ object Utils {
* @param default The default value if parsing fails. * @param default The default value if parsing fails.
* @return The parsed integer, or the default value if parsing fails. * @return The parsed integer, or the default value if parsing fails.
*/ */
fun parseInt(str: String?, default: Int): Int { fun parseInt(str: String?, default: Int = 0): Int {
return str?.toIntOrNull() ?: default return str?.toIntOrNull() ?: default
} }
@@ -86,7 +77,7 @@ object Utils {
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)
"" ""
} }
} }
@@ -103,7 +94,7 @@ 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)
} }
} }
@@ -124,15 +115,17 @@ object Utils {
* @return The decoded string, or null if decoding fails. * @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
} }
@@ -147,7 +140,7 @@ object Utils {
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)
"" ""
} }
} }
@@ -159,43 +152,38 @@ object Utils {
* @return True if the string is a valid IP address, false otherwise. * @return True if the string is a valid IP address, false otherwise.
*/ */
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
} }
} }
@@ -217,9 +205,7 @@ object Utils {
* @return True if the string is a valid IPv4 address, false otherwise. * @return True if the string is a valid IPv4 address, false otherwise.
*/ */
private fun isIpv4Address(value: String): Boolean { private fun isIpv4Address(value: String): Boolean {
val regV4 = return IPV4_REGEX.matches(value)
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)
} }
/** /**
@@ -230,13 +216,10 @@ object Utils {
*/ */
private fun isIpv6Address(value: String): Boolean { private fun isIpv6Address(value: String): Boolean {
var addr = value var addr = value
if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) { if (addr.startsWith("[") && addr.endsWith("]")) {
addr = addr.drop(1) addr = addr.drop(1).dropLast(1)
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
} }
val regV6 = return IPV6_REGEX.matches(addr)
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)
} }
/** /**
@@ -246,10 +229,10 @@ object Utils {
* @return True if the string is a CoreDNS address, false otherwise. * @return True if the string is a CoreDNS address, false otherwise.
*/ */
fun isCoreDNSAddress(s: String): Boolean { fun isCoreDNSAddress(s: String): Boolean {
return s.startsWith("https") return s.startsWith("https") ||
|| s.startsWith("tcp") s.startsWith("tcp") ||
|| s.startsWith("quic") s.startsWith("quic") ||
|| s == "localhost" s == "localhost"
} }
/** /**
@@ -259,21 +242,16 @@ object Utils {
* @return True if the string is a valid URL, false otherwise. * @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
} }
/** /**
@@ -283,8 +261,12 @@ object Utils {
* @param uriString The URI string to open. * @param uriString The URI string to open.
*/ */
fun openUri(context: Context, uriString: String) { fun openUri(context: Context, uriString: String) {
val uri = uriString.toUri() 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)
}
} }
/** /**
@@ -296,7 +278,7 @@ object Utils {
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)
"" ""
} }
} }
@@ -311,7 +293,7 @@ object Utils {
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
} }
} }
@@ -326,7 +308,7 @@ object Utils {
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
} }
} }
@@ -339,13 +321,18 @@ object Utils {
* @return The content of the asset file as a string. * @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 ""
return try {
context.assets.open(fileName).use { inputStream ->
inputStream.bufferedReader().use { reader ->
reader.readText()
}
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
""
} }
val content = context.assets.open(fileName).bufferedReader().use {
it.readText()
}
return content
} }
/** /**
@@ -355,11 +342,15 @@ object Utils {
* @return The path to the user asset directory. * @return The path to the user asset directory.
*/ */
fun userAssetPath(context: Context?): String { fun userAssetPath(context: Context?): String {
if (context == null) if (context == null) return ""
return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS) return try {
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
return extDir.absolutePath ?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get user asset path", e)
""
}
} }
/** /**
@@ -369,11 +360,15 @@ object Utils {
* @return The path to the backup directory. * @return The path to the backup directory.
*/ */
fun backupPath(context: Context?): String { fun backupPath(context: Context?): String {
if (context == null) if (context == null) return ""
return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS) return try {
?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
return extDir.absolutePath ?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get backup path", e)
""
}
} }
/** /**
@@ -382,8 +377,13 @@ object Utils {
* @return The device ID for XUDP base key. * @return The device ID for XUDP base key.
*/ */
fun getDeviceIdForXUDPBaseKey(): String { fun getDeviceIdForXUDPBaseKey(): String {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8) return try {
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE)) 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)
""
}
} }
/** /**
@@ -403,11 +403,10 @@ object Utils {
* @return The formatted IPv6 address, or the original address if not valid. * @return The formatted IPv6 address, or the original address if not valid.
*/ */
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
} }
@@ -431,8 +430,7 @@ object Utils {
* @return The URL string with illegal characters replaced. * @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")
} }
@@ -463,12 +461,23 @@ object Utils {
* @return True if the string is a valid subscription URL, false otherwise. * @return True if the string is a valid subscription URL, false otherwise.
*/ */
fun isValidSubUrl(value: String?): Boolean { fun isValidSubUrl(value: String?): Boolean {
if (value.isNullOrEmpty()) return false
try { try {
if (value.isNullOrEmpty()) return false
if (URLUtil.isHttpsUrl(value)) return true if (URLUtil.isHttpsUrl(value)) return true
if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true if (URLUtil.isHttpUrl(value)) {
if (value.contains(LOOPBACK)) return true
//Check private ip address
val uri = URI(fixIllegalUrl(value))
if (isIpAddress(uri.host)) {
AppConfig.PRIVATE_IP_LIST.forEach {
if (isIpInCidr(uri.host, it)) return true
}
}
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
} }
return false return false
} }
@@ -489,7 +498,58 @@ object Utils {
* *
* @return True if the package is Xray, false otherwise. * @return True if the package is Xray, false otherwise.
*/ */
fun isXray(): Boolean = (ANG_PACKAGE.startsWith("com.v2ray.ang")) fun isXray(): Boolean = BuildConfig.APPLICATION_ID.startsWith("com.v2ray.ang")
/**
* Check if it is the Google Play version.
*
* @return True if the package is Google Play, false otherwise.
*/
fun isGoogleFlavor(): Boolean = BuildConfig.FLAVOR == "playstore"
/**
* Converts an InetAddress to its long representation
*
* @param ip The InetAddress to convert
* @return The long representation of the IP address
*/
private fun inetAddressToLong(ip: InetAddress): Long {
val bytes = ip.address
var result: Long = 0
for (i in bytes.indices) {
result = result shl 8 or (bytes[i].toInt() and 0xff).toLong()
}
return result
}
/**
* Check if an IP address is within a CIDR range
*
* @param ip The IP address to check
* @param cidr The CIDR notation range (e.g., "192.168.1.0/24")
* @return True if the IP is within the CIDR range, false otherwise
*/
fun isIpInCidr(ip: String, cidr: String): Boolean {
try {
if (!isIpAddress(ip)) return false
// Parse CIDR (e.g., "192.168.1.0/24")
val (cidrIp, prefixLen) = cidr.split("/")
val prefixLength = prefixLen.toInt()
// Convert IP and CIDR's IP portion to Long
val ipLong = inetAddressToLong(InetAddress.getByName(ip))
val cidrIpLong = inetAddressToLong(InetAddress.getByName(cidrIp))
// Calculate subnet mask (e.g., /24 → 0xFFFFFF00)
val mask = if (prefixLength == 0) 0L else (-1L shl (32 - prefixLength))
// Check if they're in the same subnet
return (ipLong and mask) == (cidrIpLong and mask)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e)
return false
}
}
} }

View File

@@ -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
@@ -61,7 +63,7 @@ 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
@@ -97,7 +99,7 @@ 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

View File

@@ -13,12 +13,12 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.v2ray.ang.AngApplication import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServersCache import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.serializable
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.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
@@ -62,7 +62,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver) getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
tcpingTestScope.coroutineContext[Job]?.cancelChildren() tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestManager.closeAllTcpSockets() SpeedtestManager.closeAllTcpSockets()
Log.i(ANG_PACKAGE, "Main ViewModel is cleared") Log.i(AppConfig.TAG, "Main ViewModel is cleared")
super.onCleared() super.onCleared()
} }
@@ -419,12 +419,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
AppConfig.MSG_STATE_START_SUCCESS -> { AppConfig.MSG_STATE_START_SUCCESS -> {
getApplication<AngApplication>().toast(R.string.toast_services_success) getApplication<AngApplication>().toastSuccess(R.string.toast_services_success)
isRunning.value = true isRunning.value = true
} }
AppConfig.MSG_STATE_START_FAILURE -> { AppConfig.MSG_STATE_START_FAILURE -> {
getApplication<AngApplication>().toast(R.string.toast_services_failure) getApplication<AngApplication>().toastError(R.string.toast_services_failure)
isRunning.value = false isRunning.value = false
} }

View File

@@ -26,7 +26,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
override fun onCleared() { override fun onCleared() {
PreferenceManager.getDefaultSharedPreferences(getApplication()) PreferenceManager.getDefaultSharedPreferences(getApplication())
.unregisterOnSharedPreferenceChangeListener(this) .unregisterOnSharedPreferenceChangeListener(this)
Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared") Log.i(AppConfig.TAG, "Settings ViewModel is cleared")
super.onCleared() super.onCleared()
} }
@@ -36,7 +36,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
* @param key The key of the changed preference. * @param key The key of the changed preference.
*/ */
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
Log.d(AppConfig.ANG_PACKAGE, "Observe settings changed: $key") Log.i(AppConfig.TAG, "Observe settings changed: $key")
when (key) { when (key) {
AppConfig.PREF_MODE, AppConfig.PREF_MODE,
AppConfig.PREF_VPN_DNS, AppConfig.PREF_VPN_DNS,

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z" />
</vector>

View File

@@ -4,6 +4,6 @@
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" android:fillColor="#FFFFFFFF"
android:fillColor="#FFFFFFFF" /> android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" />
</vector> </vector>

View File

@@ -4,12 +4,12 @@
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" android:fillColor="#FFFFFFFF"
android:fillColor="#FFFFFFFF" /> android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" />
<path <path
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" android:fillColor="#FFFFFFFF"
android:fillColor="#FFFFFFFF" /> android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" />
<path <path
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" android:fillColor="#FFFFFFFF"
android:fillColor="#FFFFFFFF" /> android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" />
</vector> </vector>

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" /> android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />

View File

@@ -4,9 +4,9 @@
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z" android:fillColor="#FFFFFFFF"
android:fillColor="#FFFFFFFF" /> android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z" />
<path <path
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z" android:fillColor="#FFFFFFFF"
android:fillColor="#FFFFFFFF" /> android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z" />
</vector> </vector>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:width="24dp"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFF" android:fillColor="#FFFFFF"
android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z" /> android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z" />

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z" />
</vector>

View File

@@ -3,5 +3,5 @@
<solid android:color="@color/divider_color_light" /> <solid android:color="@color/divider_color_light" />
<size <size
android:width="48dp" android:width="48dp"
android:height="48dp"/> android:height="48dp" />
</shape> </shape>

View File

@@ -4,6 +4,6 @@
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" android:fillColor="#FF000000"
android:fillColor="#FF000000" /> android:pathData="M537,85.3A85.3,85.3 0,0 1,597.3 110.3L828.3,341.3A85.3,85.3 0,0 1,853.3 401.7L853.3,810.7a128,128 0,0 1,-128 128L298.7,938.7a128,128 0,0 1,-128 -128L170.7,213.3a128,128 0,0 1,128 -128zM537,170.7L298.7,170.7a42.7,42.7 0,0 0,-42.7 42.7v597.3a42.7,42.7 0,0 0,42.7 42.7h426.7a42.7,42.7 0,0 0,42.7 -42.7L768,401.7L537,170.7zM512,384a42.7,42.7 0,0 1,42.4 37.7L554.7,426.7v85.3h85.3a42.7,42.7 0,0 1,5 85L640,597.3h-85.3v85.3a42.7,42.7 0,0 1,-85 5L469.3,682.7v-85.3L384,597.3a42.7,42.7 0,0 1,-5 -85L384,512h85.3v-85.3a42.7,42.7 0,0 1,42.7 -42.7z" />
</vector> </vector>

View File

@@ -4,12 +4,12 @@
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" android:fillColor="#FF000000"
android:fillColor="#FF000000" /> android:pathData="M273.2,212.8h583.7L856.9,906.2h-583.7L273.2,212.8zM344.9,284.5L344.9,834.6h440.3L785.2,284.5h-440.3z" />
<path <path
android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" android:fillColor="#FF000000"
android:fillColor="#FF000000" /> android:pathData="M167.1,117.8h554.4v123.6h-71.7V189.4H238.8v498.8h73.9v71.7H167.1V117.8z" />
<path <path
android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" android:fillColor="#FF000000"
android:fillColor="#FF000000" /> android:pathData="M674.8,504H455.3v-71.7h219.4v71.7zM674.8,650.2H455.3v-71.7h219.4v71.7z" />
</vector> </vector>

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" /> android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />

View File

@@ -4,9 +4,9 @@
android:viewportWidth="1024" android:viewportWidth="1024"
android:viewportHeight="1024"> android:viewportHeight="1024">
<path <path
android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z" android:fillColor="#FF000000"
android:fillColor="#FF000000" /> android:pathData="M723.9,312c-13.2,-13.2 -34.7,-13.2 -47.9,0 -6.6,6.6 -9.9,15.3 -9.9,23.9 0,8.7 3.3,17.3 9.9,23.9 65.9,65.9 80.9,165.5 37.3,247.8 -4.9,9.2 -10.5,18.2 -16.9,26.8 -6.4,8.7 -12.9,16.4 -20,23.3 -13.4,13.1 -13.7,34.5 -0.6,47.9 13.1,13.4 34.5,13.7 47.9,0.6 9.7,-9.5 18.9,-20.2 27.3,-31.6 8.3,-11.2 15.7,-23 22.2,-35.2 57.5,-108.7 37.7,-240.3 -49.3,-327.4zM507.6,470c-0.2,-0.2 -0.4,-0.3 -0.7,-0.5 -0.8,-0.3 -1.7,-0.1 -2.3,0.5 -0.2,0.2 -0.3,0.4 -0.5,0.7 -0.1,0.3 -0.2,0.5 -0.2,0.8 0,0.6 0.3,1.1 0.6,1.5 0.2,0.2 0.4,0.3 0.7,0.5 0.3,0.1 0.5,0.2 0.8,0.2 0.6,0 1.1,-0.3 1.5,-0.6 0.4,-0.4 0.6,-1 0.6,-1.5 0,-0.3 -0.1,-0.5 -0.2,-0.8 0,-0.4 -0.2,-0.6 -0.3,-0.8z" />
<path <path
android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z" android:fillColor="#FF000000"
android:fillColor="#FF000000" /> android:pathData="M833.7,209.6c-13.2,-13.2 -34.5,-13.2 -47.6,0 -13.2,13.2 -13.2,34.5 0,47.6 122.3,122.3 140,314.4 42.1,456.6 -12.5,18.1 -26.5,35 -42.1,50.5 -6.6,6.6 -9.9,15.2 -9.9,23.8 0,8.6 3.3,17.2 9.9,23.8 13.2,13.2 34.5,13.2 47.6,0 18.4,-18.4 35.1,-38.5 49.9,-60C1000,583.1 979,355 833.7,209.6zM515.1,166.8c-48,-22.3 -102.9,-15.1 -143.4,19L176.6,349.9h-50.2c-31,0 -63.3,25.2 -63.3,56.3v209.4c0,31 32.2,56.3 63.3,56.3h50.2L371.7,836c24.9,21 55.4,31.8 86.2,31.8 19.3,0 38.7,-4.2 57.2,-12.8 48,-22.3 77.8,-69.1 77.8,-122L592.9,288.8c0,-52.9 -29.8,-99.6 -77.8,-122zM531.1,732.9c0,31.8 -17.2,52.9 -46.1,66.3 -28.9,13.4 -56.7,6.2 -81,-14.3L206.2,623.5c-4.9,-4.2 -11.2,-6.4 -17.6,-6.4h-60.2c-0.8,0 -1.5,-0.7 -1.5,-1.5L126.9,406.2c0,-0.8 0.7,-1.5 1.5,-1.5h60.2c6.5,0 12.7,-2.3 17.6,-6.4L412,237.7c24.4,-20.5 51.2,-24.7 80,-11.3 28.9,13.4 39.1,30.5 39.1,62.3v444.2z" />
</vector> </vector>

View File

@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportHeight="1024" android:viewportWidth="1024"
android:viewportWidth="1024"> android:viewportHeight="1024">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M0,512C0,229.23 229.81,0 512,0 794.77,0 1024,229.81 1024,512 1024,794.77 794.19,1024 512,1024 229.23,1024 0,794.19 0,512Z" /> android:pathData="M0,512C0,229.23 229.81,0 512,0 794.77,0 1024,229.81 1024,512 1024,794.77 794.19,1024 512,1024 229.23,1024 0,794.19 0,512Z" />

View File

@@ -1,8 +1,8 @@
<vector android:height="24dp" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="1024"
android:viewportWidth="1024"
android:width="24dp" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M469.2,802.1l-81.7,-24.6L554.8,221.9l81.7,24.6L469.2,802.1zM362.7,654.5l-124.7,-141.7 124.8,-143.5 -64.4,-56 -173.8,199.8 174,197.7 64.1,-56.3zM899.4,513.1l-173.8,-199.8 -64.4,56 124.8,143.5 -124.7,141.7 64.1,56.4 174,-197.7z" /> android:pathData="M469.2,802.1l-81.7,-24.6L554.8,221.9l81.7,24.6L469.2,802.1zM362.7,654.5l-124.7,-141.7 124.8,-143.5 -64.4,-56 -173.8,199.8 174,197.7 64.1,-56.3zM899.4,513.1l-173.8,-199.8 -64.4,56 124.8,143.5 -124.7,141.7 64.1,56.4 174,-197.7z" />

View File

@@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960" android:viewportHeight="960">
android:tint="?attr/colorControlNormal"> <path
<path android:fillColor="@android:color/white"
android:fillColor="@android:color/white" android:pathData="M480,520Q430,520 395,485Q360,450 360,400Q360,350 395,315Q430,280 480,280Q530,280 565,315Q600,350 600,400Q600,450 565,485Q530,520 480,520ZM240,920L240,611Q202,569 181,515Q160,461 160,400Q160,266 253,173Q346,80 480,80Q614,80 707,173Q800,266 800,400Q800,461 779,515Q758,569 720,611L720,920L480,840L240,920ZM480,640Q580,640 650,570Q720,500 720,400Q720,300 650,230Q580,160 480,160Q380,160 310,230Q240,300 240,400Q240,500 310,570Q380,640 480,640ZM320,801L480,760L640,801L640,677Q605,697 564.5,708.5Q524,720 480,720Q436,720 395.5,708.5Q355,697 320,677L320,801ZM480,739L480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739L480,739L480,739Z" />
android:pathData="M480,520Q430,520 395,485Q360,450 360,400Q360,350 395,315Q430,280 480,280Q530,280 565,315Q600,350 600,400Q600,450 565,485Q530,520 480,520ZM240,920L240,611Q202,569 181,515Q160,461 160,400Q160,266 253,173Q346,80 480,80Q614,80 707,173Q800,266 800,400Q800,461 779,515Q758,569 720,611L720,920L480,840L240,920ZM480,640Q580,640 650,570Q720,500 720,400Q720,300 650,230Q580,160 480,160Q380,160 310,230Q240,300 240,400Q240,500 310,570Q380,640 480,640ZM320,801L480,760L640,801L640,677Q605,697 564.5,708.5Q524,720 480,720Q436,720 395.5,708.5Q355,697 320,677L320,801ZM480,739L480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739L480,739L480,739Z"/>
</vector> </vector>

View File

@@ -111,6 +111,49 @@
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="@dimen/padding_spacing_dp16"> android:paddingTop="@dimen/padding_spacing_dp16">
<LinearLayout
android:id="@+id/layout_check_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
app:srcCompat="@drawable/ic_check_update_24dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/padding_spacing_dp16">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/update_check_for_update"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/check_pre_release"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:maxLines="1"
android:text="@string/update_check_pre_release"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/colorAccent"
app:theme="@style/BrandedSwitch" />
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/layout_soure_ccode" android:id="@+id/layout_soure_ccode"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -5,7 +5,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:orientation="vertical"> android:orientation="vertical"
tools:context=".ui.PerAppProxyActivity">
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/pb_waiting" android:id="@+id/pb_waiting"
@@ -71,6 +72,23 @@
android:textColor="@color/colorAccent" android:textColor="@color/colorAccent"
app:theme="@style/BrandedSwitch" /> app:theme="@style/BrandedSwitch" />
<LinearLayout
android:id="@+id/layout_switch_bypass_apps_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:padding="@dimen/padding_spacing_dp8">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
app:srcCompat="@drawable/ic_about_24dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -83,7 +101,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" />
tools:context=".ui.PerAppProxyActivity" />
</LinearLayout> </LinearLayout>

View File

@@ -14,9 +14,9 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

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