Compare commits

..

108 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
2dust
397989769c up 1.9.42 2025-03-26 18:54:42 +08:00
2dust
796676abdc Enable double column display
The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect.
2025-03-26 18:48:14 +08:00
2dust
3c93ccb86c Optimize and improve 2025-03-25 20:26:28 +08:00
2dust
fc8c74184a Optimize and improve for toast 2025-03-25 17:44:30 +08:00
solokot
1939e6b5cf Optimize Russian translation (#4411) 2025-03-25 17:05:12 +08:00
2dust
1c2ac9385d Fix
https://github.com/2dust/v2rayNG/issues/4410
2025-03-25 10:49:07 +08:00
2dust
7513f1fe07 Optimize and improve 2025-03-25 10:44:06 +08:00
2dust
63e2c02daa Fix
https://github.com/2dust/v2rayNG/issues/4408
2025-03-24 20:17:49 +08:00
2dust
fec76385e1 Bug fix
https://github.com/2dust/v2rayNG/issues/4409
2025-03-24 20:17:05 +08:00
142 changed files with 2971 additions and 1819 deletions

View File

@@ -13,4 +13,4 @@ jobs:
steps:
- uses: actions/checkout@v4
- 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
*.jks
# Ignore output JSON file
V2rayNG/app/release/output.json
# Ignore IDE and build system directories
.idea/
.gradle/
*.iml
# Ignore local properties and DS_Store files
/local.properties
.DS_Store
# Ignore build directories and captures
/build
/captures
V2rayNG/app/build
V2rayNG/build
V2rayNG/local.properties
# Ignore APK and AAR files
*.apk
*.aar
# Ignore signing properties
signing.properties
# Ignore shared object files
*.so
# Ignore Google services JSON
V2rayNG/app/google-services.json
# Additional common Android/Java ignores
*.log
*.tmp
*.bak
*.swp
*.orig
*.class
*.jar
*.war
*.ear
# Ignore executable files
*.exe
*.dll
*.obj
*.o
*.pyc
*.pyo
# Ignore files from other IDEs
.vscode/
.classpath
.project
.settings/
*.sublime-workspace
*.sublime-project
# Ignore OS-specific files
Thumbs.db
.DS_Store

View File

@@ -21,7 +21,7 @@ A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-cor
#### 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)
- 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)
### 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"
minSdk = 21
targetSdk = 35
versionCode = 641
versionName = "1.9.41"
versionCode = 652
versionName = "1.10.2"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -82,8 +82,9 @@ android {
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
if (isFdroid) {
val versionCodes =
mapOf("armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
)
mapOf(
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
)
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
@@ -148,7 +149,7 @@ dependencies {
// UI Libraries
implementation(libs.material)
implementation(libs.toastcompat)
implementation(libs.toasty)
implementation(libs.editorkit)
implementation(libs.flexbox)

View File

@@ -35,7 +35,6 @@
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<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.FOREGROUND_SERVICE" />
<uses-permission
@@ -52,10 +51,10 @@
android:banner="@mipmap/ic_banner"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/AppThemeDayNight"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="m">
<activity
@@ -213,7 +212,8 @@
android:icon="@drawable/ic_stat_name"
android:label="@string/app_tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:process=":RunSoLibV2RayDaemon">
android:process=":RunSoLibV2RayDaemon"
tools:targetApi="24">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>

View File

@@ -20,13 +20,6 @@
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",

View File

@@ -5,13 +5,6 @@
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",

View File

@@ -13,13 +13,6 @@
"port": "443",
"network": "udp"
},
{
"remarks": "阻断广告",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "绕过局域网IP",
"outboundTag": "direct",

View File

@@ -5,13 +5,6 @@
"port": "443",
"network": "udp"
},
{
"remarks": "Block ads and trackers",
"outboundTag": "block",
"domain": [
"geosite:category-ads-all"
]
},
{
"remarks": "Direct LAN IP",
"outboundTag": "direct",

View File

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

View File

@@ -39,5 +39,9 @@ class AngApplication : MultiDexApplication() {
WorkManager.initialize(this, workManagerConfiguration)
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. */
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
const val TAG = BuildConfig.APPLICATION_ID
/** Directory names used in the app's file system. */
const val DIR_ASSETS = "assets"
@@ -42,6 +43,7 @@ object AppConfig {
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
const val PREF_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display"
const val PREF_LANGUAGE = "pref_language"
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
@@ -55,13 +57,15 @@ object AppConfig {
const val PREF_LOGLEVEL = "pref_core_loglevel"
const val PREF_MODE = "pref_mode"
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. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */
const val PROTOCOL_FREEDOM: String = "freedom"
const val PROTOCOL_FREEDOM = "freedom"
/** Broadcast actions. */
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
@@ -86,19 +90,20 @@ object AppConfig {
const val DOWNLINK = "downlink"
/** URLs for various resources. */
const val androidpackagenamelistUrl =
"https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
const val v2rayCustomRoutingListUrl =
"https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
const val v2rayNGIssues = "$v2rayNGUrl/issues"
const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
const val TgChannelUrl = "https://t.me/github_2dust"
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
const val DelayTestUrl2 = "https://www.google.com/generate_204"
const val GITHUB_URL = "https://github.com"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
const val APP_ISSUES_URL = "$APP_URL/issues"
const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
const val DELAY_TEST_URL = "https://www.gstatic.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. */
const val DNS_PROXY = "1.1.1.1"
@@ -168,14 +173,6 @@ object AppConfig {
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8
@@ -184,4 +181,62 @@ object AppConfig {
const val REALITY = "reality"
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 url: String = "",
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 guid: String? = null,
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),
TROJAN(6, AppConfig.TROJAN),
WIREGUARD(7, AppConfig.WIREGUARD),
// TUIC(8, AppConfig.TUIC),
// TUIC(8, AppConfig.TUIC),
HYSTERIA2(9, AppConfig.HYSTERIA2),
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"),
RUSSIAN("ru"),
PERSIAN("fa"),
ARABIC("ar"),
BANGLA("bn"),
BAKHTIARI("bqi-rIR");

View File

@@ -8,6 +8,7 @@ enum class NetworkType(val type: String) {
XHTTP("xhttp"),
HTTP("http"),
H2("h2"),
//QUIC("quic"),
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 nextProfile: String? = null,
var filter: String? = null,
var allowInsecureUrl: Boolean = false,
)

View File

@@ -1,28 +1,17 @@
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.reflect.TypeToken
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 java.lang.reflect.Type
data class V2rayConfig(
var remarks: String? = null,
var stats: Any? = null,
val log: LogBean,
var policy: PolicyBean?,
var policy: PolicyBean? = null,
val inbounds: ArrayList<InboundBean>,
var outbounds: ArrayList<OutboundBean>,
var dns: DnsBean,
var dns: DnsBean? = null,
val routing: RoutingBean,
val api: Any? = null,
val transport: Any? = null,
@@ -34,9 +23,9 @@ data class V2rayConfig(
) {
data class LogBean(
val access: String,
val error: String,
var loglevel: String?,
val access: String? = null,
val error: String? = null,
var loglevel: String? = null,
val dnsLog: Boolean? = null
)
@@ -46,7 +35,7 @@ data class V2rayConfig(
var protocol: String,
var listen: String? = null,
val settings: Any? = null,
val sniffing: SniffingBean?,
val sniffing: SniffingBean? = null,
val streamSettings: Any? = null,
val allocate: Any? = null
) {
@@ -77,50 +66,6 @@ data class V2rayConfig(
val sendThrough: String? = null,
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(
var vnext: List<VnextBean>? = null,
var fragment: FragmentBean? = null,
@@ -197,7 +142,7 @@ data class V2rayConfig(
data class WireGuardBean(
var publicKey: String = "",
var preSharedKey: String = "",
var preSharedKey: String? = null,
var endpoint: String = ""
)
}
@@ -261,7 +206,8 @@ data class V2rayConfig(
) {
data class HeaderBean(
var type: String = "none",
var domain: String? = null)
var domain: String? = null
)
}
data class WsSettingsBean(
@@ -298,7 +244,8 @@ data class V2rayConfig(
var tcpFastOpen: Boolean? = null,
var tproxy: String? = null,
var mark: Int? = null,
var dialerProxy: String? = null
var dialerProxy: String? = null,
var domainStrategy: String? = null
)
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(
@@ -646,6 +460,18 @@ data class V2rayConfig(
}
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(
@@ -722,15 +548,9 @@ data class V2rayConfig(
return null
}
fun toPrettyPrinting(): String {
return GsonBuilder()
.setPrettyPrinting()
.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)
fun getAllProxyOutbound(): List<OutboundBean> {
return outbounds.filter { outbound ->
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
}
}
}
}

View File

@@ -8,7 +8,7 @@ import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.v2ray.ang.AngApplication
import me.drakeet.support.toast.ToastCompat
import es.dmoral.toasty.Toasty
import org.json.JSONObject
import java.io.Serializable
import java.net.URI
@@ -23,7 +23,7 @@ val Context.v2RayApplication: AngApplication?
* @param message The resource ID of the message to show.
*/
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.
*/
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.
*
@@ -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.
*/
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 {
val query = if (dicQuery != null)
("?" + dicQuery.toList().joinToString(
"?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + Utils.urlEncode(it.second) }))
transform = { it.first + "=" + Utils.urlEncode(it.second) })
else ""
val url = String.format(
@@ -120,7 +120,7 @@ open class FmtBase {
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.path.let { if (it.isNotNullEmpty()) dicQuery["path"] = it.orEmpty() }
config.xhttpMode.let { if (it.isNotNullEmpty()) dicQuery["mode"] = it.orEmpty() }
@@ -148,4 +148,7 @@ open class FmtBase {
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.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.V2rayConfigManager
object HttpFmt : FmtBase() {
/**
@@ -13,7 +14,7 @@ object HttpFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HTTP)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
outboundBean?.settings?.servers?.first()?.let { server ->
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.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -24,7 +25,7 @@ object Hysteria2Fmt : FmtBase() {
val config = ProfileItem.create(EConfigType.HYSTERIA2)
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.serverPort = uri.port.toString()
config.password = uri.userInfo
@@ -144,7 +145,7 @@ object Hysteria2Fmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
return outboundBean
}
}

View File

@@ -1,10 +1,13 @@
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.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -33,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
if (uri.port <= 0) 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.serverPort = uri.port.toString()
@@ -82,7 +85,7 @@ object ShadowsocksFmt : FmtBase() {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
}
result = result.substring(0, indexSplit)
@@ -129,7 +132,7 @@ object ShadowsocksFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
@@ -138,29 +141,13 @@ object ShadowsocksFmt : FmtBase() {
server.method = profileItem.method
}
val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
val sni = outboundBean?.streamSettings?.let {
V2rayConfigManager.populateTransportSettings(it, profileItem)
}
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
outboundBean?.streamSettings?.let {
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
}
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.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -22,7 +23,7 @@ object SocksFmt : FmtBase() {
if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
@@ -60,7 +61,7 @@ object SocksFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.SOCKS)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
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.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -22,7 +23,7 @@ object TrojanFmt : FmtBase() {
val config = ProfileItem.create(EConfigType.TROJAN)
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.serverPort = uri.port.toString()
config.password = uri.userInfo
@@ -60,7 +61,7 @@ object TrojanFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.TROJAN)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
outboundBean?.settings?.servers?.first()?.let { server ->
server.address = profileItem.server.orEmpty()
@@ -69,29 +70,13 @@ object TrojanFmt : FmtBase() {
server.flow = profileItem.flow
}
val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
val sni = outboundBean?.streamSettings?.let {
V2rayConfigManager.populateTransportSettings(it, profileItem)
}
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
outboundBean?.streamSettings?.let {
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
}
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.extension.idnHost
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 java.net.URI
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null
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.serverPort = uri.port.toString()
config.password = uri.userInfo
@@ -57,7 +57,7 @@ object VlessFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VLESS)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
@@ -67,31 +67,13 @@ object VlessFmt : FmtBase() {
vnext.users[0].flow = profileItem.flow
}
val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
val sni = outboundBean?.streamSettings?.let {
V2rayConfigManager.populateTransportSettings(it, profileItem)
}
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
profileItem.publicKey,
profileItem.shortId,
profileItem.spiderX,
)
outboundBean?.streamSettings?.let {
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
}
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.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -33,7 +34,7 @@ object VmessFmt : FmtBase() {
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
Log.w(AppConfig.TAG, "Toast decoding failed")
return null
}
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
@@ -43,7 +44,7 @@ object VmessFmt : FmtBase() {
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
Log.w(AppConfig.TAG, "Toast incorrect protocol")
return null
}
@@ -73,6 +74,7 @@ object VmessFmt : FmtBase() {
config.serviceName = vmessQRCode.path
config.authority = vmessQRCode.host
}
else -> {}
}
@@ -119,6 +121,7 @@ object VmessFmt : FmtBase() {
vmessQRCode.path = config.serviceName.orEmpty()
vmessQRCode.host = config.authority.orEmpty()
}
else -> {}
}
@@ -148,7 +151,7 @@ object VmessFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null
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.serverPort = uri.port.toString()
config.password = uri.userInfo
@@ -166,7 +169,7 @@ object VmessFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.VMESS)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
vnext.address = profileItem.server.orEmpty()
@@ -175,29 +178,13 @@ object VmessFmt : FmtBase() {
vnext.users[0].security = profileItem.method
}
val sni = outboundBean?.streamSettings?.populateTransportSettings(
profileItem.network.orEmpty(),
profileItem.headerType,
profileItem.host,
profileItem.path,
profileItem.seed,
profileItem.quicSecurity,
profileItem.quicKey,
profileItem.mode,
profileItem.serviceName,
profileItem.authority,
)
val sni = outboundBean?.streamSettings?.let {
V2rayConfigManager.populateTransportSettings(it, profileItem)
}
outboundBean?.streamSettings?.populateTlsSettings(
profileItem.security.orEmpty(),
profileItem.insecure == true,
if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
profileItem.fingerPrint,
profileItem.alpn,
null,
null,
null
)
outboundBean?.streamSettings?.let {
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
}
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.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -24,16 +25,16 @@ object WireguardFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null
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.serverPort = uri.port.toString()
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.preSharedKey = queryParam["presharedkey"].orEmpty()
config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
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
}
@@ -83,7 +84,7 @@ object WireguardFmt : FmtBase() {
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.publicKey = peerParams["publickey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"].orEmpty()
config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
val endpoint = peerParams["endpoint"].orEmpty()
val endpointParts = endpoint.split(":", limit = 2)
if (endpointParts.size == 2) {
@@ -105,18 +106,18 @@ object WireguardFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() }
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
}
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

View File

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

View File

@@ -3,7 +3,6 @@ package com.v2ray.ang.handler
import android.util.Log
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
@@ -26,7 +25,7 @@ object MigrateManager {
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) {
var configOld = decodeServerConfigOld(guid) ?: continue
@@ -43,9 +42,9 @@ object MigrateManager {
//check and remove old
decodeServerConfig(guid) ?: continue
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
}
@@ -172,7 +171,7 @@ object MigrateManager {
outbound.settings?.let { wireguard ->
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.mtu = wireguard.mtu
config.reserved = wireguard.reserved?.joinToString(",").removeWhiteSpace().toString()

View File

@@ -571,7 +571,7 @@ object MmkvManager {
* @param startOnBoot Whether to start on boot.
*/
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)
return true
} catch (e: Exception) {
e.printStackTrace()
Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
return false
}
}
@@ -167,11 +167,11 @@ object SettingsManager {
}
val guid = MmkvManager.getSelectServer() ?: return false
val config = MmkvManager.decodeServerConfig(guid) ?: return false
val config = decodeServerConfig(guid) ?: return false
if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) ?: return false
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java)
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }?.any {
val exist = v2rayConfig.routing.rules.filter { it.outboundTag == TAG_DIRECT }.any {
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
}
return exist == true
@@ -216,7 +216,7 @@ object SettingsManager {
* @return The ProfileItem.
*/
fun getServerViaRemarks(remarks: String?): ProfileItem? {
if (remarks == null) {
if (remarks.isNullOrEmpty()) {
return null
}
val serverList = decodeServerList()
@@ -242,7 +242,7 @@ object SettingsManager {
* @return The HTTP port.
*/
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)
}
}
Log.i(
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
@@ -319,10 +316,10 @@ object SettingsManager {
*/
fun getDelayTestUrl(second: Boolean = false): String {
return if (second) {
AppConfig.DelayTestUrl2
AppConfig.DELAY_TEST_URL2
} else {
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
?: AppConfig.DelayTestUrl
?: AppConfig.DELAY_TEST_URL
}
}
@@ -343,6 +340,7 @@ object SettingsManager {
Language.VIETNAMESE -> Locale("vi")
Language.RUSSIAN -> Locale("ru")
Language.PERSIAN -> Locale("fa")
Language.ARABIC -> Locale("ar")
Language.BANGLA -> Locale("bn")
Language.BAKHTIARI -> Locale("bqi", "IR")
}

View File

@@ -6,8 +6,10 @@ import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.IPAPIInfo
import com.v2ray.ang.extension.responseLength
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import kotlinx.coroutines.isActive
import libv2ray.Libv2ray
import java.io.IOException
@@ -51,7 +53,7 @@ object SpeedtestManager {
return try {
Libv2ray.measureOutboundDelay(config, SettingsManager.getDelayTestUrl())
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, "realPing: $e")
Log.e(AppConfig.TAG, "Failed to measure outbound delay", e)
-1L
}
}
@@ -76,7 +78,7 @@ object SpeedtestManager {
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to ping URL: $url", e)
}
return "-1ms"
}
@@ -103,11 +105,11 @@ object SpeedtestManager {
socket.close()
return time
} catch (e: UnknownHostException) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Unknown host: $url", e)
} catch (e: IOException) {
Log.d(AppConfig.ANG_PACKAGE, "socketConnectTime IOException: $e")
Log.e(AppConfig.TAG, "socketConnectTime IOException: $e")
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
}
return -1
}
@@ -152,18 +154,26 @@ object SpeedtestManager {
)
}
} 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)
} 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)
} finally {
conn?.disconnect()
conn.disconnect()
}
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.
*

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 ->
val value = animation.animatedValue as Float
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()
duration = ANIMATION_DURATION
@@ -144,4 +144,4 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
private const val SWIPE_THRESHOLD = 0.25f
private const val ANIMATION_DURATION: Long = 200
}
}
}

View File

@@ -32,10 +32,10 @@ import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.system.Os
import android.widget.Toast
import androidx.core.os.bundleOf
import com.v2ray.ang.AngApplication
import com.v2ray.ang.extension.listenForPackageChanges
import com.v2ray.ang.extension.toast
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
import java.io.File
import java.io.FileNotFoundException
@@ -126,7 +126,7 @@ object PluginManager {
if (providers.size > 1) {
val message =
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show()
AngApplication.application.toast(message)
throw IllegalStateException(message)
}
val provider = providers.single().providerInfo
@@ -224,8 +224,8 @@ object PluginManager {
fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
is String -> value
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
.getString(value)
// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
// .getString(value)
null -> null
else -> error("meta-data $key has invalid type ${value.javaClass}")

View File

@@ -35,7 +35,7 @@ class TaskerReceiver : BroadcastReceiver() {
V2RayServiceManager.stopVService(context)
}
} 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
speedNotificationJob?.cancel()
speedNotificationJob = null
mNotificationManager = null
}
/**

View File

@@ -2,7 +2,7 @@ package com.v2ray.ang.service
import android.content.Context
import android.util.Log
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -16,7 +16,7 @@ class ProcessService {
* @param cmd The command to run.
*/
fun runProcess(context: Context, cmd: MutableList<String>) {
Log.d(ANG_PACKAGE, cmd.toString())
Log.i(AppConfig.TAG, cmd.toString())
try {
val proBuilder = ProcessBuilder(cmd)
@@ -27,14 +27,14 @@ class ProcessService {
CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(50L)
Log.d(ANG_PACKAGE, "runProcess check")
Log.i(AppConfig.TAG, "runProcess check")
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) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, e.toString(), e)
}
}
@@ -43,10 +43,10 @@ class ProcessService {
*/
fun stopProcess() {
try {
Log.d(ANG_PACKAGE, "runProcess destroy")
Log.i(AppConfig.TAG, "runProcess destroy")
process?.destroy()
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to destroy process", e)
}
}
}

View File

@@ -1,6 +1,5 @@
package com.v2ray.ang.service
import android.annotation.TargetApi
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -9,6 +8,8 @@ import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
@@ -16,7 +17,7 @@ import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference
@TargetApi(Build.VERSION_CODES.N)
@RequiresApi(Build.VERSION_CODES.N)
class QSTileService : TileService() {
/**
@@ -61,7 +62,7 @@ class QSTileService : TileService() {
applicationContext.unregisterReceiver(mMsgReceive)
mMsgReceive = null
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to unregister receiver", e)
}
}

View File

@@ -38,7 +38,7 @@ object SubscriptionUpdater {
*/
@SuppressLint("MissingPermission")
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 }
@@ -56,10 +56,7 @@ object SubscriptionUpdater {
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(3, notification.build())
Log.d(
AppConfig.ANG_PACKAGE,
"subscription automatic update: ---${subItem.remarks}"
)
Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
updateConfigViaSub(Pair(sub.first, subItem))
notification.setContentText("Updating ${subItem.remarks}")
}

View File

@@ -27,7 +27,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
* @return The start mode.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
V2RayServiceManager.startV2rayPoint()
V2RayServiceManager.startCoreLoop()
return START_STICKY
}
@@ -36,7 +36,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
*/
override fun onDestroy() {
super.onDestroy()
V2RayServiceManager.stopV2rayPoint()
V2RayServiceManager.stopCoreLoop()
}
/**

View File

@@ -9,13 +9,13 @@ import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.PluginUtil
@@ -24,14 +24,14 @@ import go.Seq
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import libv2ray.CoreCallbackHandler
import libv2ray.CoreController
import libv2ray.Libv2ray
import libv2ray.V2RayPoint
import libv2ray.V2RayVPNServiceSupportsSet
import java.lang.ref.SoftReference
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 var currentConfig: ProfileItem? = null
@@ -39,7 +39,7 @@ object V2RayServiceManager {
set(value) {
field = value
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.
* @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.
@@ -91,10 +91,13 @@ object V2RayServiceManager {
/**
* 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.
*/
private fun startContextService(context: Context) {
if (v2rayPoint.isRunning) return
if (coreController.isRunning) {
return
}
val guid = MmkvManager.getSelectServer() ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return
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):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
* Starts the V2Ray point.
* Starts the V2Ray core service.
*/
fun startV2rayPoint() {
val service = getService() ?: return
val guid = MmkvManager.getSelectServer() ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return
if (v2rayPoint.isRunning) {
return
fun startCoreLoop(): Boolean {
if (coreController.isRunning) {
return false
}
val service = getService() ?: return false
val guid = MmkvManager.getSelectServer() ?: return false
val config = MmkvManager.decodeServerConfig(guid) ?: return false
val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status)
return
return false
try {
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
@@ -144,42 +148,52 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} 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
try {
v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
coreController.startLoop(result.content)
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to start Core loop", e)
return false
}
if (v2rayPoint.isRunning) {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
NotificationService.showNotification(currentConfig)
PluginUtil.runPlugin(service, config, result.domainPort)
} else {
if (coreController.isRunning == false) {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
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() {
val service = getService() ?: return
fun stopCoreLoop(): Boolean {
val service = getService() ?: return false
if (v2rayPoint.isRunning) {
if (coreController.isRunning) {
CoroutineScope(Dispatchers.IO).launch {
try {
v2rayPoint.stopLoop()
coreController.stopLoop()
} 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 {
service.unregisterReceiver(mMsgReceive)
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
}
PluginUtil.stopPlugin()
return true
}
/**
@@ -202,40 +218,52 @@ object V2RayServiceManager {
* @return The statistics value.
*/
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() {
if (coreController.isRunning == false) {
return
}
CoroutineScope(Dispatchers.IO).launch {
val service = getService() ?: return@launch
var time = -1L
var errstr = ""
if (v2rayPoint.isRunning) {
try {
time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = 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"
}
}
var errorStr = ""
try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message"
}
val result = if (time == -1L) {
service.getString(R.string.connection_test_error, errstr)
} else {
service.getString(R.string.connection_test_available, time)
if (time == -1L) {
try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
} 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)
// 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()
}
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 {
val serviceControl = serviceControl?.get() ?: return -1
// called by go
return try {
serviceControl.stopService()
0
} catch (e: Exception) {
Log.d(ANG_PACKAGE, e.toString())
Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
-1
}
}
override fun prepare(): Long {
return 0
}
override fun protect(l: Long): Boolean {
val serviceControl = serviceControl?.get() ?: return true
return serviceControl.vpnProtect(l.toInt())
}
/**
* Called by Go to emit status.
* @param l The status code.
* @param s The status message.
* @return The status code.
* Called when V2Ray core emits status information.
* @param l Status code.
* @param s Status message.
* @return Always returns 0.
*/
override fun onEmitStatus(l: Long, s: String?): Long {
return 0
}
/**
* Called by Go to set up the service.
* @param s The setup string.
* @return The status code.
*/
override fun setup(s: String): Long {
val serviceControl = serviceControl?.get() ?: return -1
return try {
serviceControl.startService()
NotificationService.startSpeedNotification(currentConfig)
0
} catch (e: Exception) {
Log.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() {
/**
* Handles received broadcast messages.
* Processes service control messages and screen state changes.
* @param ctx The context in which the receiver is running.
* @param intent The intent being received.
*/
@@ -307,7 +329,7 @@ object V2RayServiceManager {
val serviceControl = serviceControl?.get() ?: return
when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_REGISTER_CLIENT -> {
if (v2rayPoint.isRunning) {
if (coreController.isRunning) {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
} else {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
@@ -323,12 +345,12 @@ object V2RayServiceManager {
}
AppConfig.MSG_STATE_STOP -> {
Log.d(ANG_PACKAGE, "Stop Service")
Log.i(AppConfig.TAG, "Stop Service")
serviceControl.stopService()
}
AppConfig.MSG_STATE_RESTART -> {
Log.d(ANG_PACKAGE, "Restart Service")
Log.i(AppConfig.TAG, "Restart Service")
serviceControl.stopService()
Thread.sleep(500L)
startVService(serviceControl.getService())
@@ -341,12 +363,12 @@ object V2RayServiceManager {
when (intent?.action) {
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)
}
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)
}
}

View File

@@ -32,7 +32,7 @@ class V2RayTestService : Service() {
override fun onCreate() {
super.onCreate()
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)
return delay
} else {
val config = V2rayConfigManager.getV2rayConfig(this, guid)
if (!config.status) {
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(this, guid)
if (!configResult.status) {
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 androidx.annotation.RequiresApi
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
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_ROUTER = "fc00::10:10:14:2"
private const val TUN2SOCKS = "libtun2socks.so"
}
private lateinit var mInterface: ParcelFileDescriptor
@@ -105,7 +104,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
V2RayServiceManager.startV2rayPoint()
if (V2RayServiceManager.startCoreLoop()) {
startService()
}
return START_STICKY
//return super.onStartCommand(intent, flags, startId)
}
@@ -166,7 +167,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
val bypassLan = SettingsManager.routingRulesetsBypassLan()
if (bypassLan) {
resources.getStringArray(R.array.bypass_private_ip_address).forEach {
AppConfig.BYPASS_PRIVATE_IP_LIST.forEach {
val addr = it.split('/')
builder.addRoute(addr[0], addr[1].toInt())
}
@@ -209,7 +210,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
else
builder.addAllowedApplication(it)
} 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 {
@@ -227,7 +228,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
try {
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
} 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
} catch (e: Exception) {
// non-nullable lateinit var
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to establish VPN interface", e)
stopV2Ray()
}
return false
@@ -256,6 +257,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
* Starts the tun2socks process with the appropriate parameters.
*/
private fun runTun2socks() {
Log.i(AppConfig.TAG, "Start run $TUN2SOCKS")
val socksPort = SettingsManager.getSocksPort()
val cmd = arrayListOf(
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
@@ -277,7 +279,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
cmd.add("--dnsgw")
cmd.add("$LOOPBACK:${localDnsPort}")
}
Log.d(packageName, cmd.toString())
Log.i(AppConfig.TAG, cmd.toString())
try {
val proBuilder = ProcessBuilder(cmd)
@@ -286,19 +288,19 @@ class V2RayVpnService : VpnService(), ServiceControl {
.directory(applicationContext.filesDir)
.start()
Thread {
Log.d(packageName, "$TUN2SOCKS check")
Log.i(AppConfig.TAG, "$TUN2SOCKS check")
process.waitFor()
Log.d(packageName, "$TUN2SOCKS exited")
Log.i(AppConfig.TAG, "$TUN2SOCKS exited")
if (isRunning) {
Log.d(packageName, "$TUN2SOCKS restart")
Log.i(AppConfig.TAG, "$TUN2SOCKS restart")
runTun2socks()
}
}.start()
Log.d(packageName, process.toString())
Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}")
sendFd()
} 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() {
val fd = mInterface.fileDescriptor
val path = File(applicationContext.filesDir, "sock_path").absolutePath
Log.d(packageName, path)
Log.i(AppConfig.TAG, "LocalSocket path : $path")
CoroutineScope(Dispatchers.IO).launch {
var tries = 0
while (true) try {
Thread.sleep(50L shl tries)
Log.d(packageName, "sendFd tries: $tries")
Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries")
LocalSocket().use { localSocket ->
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.setFileDescriptorsForSend(arrayOf(fd))
@@ -323,7 +325,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
break
} catch (e: Exception) {
Log.d(packageName, e.toString())
Log.e(AppConfig.TAG, "Failed to send file descriptor, try: $tries", e)
if (tries > 5) break
tries += 1
}
@@ -349,13 +351,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
try {
Log.d(packageName, "tun2socks destroy")
Log.i(AppConfig.TAG, "$TUN2SOCKS destroy")
process.destroy()
} catch (e: Exception) {
Log.d(packageName, e.toString())
Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e)
}
V2RayServiceManager.stopV2rayPoint()
V2RayServiceManager.stopCoreLoop()
if (isForced) {
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
@@ -367,8 +369,8 @@ class V2RayVpnService : VpnService(), ServiceControl {
try {
mInterface.close()
} catch (ignored: Exception) {
// ignored
} catch (e: Exception) {
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.Bundle
import android.util.Log
import android.widget.Toast
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.dto.CheckUpdateResult
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.UpdateCheckerManager
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
@@ -32,7 +42,7 @@ class AboutActivity : BaseActivity() {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
}
} else {
toast(R.string.toast_permission_denied)
@@ -50,9 +60,9 @@ class AboutActivity : BaseActivity() {
binding.layoutBackup.setOnClickListener {
val ret = backupConfiguration(extDir.absolutePath)
if (ret.first) {
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
@@ -72,7 +82,7 @@ class AboutActivity : BaseActivity() {
)
)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
@@ -88,23 +98,40 @@ class AboutActivity : BaseActivity() {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
}
} else {
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 {
Utils.openUri(this, AppConfig.v2rayNGUrl)
Utils.openUri(this, AppConfig.APP_URL)
}
binding.layoutFeedback.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGIssues)
Utils.openUri(this, AppConfig.APP_ISSUES_URL)
}
binding.layoutOssLicenses.setOnClickListener {
val webView = android.webkit.WebView(this);
val webView = android.webkit.WebView(this)
webView.loadUrl("file:///android_asset/open_source_licenses.html")
android.app.AlertDialog.Builder(this)
.setTitle("Open source licenses")
@@ -114,11 +141,11 @@ class AboutActivity : BaseActivity() {
}
binding.layoutTgChannel.setOnClickListener {
Utils.openUri(this, AppConfig.TgChannelUrl)
Utils.openUri(this, AppConfig.TG_CHANNEL_URL)
}
binding.layoutPrivacyPolicy.setOnClickListener {
Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY)
}
"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(
"yyyy-MM-dd-HH-mm-ss",
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()}"
if (!ZipUtil.unzipToFolder(zipFile, backupDir)) {
@@ -167,7 +194,7 @@ class AboutActivity : BaseActivity() {
try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} 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)
}
}
@@ -185,18 +212,38 @@ class AboutActivity : BaseActivity() {
}
}
if (restoreConfiguration(targetFile)) {
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
} catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e)
toast(R.string.toast_failure)
Log.e(AppConfig.TAG, "Error during file restore", e)
toastError(R.string.toast_failure)
}
}
}
private fun toast(messageResId: Int) {
Toast.makeText(this, getString(messageResId), Toast.LENGTH_SHORT).show()
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
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -21,7 +24,7 @@ import java.io.IOException
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
var logsetsAll: MutableList<String> = mutableListOf()
private var logsetsAll: MutableList<String> = mutableListOf()
var logsets: MutableList<String> = mutableListOf()
private val adapter by lazy { LogcatRecyclerAdapter(this) }
@@ -62,12 +65,12 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
launch(Dispatchers.Main) {
logsetsAll = allText.toMutableList()
logsets = allText.toMutableList()
adapter.notifyDataSetChanged()
refreshData()
binding.refreshLayout.isRefreshing = false
}
}
} 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) {
logsetsAll.clear()
logsets.clear()
adapter.notifyDataSetChanged()
refreshData()
}
}
} 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) {
R.id.copy_all -> {
Utils.setClipboard(this, logsets.joinToString("\n"))
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
true
}
@@ -138,11 +141,16 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
logsetsAll.filter { it.contains(key) }.toMutableList()
}
adapter?.notifyDataSetChanged()
refreshData()
return true
}
override fun onRefresh() {
getLogcat()
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
adapter.notifyDataSetChanged()
}
}

View File

@@ -1,13 +1,16 @@
package com.v2ray.ang.ui
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter<LogcatRecyclerAdapter.MainViewHolder>() {
private var mActivity: LogcatActivity = activity
override fun getItemCount() = mActivity.logsets.size
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
@@ -22,7 +25,7 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
}
} 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
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
@@ -8,6 +9,7 @@ import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
@@ -21,8 +23,8 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.navigation.NavigationView
import com.google.android.material.tabs.TabLayout
import com.v2ray.ang.AppConfig
@@ -31,6 +33,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MigrateManager
import com.v2ray.ang.handler.MmkvManager
@@ -84,9 +87,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
Action.IMPORT_QR_CODE_CONFIG ->
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
// Action.IMPORT_QR_CODE_URL ->
// scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
Action.READ_CONTENT_FROM_URI ->
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
@@ -107,8 +107,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
enum class Action {
NONE,
IMPORT_QR_CODE_CONFIG,
//IMPORT_QR_CODE_URL,
READ_CONTENT_FROM_URI,
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?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
@@ -162,7 +154,11 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) {
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
} else {
binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
}
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = adapter
@@ -200,6 +196,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
})
}
@SuppressLint("NotifyDataSetChanged")
private fun setupViewModel() {
mainViewModel.updateListAction.observe(this) { index ->
if (index >= 0) {
@@ -265,7 +262,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
binding.tabGroup.isVisible = true
}
fun startV2Ray() {
private fun startV2Ray() {
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
toast(R.string.title_file_chooser)
return
@@ -273,7 +270,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
V2RayServiceManager.startVService(this)
}
fun restartV2Ray() {
private fun restartV2Ray() {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this)
}
@@ -317,7 +314,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.import_qrcode -> {
importQRcode(true)
importQRcode()
true
}
@@ -371,26 +368,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
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 -> {
exportAll()
true
@@ -454,16 +431,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from qrcode
*/
private fun importQRcode(forConfig: Boolean): Boolean {
private fun importQRcode(): Boolean {
val permission = Manifest.permission.CAMERA
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
if (forConfig) {
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
} else {
//scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
}
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
} 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)
}
return true
@@ -478,7 +451,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
val clipboard = Utils.getClipboard(this)
importBatchConfig(clipboard)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import config from clipboard", e)
return false
}
return true
@@ -499,102 +472,34 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
countSub > 0 -> initGroupTab()
else -> toast(R.string.toast_failure)
else -> toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
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 {
try {
showFileChooser()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import config from local file", e)
return false
}
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
*/
@@ -609,7 +514,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
toast(getString(R.string.title_update_config_count, count))
mainViewModel.reloadServerList()
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
}
@@ -625,7 +530,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
if (ret > 0)
toast(getString(R.string.title_export_config_count, ret))
else
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
binding.pbWaiting.hide()
}
}
@@ -737,36 +642,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
importBatchConfig(input?.bufferedReader()?.readText())
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to read content from URI", e)
}
} else {
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?) {
binding.tvTestState.text = content
}
@@ -793,14 +675,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
// Handle navigation view item clicks here.
when (item.itemId) {
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(
Intent(this, SettingsActivity::class.java)
.putExtra("isRunning", mainViewModel.isRunning.value == true)
)
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.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
R.id.logcat -> startActivity(Intent(this, LogcatActivity::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.graphics.Color
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -17,13 +18,16 @@ import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
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.MmkvManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.service.V2RayServiceManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -37,175 +41,278 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
private val share_method: Array<out String> by lazy {
mActivity.resources.getStringArray(R.array.share_method)
}
private val share_method_more: Array<out String> by lazy {
mActivity.resources.getStringArray(R.array.share_method_more)
}
var isRunning = false
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 onBindViewHolder(holder: BaseViewHolder, position: Int) {
if (holder is MainViewHolder) {
val guid = mActivity.mainViewModel.serversCache[position].guid
val profile = mActivity.mainViewModel.serversCache[position].profile
// //filter
// if (mActivity.mainViewModel.subscriptionId.isNotEmpty()
// && mActivity.mainViewModel.subscriptionId != config.subscriptionId
// ) {
// holder.itemMainBinding.cardView.visibility = View.GONE
// } else {
// holder.itemMainBinding.cardView.visibility = View.VISIBLE
// }
val isCustom = profile.configType == EConfigType.CUSTOM
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
holder.itemMainBinding.tvName.text = profile.remarks
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
//Name address
holder.itemMainBinding.tvName.text = profile.remarks
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
holder.itemMainBinding.tvType.text = profile.configType.name
//TestResult
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
if ((aff?.testDelayMillis ?: 0L) < 0L) {
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
} else {
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
}
//layoutIndicator
if (guid == MmkvManager.getSelectServer()) {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorAccent)
} else {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
}
holder.itemMainBinding.tvSubscription.text =
if (mActivity.mainViewModel.subscriptionId.isEmpty())
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks.orEmpty()
else
""
var shareOptions = share_method.asList()
when (profile.configType) {
EConfigType.CUSTOM -> {
holder.itemMainBinding.tvType.text = mActivity.getString(R.string.server_customize_config)
shareOptions = shareOptions.takeLast(1)
//subscription remarks
val subRemarks = getSubscriptionRemarks(profile)
holder.itemMainBinding.tvSubscription.text = subRemarks
holder.itemMainBinding.layoutSubscription.visibility = if (subRemarks.isEmpty()) View.GONE else View.VISIBLE
//layout
if (doubleColumnDisplay) {
holder.itemMainBinding.layoutShare.visibility = View.GONE
holder.itemMainBinding.layoutEdit.visibility = View.GONE
holder.itemMainBinding.layoutRemove.visibility = View.GONE
holder.itemMainBinding.layoutMore.visibility = View.VISIBLE
//share method
val shareOptions = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
holder.itemMainBinding.layoutMore.setOnClickListener {
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
}
} else {
holder.itemMainBinding.layoutShare.visibility = View.VISIBLE
holder.itemMainBinding.layoutEdit.visibility = View.VISIBLE
holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE
holder.itemMainBinding.layoutMore.visibility = View.GONE
//share method
val shareOptions = if (isCustom) share_method.asList().takeLast(1) else share_method.asList()
holder.itemMainBinding.layoutShare.setOnClickListener {
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
}
else -> {
holder.itemMainBinding.tvType.text = profile.configType.name
holder.itemMainBinding.layoutEdit.setOnClickListener {
editServer(guid, profile)
}
}
// Hide xxx:xxx:***/xxx.xxx.xxx.***
val strState = "${
profile.server?.let {
if (it.contains(":"))
it.split(":").take(2).joinToString(":", postfix = ":***")
else
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
}
} : ${profile.serverPort}"
holder.itemMainBinding.tvStatistics.text = strState
holder.itemMainBinding.layoutShare.setOnClickListener {
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
try {
when (i) {
0 -> {
if (profile.configType == EConfigType.CUSTOM) {
shareFullContent(guid)
} else {
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
}
}
1 -> {
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
mActivity.toast(R.string.toast_success)
} else {
mActivity.toast(R.string.toast_failure)
}
}
2 -> shareFullContent(guid)
else -> mActivity.toast("else")
}
} catch (e: Exception) {
e.printStackTrace()
}
}.show()
}
holder.itemMainBinding.layoutEdit.setOnClickListener {
val intent = Intent().putExtra("guid", guid)
.putExtra("isRunning", isRunning)
.putExtra("createConfigType", profile.configType.value)
if (profile.configType == EConfigType.CUSTOM) {
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
} else {
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
}
}
holder.itemMainBinding.layoutRemove.setOnClickListener {
if (guid != MmkvManager.getSelectServer()) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
removeServer(guid, position)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting
}
.show()
} else {
removeServer(guid, position)
}
} else {
application.toast(R.string.toast_action_not_allowed)
holder.itemMainBinding.layoutRemove.setOnClickListener {
removeServer(guid, position)
}
}
holder.itemMainBinding.infoContainer.setOnClickListener {
val selected = MmkvManager.getSelectServer()
if (guid != selected) {
MmkvManager.setSelectServer(guid)
if (!TextUtils.isEmpty(selected)) {
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
}
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) {
V2RayServiceManager.stopVService(mActivity)
mActivity.lifecycleScope.launch {
try {
delay(500)
V2RayServiceManager.startVService(mActivity)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
}
if (holder is FooterViewHolder) {
if (true) {
holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
} else {
holder.itemFooterBinding.layoutEdit.setOnClickListener {
Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
}
setSelectServer(guid)
}
}
// if (holder is FooterViewHolder) {
// if (true) {
// holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
// } else {
// holder.itemFooterBinding.layoutEdit.setOnClickListener {
// Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
// }
// }
// }
}
private fun shareFullContent(guid: String) {
if (AngConfigManager.shareFullContent2Clipboard(mActivity, guid) == 0) {
mActivity.toast(R.string.toast_success)
/**
* Gets the server address information
* Hides part of IP or domain information for privacy protection
* @param profile The server configuration
* @return Formatted address string
*/
private fun getAddress(profile: ProfileItem): String {
// Hide xxx:xxx:***/xxx.xxx.xxx.***
return "${
profile.server?.let {
if (it.contains(":"))
it.split(":").take(2).joinToString(":", postfix = ":***")
else
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
}
} : ${profile.serverPort}"
}
/**
* Gets the subscription remarks information
* @param profile The server configuration
* @return Subscription remarks string, or empty string if none
*/
private fun getSubscriptionRemarks(profile: ProfileItem): String {
val subRemarks =
if (mActivity.mainViewModel.subscriptionId.isEmpty())
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
else
null
return subRemarks?.toString() ?: ""
}
/**
* Shares server configuration
* Displays a dialog with sharing options and executes the selected action
* @param guid The server unique identifier
* @param profile The server configuration
* @param position The position in the list
* @param shareOptions The list of share options
* @param skip The number of options to skip
*/
private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List<String>, skip: Int) {
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
try {
when (i + skip) {
0 -> showQRCode(guid)
1 -> share2Clipboard(guid)
2 -> shareFullContent(guid)
3 -> editServer(guid, profile)
4 -> removeServer(guid, position)
else -> mActivity.toast("else")
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error when sharing server", e)
}
}.show()
}
/**
* Displays QR code for the server configuration
* @param guid The server unique identifier
*/
private fun showQRCode(guid: String) {
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
}
/**
* Shares server configuration to clipboard
* @param guid The server unique identifier
*/
private fun share2Clipboard(guid: String) {
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
mActivity.toastSuccess(R.string.toast_success)
} else {
mActivity.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) {
mActivity.lifecycleScope.launch(Dispatchers.IO) {
val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
launch(Dispatchers.Main) {
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) {
val intent = Intent().putExtra("guid", guid)
.putExtra("isRunning", isRunning)
.putExtra("createConfigType", profile.configType.value)
if (profile.configType == EConfigType.CUSTOM) {
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
} else {
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
}
}
/**
* Removes server configuration
* Handles confirmation dialog and related checks
* @param guid The server unique identifier
* @param position The position in the list
*/
private fun removeServer(guid: String, position: Int) {
if (guid != MmkvManager.getSelectServer()) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
removeServerSub(guid, position)
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
//do noting
}
.show()
} else {
removeServerSub(guid, position)
}
} else {
application.toast(R.string.toast_action_not_allowed)
}
}
/**
* Executes the actual server removal process
* @param guid The server unique identifier
* @param position The position in the list
*/
private fun removeServerSub(guid: String, position: Int) {
mActivity.mainViewModel.removeServer(guid)
notifyItemRemoved(position)
notifyItemRangeChanged(position, mActivity.mainViewModel.serversCache.size)
}
/**
* Sets the selected server
* Updates UI and restarts service if needed
* @param guid The server unique identifier to select
*/
private fun setSelectServer(guid: String) {
val selected = MmkvManager.getSelectServer()
if (guid != selected) {
MmkvManager.setSelectServer(guid)
if (!TextUtils.isEmpty(selected)) {
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
}
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) {
V2RayServiceManager.stopVService(mActivity)
mActivity.lifecycleScope.launch {
try {
delay(500)
V2RayServiceManager.startVService(mActivity)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to restart V2Ray service", e)
}
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
return when (viewType) {
VIEW_TYPE_ITEM ->

View File

@@ -1,10 +1,12 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
@@ -13,21 +15,21 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBypassListBinding
import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.extension.v2RayApplication
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.Utils
import es.dmoral.toasty.Toasty
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.Collator
class PerAppProxyActivity : BaseActivity() {
private val binding by lazy {
ActivityBypassListBinding.inflate(layoutInflater)
}
private val binding by lazy { ActivityBypassListBinding.inflate(layoutInflater) }
private var adapter: PerAppProxyAdapter? = null
private var appsAll: List<AppInfo>? = null
@@ -51,13 +53,13 @@ class PerAppProxyActivity : BaseActivity() {
appsList.forEach { app ->
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
}
appsList.sortedWith(Comparator { p1, p2 ->
appsList.sortedWith { p1, p2 ->
when {
p1.isSelected > p2.isSelected -> -1
p1.isSelected == p2.isSelected -> 0
else -> 1
}
})
}
} else {
val collator = Collator.getInstance()
appsList.sortedWith(compareBy(collator) { it.appName })
@@ -83,6 +85,10 @@ class PerAppProxyActivity : BaseActivity() {
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
}
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() {
@@ -112,8 +118,10 @@ class PerAppProxyActivity : BaseActivity() {
return super.onCreateOptionsMenu(menu)
}
@SuppressLint("NotifyDataSetChanged")
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 }
if (it.blacklist.containsAll(pkgNames)) {
it.apps.forEach {
@@ -152,7 +160,7 @@ class PerAppProxyActivity : BaseActivity() {
toast(R.string.msg_downloading_content)
binding.pbWaiting.show()
val url = AppConfig.androidpackagenamelistUrl
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000)
if (content.isNullOrEmpty()) {
@@ -160,9 +168,9 @@ class PerAppProxyActivity : BaseActivity() {
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
}
launch(Dispatchers.Main) {
Log.d(ANG_PACKAGE, content)
Log.i(AppConfig.TAG, content)
selectProxyApp(content, true)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
binding.pbWaiting.hide()
}
}
@@ -172,7 +180,7 @@ class PerAppProxyActivity : BaseActivity() {
val content = Utils.getClipboard(applicationContext)
if (TextUtils.isEmpty(content)) return
selectProxyApp(content, false)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
}
private fun exportProxyApp() {
@@ -182,9 +190,10 @@ class PerAppProxyActivity : BaseActivity() {
lst = lst + System.getProperty("line.separator") + it
}
Utils.setClipboard(applicationContext, lst)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
}
@SuppressLint("NotifyDataSetChanged")
private fun selectProxyApp(content: String, force: Boolean): Boolean {
try {
val proxyApps = if (TextUtils.isEmpty(content)) {
@@ -197,10 +206,10 @@ class PerAppProxyActivity : BaseActivity() {
adapter?.blacklist?.clear()
if (binding.switchBypassApps.isChecked) {
adapter?.let {
adapter?.let { it ->
it.apps.forEach block@{
val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName)
Log.i(AppConfig.TAG, packageName)
if (!inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist?.add(packageName)
println(packageName)
@@ -210,10 +219,10 @@ class PerAppProxyActivity : BaseActivity() {
it.notifyDataSetChanged()
}
} else {
adapter?.let {
adapter?.let { it ->
it.apps.forEach block@{
val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName)
Log.i(AppConfig.TAG, packageName)
if (inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist?.add(packageName)
println(packageName)
@@ -224,7 +233,7 @@ class PerAppProxyActivity : BaseActivity() {
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Error selecting proxy app", e)
return false
}
return true
@@ -259,7 +268,12 @@ class PerAppProxyActivity : BaseActivity() {
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
binding.recyclerView.adapter = adapter
adapter?.notifyDataSetChanged()
refreshData()
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.dto.RulesetItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
@@ -78,7 +79,7 @@ class RoutingEditActivity : BaseActivity() {
}
SettingsManager.saveRoutingRuleset(position, rulesetItem)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -1,12 +1,12 @@
package com.v2ray.ang.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
@@ -17,6 +17,8 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
import com.v2ray.ang.dto.RulesetItem
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.SettingsManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -63,15 +65,9 @@ class RoutingSettingActivity : BaseActivity() {
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
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])
}
binding.tvDomainStrategySummary.text = getDomainStrategy()
binding.layoutDomainStrategy.setOnClickListener {
setDomainStrategy()
}
}
@@ -87,7 +83,6 @@ class RoutingSettingActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
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_rulesets_from_clipboard -> importFromClipboard().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)
}
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() {
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
@@ -104,11 +115,11 @@ class RoutingSettingActivity : BaseActivity() {
SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i)
launch(Dispatchers.Main) {
refreshData()
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
@@ -124,8 +135,8 @@ class RoutingSettingActivity : BaseActivity() {
val clipboard = try {
Utils.getClipboard(this)
} catch (e: Exception) {
e.printStackTrace()
toast(R.string.toast_failure)
Log.e(AppConfig.TAG, "Failed to get clipboard content", e)
toastError(R.string.toast_failure)
return@setPositiveButton
}
lifecycleScope.launch(Dispatchers.IO) {
@@ -133,9 +144,9 @@ class RoutingSettingActivity : BaseActivity() {
withContext(Dispatchers.Main) {
if (result) {
refreshData()
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
}
@@ -149,10 +160,10 @@ class RoutingSettingActivity : BaseActivity() {
private fun export2Clipboard() {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
} else {
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) {
if (result) {
refreshData()
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
}
@@ -184,6 +195,7 @@ class RoutingSettingActivity : BaseActivity() {
return true
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
rulesets.clear()
rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf())

View File

@@ -6,6 +6,8 @@ import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
class ScScannerActivity : BaseActivity() {
@@ -27,7 +29,7 @@ class ScScannerActivity : BaseActivity() {
importQRcode()
}
fun importQRcode(): Boolean {
private fun importQRcode(): Boolean {
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
return true
}
@@ -38,9 +40,9 @@ class ScScannerActivity : BaseActivity() {
val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false)
if (count + countSub > 0) {
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
startActivity(Intent(this, MainActivity::class.java))

View File

@@ -6,6 +6,7 @@ import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
@@ -21,6 +22,7 @@ import io.github.g00fy2.quickie.config.ScannerConfig
class ScannerActivity : BaseActivity() {
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
@@ -37,7 +39,7 @@ class ScannerActivity : BaseActivity() {
finished(text)
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
toast(R.string.toast_decoding_failed)
}
}

View File

@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -14,13 +13,11 @@ import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.DEFAULT_PORT
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.REALITY
import com.v2ray.ang.AppConfig.TLS
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.R
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.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
@@ -328,7 +326,7 @@ class ServerActivity : BaseActivity() {
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
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)
} else if (config.configType == EConfigType.HYSTERIA2) {
@@ -354,11 +352,11 @@ class ServerActivity : BaseActivity() {
container_alpn?.visibility = View.VISIBLE
et_sni?.text = Utils.getEditable(config.sni)
config.fingerPrint?.let {
config.fingerPrint?.let { it ->
val utlsIndex = Utils.arrayFind(uTlsItems, it)
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)
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_reserved1?.text = Utils.getEditable("0,0,0")
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)
return true
}
@@ -480,9 +478,9 @@ class ServerActivity : BaseActivity() {
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty()
}
Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "")
//Log.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
MmkvManager.encodeServerConfig(editGuid, config)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -2,21 +2,22 @@ package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.utils.EditorTheme
import com.blacksquircle.ui.language.json.JsonLanguage
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat
class ServerCustomConfigActivity : BaseActivity() {
private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) }
@@ -77,8 +78,8 @@ class ServerCustomConfigActivity : BaseActivity() {
val profileItem = try {
CustomFmt.parse(binding.editor.text.toString())
} catch (e: Exception) {
e.printStackTrace()
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
Log.e(AppConfig.TAG, "Failed to parse custom configuration", e)
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
return false
}
@@ -91,7 +92,7 @@ class ServerCustomConfigActivity : BaseActivity() {
MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -161,7 +161,7 @@ class SettingsActivity : BaseActivity() {
}
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
delayTestUrl?.summary = if (nval == "") AppConfig.DELAY_TEST_URL else nval
true
}
mode?.setOnPreferenceChangeListener { _, newValue ->
@@ -202,7 +202,7 @@ class SettingsActivity : BaseActivity() {
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL)
initSharedPreference()
}
@@ -238,6 +238,7 @@ class SettingsActivity : BaseActivity() {
AppConfig.PREF_SPEED_ENABLED,
AppConfig.PREF_CONFIRM_REMOVE,
AppConfig.PREF_START_SCAN_IMMEDIATE,
AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
AppConfig.PREF_PREFER_IPV6,
AppConfig.PREF_PROXY_SHARING,
AppConfig.PREF_ALLOW_INSECURE
@@ -363,6 +364,6 @@ class SettingsActivity : BaseActivity() {
}
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.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
@@ -18,8 +19,8 @@ import kotlinx.coroutines.launch
class SubEditActivity : BaseActivity() {
private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null
var save_config: MenuItem? = null
private var del_config: MenuItem? = null
private var save_config: MenuItem? = null
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 {
binding.etRemarks.text = Utils.getEditable(subItem.remarks)
@@ -45,6 +46,7 @@ class SubEditActivity : BaseActivity() {
binding.etFilter.text = Utils.getEditable(subItem.filter)
binding.chkEnable.isChecked = subItem.enabled
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
return true
@@ -76,6 +78,7 @@ class SubEditActivity : BaseActivity() {
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
subItem.prevProfile = binding.etPreProfile.text.toString()
subItem.nextProfile = binding.etNextProfile.text.toString()
subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked
if (TextUtils.isEmpty(subItem.remarks)) {
toast(R.string.sub_setting_remarks)
@@ -89,12 +92,14 @@ class SubEditActivity : BaseActivity() {
if (!Utils.isValidSubUrl(subItem.url)) {
toast(R.string.toast_insecure_url_protocol)
//return false
if (!subItem.allowInsecureUrl) {
return false
}
}
}
MmkvManager.encodeSubscription(editSubId, subItem)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -1,5 +1,6 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.Menu
@@ -10,7 +11,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubSettingBinding
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.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -64,9 +66,9 @@ class SubSettingActivity : BaseActivity() {
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
} else {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
}
@@ -79,6 +81,7 @@ class SubSettingActivity : BaseActivity() {
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
subscriptions = MmkvManager.decodeSubscriptions()
adapter.notifyDataSetChanged()

View File

@@ -3,11 +3,13 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.graphics.Color
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
@@ -53,7 +55,10 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
if (TextUtils.isEmpty(subItem.url)) {
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
} else {
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
AlertDialog.Builder(mActivity)
.setItems(share_method.asList().toTypedArray()) { _, i ->
@@ -78,7 +83,7 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
else -> mActivity.toast("else")
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Share subscription failed", e)
}
}.show()
}

View File

@@ -3,6 +3,7 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -28,7 +29,7 @@ class TaskerActivity : BaseActivity() {
lstData.add("Default")
lstGuid.add(AppConfig.TASKER_DEFAULT_GUID)
MmkvManager.decodeServerList()?.forEach { key ->
MmkvManager.decodeServerList().forEach { key ->
MmkvManager.decodeServerConfig(key)?.let { config ->
lstData.add(config.remarks)
lstGuid.add(key)
@@ -60,7 +61,7 @@ class TaskerActivity : BaseActivity() {
}
}
} 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.util.Log
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.AngConfigManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -44,7 +46,7 @@ class UrlSchemeActivity : BaseActivity() {
}
else -> {
toast(R.string.toast_failure)
toastError(R.string.toast_failure)
}
}
}
@@ -53,7 +55,7 @@ class UrlSchemeActivity : BaseActivity() {
startActivity(Intent(this, MainActivity::class.java))
finish()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Error processing URL scheme", e)
}
}
@@ -61,7 +63,7 @@ class UrlSchemeActivity : BaseActivity() {
if (uriString.isNullOrEmpty()) {
return
}
Log.d("UrlScheme", uriString)
Log.i(AppConfig.TAG, uriString)
var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
val uri = Uri.parse(decodedUrl)
@@ -69,7 +71,7 @@ class UrlSchemeActivity : BaseActivity() {
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
decodedUrl += "#${fragment}"
}
Log.d("UrlScheme-decodedUrl", decodedUrl)
Log.i(AppConfig.TAG, decodedUrl)
lifecycleScope.launch(Dispatchers.IO) {
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
withContext(Dispatchers.Main) {

View File

@@ -21,11 +21,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
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.dto.AssetUrlItem
import com.v2ray.ang.extension.concatUrl
import com.v2ray.ang.extension.toTrafficString
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.SettingsManager
import com.v2ray.ang.util.HttpUtil
@@ -40,7 +43,7 @@ import java.text.DateFormat
import java.util.Date
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 builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
@@ -87,11 +90,16 @@ class UserAssetActivity : BaseActivity() {
binding.recyclerView.layoutManager = LinearLayoutManager(this)
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = UserAssetAdapter()
binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
binding.layoutGeoFilesSources.setOnClickListener {
setGeoFilesSources()
}
}
override fun onResume() {
super.onResume()
binding.recyclerView.adapter?.notifyDataSetChanged()
refreshData()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -108,6 +116,22 @@ class UserAssetActivity : BaseActivity() {
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() {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
@@ -117,7 +141,7 @@ class UserAssetActivity : BaseActivity() {
requestStoragePermissionLauncher.launch(permission)
}
val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data
if (result.resultCode == RESULT_OK && uri != null) {
val assetId = Utils.getUuid()
@@ -135,7 +159,7 @@ class UserAssetActivity : BaseActivity() {
copyFile(uri)
}
}.onFailure {
toast(R.string.toast_asset_copy_failed)
toastError(R.string.toast_asset_copy_failed)
MmkvManager.removeAssetUrl(assetId)
}
}
@@ -146,8 +170,8 @@ class UserAssetActivity : BaseActivity() {
contentResolver.openInputStream(uri).use { inputStream ->
targetFile.outputStream().use { fileOut ->
inputStream?.copyTo(fileOut)
toast(R.string.toast_success)
binding.recyclerView.adapter?.notifyDataSetChanged()
toastSuccess(R.string.toast_success)
refreshData()
}
}
return targetFile.path
@@ -161,7 +185,7 @@ class UserAssetActivity : BaseActivity() {
}.also { cursor.close() }
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to get cursor name", e)
null
}
@@ -188,7 +212,7 @@ class UserAssetActivity : BaseActivity() {
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to import asset from URL", e)
return false
}
return true
@@ -205,17 +229,21 @@ class UserAssetActivity : BaseActivity() {
var resultCount = 0
lifecycleScope.launch(Dispatchers.IO) {
assets.forEach {
var result = downloadGeo(it.second, 15000, httpPort)
if (!result) {
result = downloadGeo(it.second, 15000, 0)
try {
var result = downloadGeo(it.second, 15000, httpPort)
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) {
if (resultCount > 0) {
toast(getString(R.string.title_update_config_count, resultCount))
binding.recyclerView.adapter?.notifyDataSetChanged()
refreshData()
} else {
toast(getString(R.string.toast_failure))
}
@@ -227,7 +255,7 @@ class UserAssetActivity : BaseActivity() {
private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
val targetTemp = File(extDir, item.remarks + "_temp")
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
try {
@@ -242,10 +270,10 @@ class UserAssetActivity : BaseActivity() {
}
return true
} 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
} finally {
conn?.disconnect()
conn.disconnect()
}
}
@@ -257,7 +285,8 @@ class UserAssetActivity : BaseActivity() {
list.add(
Utils.getUuid() to AssetUrlItem(
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) {
SettingsManager.initAssets(this@UserAssetActivity, assets)
withContext(Dispatchers.Main) {
binding.recyclerView.adapter?.notifyDataSetChanged()
refreshData()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun refreshData() {
binding.recyclerView.adapter?.notifyDataSetChanged()
}
inner class UserAssetAdapter : RecyclerView.Adapter<UserAssetViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
return UserAssetViewHolder(
@@ -303,7 +337,7 @@ class UserAssetActivity : BaseActivity() {
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.layoutRemove.visibility = GONE
} else {

View File

@@ -2,13 +2,16 @@ package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils
import java.io.File
@@ -21,10 +24,10 @@ class UserAssetUrlActivity : BaseActivity() {
private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) }
var del_config: MenuItem? = null
var save_config: MenuItem? = null
private var del_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() }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -41,6 +44,7 @@ class UserAssetUrlActivity : BaseActivity() {
binding.etRemarks.setText(assetNameQrcode)
binding.etUrl.setText(assetUrlQrcode)
}
else -> clearAsset()
}
}
@@ -73,7 +77,11 @@ class UserAssetUrlActivity : BaseActivity() {
// remove file associated with the asset
val file = extDir.resolve(assetItem.remarks)
if (file.exists()) {
file.delete()
try {
file.delete()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
}
}
} else {
assetId = Utils.getUuid()
@@ -101,7 +109,7 @@ class UserAssetUrlActivity : BaseActivity() {
}
MmkvManager.encodeAsset(assetId, assetItem)
toast(R.string.toast_success)
toastSuccess(R.string.toast_success)
finish()
return true
}

View File

@@ -25,7 +25,7 @@ object AppManagerUtil {
val appName = applicationInfo.loadLabel(packageManager).toString()
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)
apps.add(appInfo)
@@ -33,4 +33,8 @@ object AppManagerUtil {
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
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode
import com.v2ray.ang.util.Utils.urlDecode
import java.io.IOException
import java.net.*
import java.util.*
import java.net.HttpURLConnection
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 {
@@ -18,9 +25,54 @@ object HttpUtil {
*/
fun idnToASCII(str: String): String {
val url = URL(str)
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file).toExternalForm()
val host = url.host
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
if (host != asciiHost) {
return str.replace(host, asciiHost)
} else {
return str
}
}
/**
* 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.
*
@@ -136,7 +188,7 @@ object HttpUtil {
)
}
} 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
conn?.disconnect()
return null

View File

@@ -1,5 +1,6 @@
package com.v2ray.ang.util
import android.util.Log
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
@@ -8,6 +9,7 @@ import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import java.lang.reflect.Type
object JsonUtil {
@@ -70,7 +72,7 @@ object JsonUtil {
try {
return JsonParser.parseString(src).getAsJsonObject()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to parse JSON string", e)
return null
}
}

View File

@@ -3,12 +3,14 @@ package com.v2ray.ang.util
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.service.V2RayTestService
import java.io.Serializable
object MessageUtil {
/**
* Sends a message to the service.
*
@@ -46,7 +48,7 @@ object MessageUtil {
intent.putExtra("content", content)
ctx.startService(intent)
} 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)
ctx.sendBroadcast(intent)
} 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.os.SystemClock
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.ProfileItem
import com.v2ray.ang.fmt.Hysteria2Fmt
@@ -13,7 +13,7 @@ import java.io.File
object PluginUtil {
private const val HYSTERIA2 = "libhysteria2.so"
private const val TAG = ANG_PACKAGE
private val procService: ProcessService by lazy {
ProcessService()
}
@@ -23,16 +23,26 @@ object PluginUtil {
*
* @param context The context to use.
* @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?) {
Log.d(TAG, "runPlugin")
fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
Log.i(AppConfig.TAG, "Starting plugin execution")
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
val configFile = genConfigHy2(context, config, domainPort) ?: return
val cmd = genCmdHy2(context, configFile)
if (config == null || socksPort == null) {
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
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.
*/
fun realPingHy2(context: Context, config: ProfileItem?): Long {
Log.d(TAG, "realPingHy2")
Log.i(AppConfig.TAG, "realPingHy2")
val retFailure = -1L
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
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 proc = ProcessService()
@@ -75,22 +85,20 @@ object PluginUtil {
*
* @param context The context to use.
* @param config The profile configuration.
* @param domainPort The domain and port information.
* @param socksPort The port information.
* @return The generated configuration file.
*/
private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
Log.d(TAG, "runPlugin $HYSTERIA2")
private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? {
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 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.writeText(JsonUtil.toJson(hy2Config))
Log.d(TAG, JsonUtil.toJson(hy2Config))
Log.i(AppConfig.TAG, JsonUtil.toJson(hy2Config))
return configFile
}
@@ -119,10 +127,10 @@ object PluginUtil {
*/
private fun stopHy2() {
try {
Log.d(TAG, "$HYSTERIA2 destroy")
Log.i(AppConfig.TAG, "$HYSTERIA2 destroy")
procService?.stopProcess()
} 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.net.toUri
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import java.io.IOException
import java.net.InetAddress
import java.net.ServerSocket
import java.net.URI
import java.net.URLDecoder
import java.net.URLEncoder
import java.util.Locale
@@ -28,6 +30,10 @@ import java.util.UUID
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.
*
@@ -46,22 +52,7 @@ object Utils {
* @return The index of the value in the array, or -1 if not found.
*/
fun arrayFind(array: Array<out String>, value: String): Int {
for (i in array.indices) {
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)
return array.indexOf(value)
}
/**
@@ -71,7 +62,7 @@ object Utils {
* @param default 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
}
@@ -86,7 +77,7 @@ object Utils {
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cmb.primaryClip?.getItemAt(0)?.text.toString()
} 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)
cmb.setPrimaryClip(clipData)
} 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.
*/
fun tryDecodeBase64(text: String?): String? {
if (text.isNullOrEmpty()) return null
try {
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
} catch (e: Exception) {
Log.i(ANG_PACKAGE, "Parse base64 standard failed $e")
Log.e(AppConfig.TAG, "Failed to decode standard base64", e)
}
try {
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
} 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
}
@@ -147,7 +140,7 @@ object Utils {
return try {
Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
} 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.
*/
fun isIpAddress(value: String?): Boolean {
if (value.isNullOrEmpty()) return false
try {
if (value.isNullOrEmpty()) {
return false
}
var addr = value
if (addr.isEmpty() || addr.isBlank()) {
return false
}
var addr = value.trim()
if (addr.isEmpty()) return false
//CIDR
if (addr.indexOf("/") > 0) {
if (addr.contains("/")) {
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]
}
}
// "::ffff:192.168.173.22"
// "[::ffff:192.168.173.22]:80"
// Handle IPv4-mapped IPv6 addresses
if (addr.startsWith("::ffff:") && '.' in addr) {
addr = addr.drop(7)
} else if (addr.startsWith("[::ffff:") && '.' in addr) {
addr = addr.drop(8).replace("]", "")
}
// addr = addr.toLowerCase()
val octets = addr.split('.').toTypedArray()
val octets = addr.split('.')
if (octets.size == 4) {
if (octets[3].indexOf(":") > 0) {
if (octets[3].contains(":")) {
addr = addr.substring(0, addr.indexOf(":"))
}
return isIpv4Address(addr)
}
// Ipv6addr [2001:abc::123]:8080
return isIpv6Address(addr)
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to validate IP address", e)
return false
}
}
@@ -217,9 +205,7 @@ object Utils {
* @return True if the string is a valid IPv4 address, false otherwise.
*/
private fun isIpv4Address(value: String): Boolean {
val regV4 =
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)
return IPV4_REGEX.matches(value)
}
/**
@@ -230,13 +216,10 @@ object Utils {
*/
private fun isIpv6Address(value: String): Boolean {
var addr = value
if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) {
addr = addr.drop(1)
addr = addr.dropLast(addr.count() - addr.lastIndexOf("]"))
if (addr.startsWith("[") && addr.endsWith("]")) {
addr = addr.drop(1).dropLast(1)
}
val regV6 =
Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$")
return regV6.matches(addr)
return IPV6_REGEX.matches(addr)
}
/**
@@ -246,10 +229,10 @@ object Utils {
* @return True if the string is a CoreDNS address, false otherwise.
*/
fun isCoreDNSAddress(s: String): Boolean {
return s.startsWith("https")
|| s.startsWith("tcp")
|| s.startsWith("quic")
|| s == "localhost"
return s.startsWith("https") ||
s.startsWith("tcp") ||
s.startsWith("quic") ||
s == "localhost"
}
/**
@@ -259,21 +242,16 @@ object Utils {
* @return True if the string is a valid URL, false otherwise.
*/
fun isValidUrl(value: String?): Boolean {
try {
if (value.isNullOrEmpty()) {
return false
}
if (Patterns.WEB_URL.matcher(value).matches()
|| Patterns.DOMAIN_NAME.matcher(value).matches()
|| URLUtil.isValidUrl(value)
) {
return true
}
if (value.isNullOrEmpty()) return false
return try {
Patterns.WEB_URL.matcher(value).matches() ||
Patterns.DOMAIN_NAME.matcher(value).matches() ||
URLUtil.isValidUrl(value)
} catch (e: Exception) {
e.printStackTrace()
return false
Log.e(AppConfig.TAG, "Failed to validate URL", e)
false
}
return false
}
/**
@@ -283,8 +261,12 @@ object Utils {
* @param uriString The URI string to open.
*/
fun openUri(context: Context, uriString: String) {
val uri = uriString.toUri()
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
try {
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 {
UUID.randomUUID().toString().replace("-", "")
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to generate UUID", e)
""
}
}
@@ -311,7 +293,7 @@ object Utils {
return try {
URLDecoder.decode(url, Charsets.UTF_8.toString())
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to decode URL", e)
url
}
}
@@ -326,7 +308,7 @@ object Utils {
return try {
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to encode URL", e)
url
}
}
@@ -339,13 +321,18 @@ object Utils {
* @return The content of the asset file as a string.
*/
fun readTextFromAssets(context: Context?, fileName: String): String {
if (context == null) {
return ""
if (context == null) 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.
*/
fun userAssetPath(context: Context?): String {
if (context == null)
return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS)
?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
return extDir.absolutePath
if (context == null) return ""
return try {
context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get user asset path", e)
""
}
}
/**
@@ -369,11 +360,15 @@ object Utils {
* @return The path to the backup directory.
*/
fun backupPath(context: Context?): String {
if (context == null)
return ""
val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS)
?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
return extDir.absolutePath
if (context == null) return ""
return try {
context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath
?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get backup path", e)
""
}
}
/**
@@ -382,8 +377,13 @@ object Utils {
* @return The device ID for XUDP base key.
*/
fun getDeviceIdForXUDPBaseKey(): String {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
return try {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to generate device ID", e)
""
}
}
/**
@@ -403,11 +403,10 @@ object Utils {
* @return The formatted IPv6 address, or the original address if not valid.
*/
fun getIpv6Address(address: String?): String {
if (address == null) {
return ""
}
if (address.isNullOrEmpty()) return ""
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
String.format("[%s]", address)
"[$address]"
} else {
address
}
@@ -431,8 +430,7 @@ object Utils {
* @return The URL string with illegal characters replaced.
*/
fun fixIllegalUrl(str: String): String {
return str
.replace(" ", "%20")
return str.replace(" ", "%20")
.replace("|", "%7C")
}
@@ -463,12 +461,23 @@ object Utils {
* @return True if the string is a valid subscription URL, false otherwise.
*/
fun isValidSubUrl(value: String?): Boolean {
if (value.isNullOrEmpty()) return false
try {
if (value.isNullOrEmpty()) return false
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) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
}
return false
}
@@ -489,7 +498,58 @@ object Utils {
*
* @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
import android.util.Log
import com.v2ray.ang.AppConfig
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
@@ -61,7 +63,7 @@ object ZipUtil {
zos.closeEntry()
zos.close()
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to zip folder", e)
return false
}
return true
@@ -97,7 +99,7 @@ object ZipUtil {
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(AppConfig.TAG, "Failed to unzip file", e)
return false
}
return true

View File

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

View File

@@ -26,7 +26,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
override fun onCleared() {
PreferenceManager.getDefaultSharedPreferences(getApplication())
.unregisterOnSharedPreferenceChangeListener(this)
Log.i(AppConfig.ANG_PACKAGE, "Settings ViewModel is cleared")
Log.i(AppConfig.TAG, "Settings ViewModel is cleared")
super.onCleared()
}
@@ -36,7 +36,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
* @param key The key of the changed preference.
*/
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) {
AppConfig.PREF_MODE,
AppConfig.PREF_VPN_DNS,
@@ -73,6 +73,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_BYPASS_APPS,
AppConfig.PREF_CONFIRM_REMOVE,
AppConfig.PREF_START_SCAN_IMMEDIATE,
AppConfig.PREF_DOUBLE_COLUMN_DISPLAY,
AppConfig.SUBSCRIPTION_AUTO_UPDATE,
AppConfig.PREF_FRAGMENT_ENABLED,
AppConfig.PREF_MUX_ENABLED,

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:viewportHeight="1024">
<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>

View File

@@ -4,12 +4,12 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<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
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
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>

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="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp"
android:tint="#FFFFFF"
android:viewportHeight="24"
android:viewportWidth="24"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
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
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" />

View File

@@ -4,9 +4,9 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<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
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>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp">
android:viewportHeight="24.0">
<path
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" />

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

@@ -0,0 +1,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/divider_color_light" />
<size
android:width="48dp"
android:height="48dp" />
</shape>

View File

@@ -4,6 +4,6 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<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>

View File

@@ -4,12 +4,12 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<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
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
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>

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="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>

View File

@@ -1,9 +1,9 @@
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
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
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" />

View File

@@ -4,9 +4,9 @@
android:viewportWidth="1024"
android:viewportHeight="1024">
<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
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>

View File

@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="1024"
android:viewportWidth="1024">
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
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" />

View File

@@ -1,8 +1,8 @@
<vector android:height="24dp"
android:viewportHeight="1024"
android:viewportWidth="1024"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
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" />

View File

@@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
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:viewportHeight="960">
<path
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" />
</vector>

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