Compare commits

...

46 Commits

Author SHA1 Message Date
2dust 30792299e8 up 2.1.0 2026-04-17 15:16:53 +08:00
solokot e6db0f6f9e Update Russian translation (#5486) 2026-04-17 15:14:36 +08:00
2dust 04478ab490 Support custom outbound tags in routing
Allow routing rules to reference outbounds by server remarks and inject missing outbounds into the generated V2ray config. Adds BUILTIN_OUTBOUND_TAGS, a pre-pass (injectCustomOutbounds) that converts referenced non-builtin tags to outbounds, and a fallback to the proxy tag when an outbound couldn't be injected. Updates getBalance to use AppConfig.TAG_PROXY constant. Replaces the outbound-tag Spinner with an AutoCompleteTextView + dropdown button that suggests builtin tags and existing profile remarks, adds a drop-down drawable and a hint string resource.
2026-04-17 15:10:19 +08:00
2dust f371da490a Kotlin formatting, import cleanup, and minor fixes 2026-04-17 14:00:38 +08:00
2dust 0a0ef6e6b5 Guard process finder init on Android Q+ 2026-04-16 13:52:35 +08:00
2dust fd57019e73 Add conditional process routing and routing strings 2026-04-16 09:56:35 +08:00
2dust 04ac6cb2d7 Update AndroidLibXrayLite 2026-04-16 09:21:54 +08:00
2dust 29a0be9629 Revert "Make tun MTU a list and port nullable"
This reverts commit f19f153f9c.
2026-04-16 09:21:15 +08:00
2dust 7aa27fd3de Add IPv6 enable preference and strings
https://github.com/2dust/v2rayNG/issues/5454
2026-04-15 19:33:55 +08:00
2dust fcc478ff63 Add launcher shortcuts to start/stop service
https://github.com/2dust/v2rayNG/issues/5485
2026-04-15 19:04:26 +08:00
2dust 54d5ad2342 Replace android.util.Log with LogUtil
Introduce a centralized LogUtil and migrate logging calls across the app to use it. Replaced android.util.Log imports/usages in many modules (fmt, handler, helper, receiver, service, ui, util, viewmodel, tests) and added a new LogUtil.kt file. Also trigger LogUtil.refreshLogLevel() when PREF_LOGLEVEL changes in MmkvPreferenceDataStore so runtime log level updates take effect.
2026-04-15 17:20:17 +08:00
2dust ba25e05536 Add process-based routing support
Introduce process field for routing rules and wire it through UI, config parsing, and runtime resolution. Adds a PackageUidResolver utility to map package names to UIDs (with caching) and converts user-entered package names into UIDs when building v2ray routing rules. Updates V2rayConfig types, RoutingEditActivity layout/bindings/strings to accept process input, and adjusts getRouting to receive Context. Also adds extra logging and a defensive check in V2RayServiceManager when resolving connection owner UID.
2026-04-15 15:46:06 +08:00
2dust 5a5a81fa0b Update hev-socks5-tunnel 2026-04-15 10:51:09 +08:00
2dust 322c4b305e Update AndroidLibXrayLite 2026-04-15 10:51:06 +08:00
2dust 590f11e671 Add Xray ProcessFinder and register it
https://github.com/2dust/AndroidLibXrayLite/commit/18c5fef49892be1c5755dd4e6ec672399521809a
https://github.com/2dust/AndroidLibXrayLite/commit/4371ac28643114042cf2499a61d381bf3d6bee75
2026-04-14 20:55:27 +08:00
2dust 872ec80292 Bug fix
https://github.com/2dust/v2rayNG/commit/d7abb8f49e3e9292c09a6fb9d2ca2f03530a7810
2026-04-14 20:49:47 +08:00
2dust f19f153f9c Make tun MTU a list and port nullable
https://github.com/XTLS/Xray-core/pull/5887
2026-04-14 14:57:12 +08:00
2dust d7abb8f49e Add dynamic local SOCKS port option
Introduce a dynamic SOCKS port feature: add PREF_DYNAMIC_SOCKS_PORT key, settings UI checkbox and localized strings, and pref XML entry. SettingsManager now holds a runtimeSocksPort, can generate and refresh a random port (generateRandomSocksPort, refreshRuntimeSocksPort) and respects the dynamic setting when returning getSocksPort(). V2Ray service startup triggers a runtime port refresh. Settings UI disables manual SOCKS port input when dynamic mode is enabled. Minor i18n additions (QR code/title) included for several locales.
2026-04-14 14:42:31 +08:00
2dust e54e713db7 Add local SOCKS username/password support 2026-04-13 20:17:30 +08:00
Copilot de91fb27db Fix VPN icon persisting in status bar after stopping service (#5476)
* Initial plan

* Fix VPN icon remaining in status bar after stopping service

Agent-Logs-Url: https://github.com/2dust/v2rayNG/sessions/4377b941-8f02-4d92-abc6-0f98fd6c9b60

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-04-13 19:35:55 +08:00
dependabot[bot] db8f46eae5 Bump android-actions/setup-android from 4.0.0 to 4.0.1 (#5456)
Bumps [android-actions/setup-android](https://github.com/android-actions/setup-android) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/android-actions/setup-android/releases)
- [Commits](https://github.com/android-actions/setup-android/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: android-actions/setup-android
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 19:27:05 +08:00
2dust 42e7f95e4f up 2.0.18 2026-04-03 09:19:22 +08:00
DHR60 0a52a79fa0 Support interval range (#5440) 2026-04-02 09:33:07 +08:00
DHR60 9dc1b1f2cf Support new fragment (#5437)
* Support new fragment

* Fix
2026-04-02 09:30:31 +08:00
DHR60 b61cad5a5f Support new hysteria2 stream settings (#5435)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 20:59:32 +08:00
2dust 66cd746ad2 Add server_lab_final_mask to translations 2026-04-01 10:18:59 +08:00
owo ae4a92b66b add finalmask into share link and support import manually (#5429)
* add finalmask into share link and support import manually

* remove temp test code

* Update V2rayNG/app/src/main/res/values/strings.xml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Make finalmask Any and parse as raw string

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 10:12:50 +08:00
solokot 55c0ccf1b0 Update Russian translation (#5430) 2026-03-31 19:32:45 +08:00
2dust deedd26fac Respect append flag in custom config parsing
https://github.com/2dust/v2rayNG/pull/5424
2026-03-29 19:33:09 +08:00
owo e6c8307b96 remove old servers when update subscription (#5424)
* remove old servers when update subscription

* Supplementary Submission: Multiple Configuration Scenarios
2026-03-29 17:47:37 +08:00
2dust d6d5753283 Move locate action from FAB to menu
https://github.com/2dust/v2rayNG/issues/5423
2026-03-29 17:32:59 +08:00
2dust 014d510774 Disable SwipeRefreshLayout in GroupServerFragment
https://github.com/2dust/v2rayNG/issues/5417
2026-03-29 17:23:23 +08:00
2dust 42f89e69a7 up 2.0.17 2026-03-28 11:32:09 +08:00
2dust d5a0997775 Update AndroidLibXrayLite 2026-03-28 11:29:51 +08:00
owo af7db6f86b fix fab shadow issue (#5413) (#5416) 2026-03-27 09:22:47 +08:00
dependabot[bot] 7b7f43e12e Bump android-actions/setup-android from 3.2.0 to 4.0.0 (#5411)
Bumps [android-actions/setup-android](https://github.com/android-actions/setup-android) from 3.2.0 to 4.0.0.
- [Release notes](https://github.com/android-actions/setup-android/releases)
- [Commits](https://github.com/android-actions/setup-android/compare/v3.2.0...v4.0.0)

---
updated-dependencies:
- dependency-name: android-actions/setup-android
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 09:22:38 +08:00
2dust 8dd1df1451 up 2.0.16 2026-03-23 20:52:45 +08:00
2dust d4a73493b5 Update AndroidLibXrayLite 2026-03-23 20:51:04 +08:00
2dust 9fce03d0ed Increase swipe refresh trigger distance to 160dp
https://github.com/2dust/v2rayNG/issues/5367
2026-03-21 14:38:34 +08:00
owo a12ca3ea04 Feature/regex and protocol search (#5364)
* add windows compile options

* add regex search and search from protocol function(#5354)

* Delete compile-hevtun.bat

* Delete fix_headers.bat

* Extract matchesPattern to extension

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-03-21 14:22:10 +08:00
solokot 4e9df84d3e Update Russian translation (#5374) 2026-03-21 13:56:58 +08:00
2dust d3118b53f2 up 2.0.15 2026-03-13 17:08:02 +08:00
2dust cd45b51487 Add locate FAB and scroll-to-selected server
https://github.com/2dust/v2rayNG/issues/5165
2026-03-13 15:02:08 +08:00
2dust 6aa26d2621 Fix
https://github.com/2dust/v2rayNG/issues/5361
2026-03-13 10:49:30 +08:00
2dust 0b452707c3 Pull down to update subscription 2026-03-12 19:23:52 +08:00
2dust 303b642ade Refactor subscription import & matching logic 2026-03-12 18:03:21 +08:00
101 changed files with 1640 additions and 507 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
fetch-depth: '0' fetch-depth: '0'
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3.2.0 uses: android-actions/setup-android@v4.0.1
with: with:
log-accepted-android-sdk-licenses: false log-accepted-android-sdk-licenses: false
cmdline-tools-version: '13114758' cmdline-tools-version: '13114758'
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 714 versionCode = 720
versionName = "2.0.14" versionName = "2.1.0"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -30,4 +30,34 @@
android:targetPackage="com.v2ray.ang.fdroid" /> android:targetPackage="com.v2ray.ang.fdroid" />
<categories android:name="android.shortcut.conversation" /> <categories android:name="android.shortcut.conversation" />
</shortcut> </shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/ic_qu_start_24dp"
android:shortcutDisabledMessage="@string/toast_services_start"
android:shortcutId="shortcuts_start"
android:shortcutLongLabel="@string/toast_services_start"
android:shortcutShortLabel="@string/toast_services_start">
<!--suppress AndroidDomInspection -->
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.v2ray.ang.ui.ScStartActivity"
android:targetPackage="com.v2ray.ang.fdroid" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/ic_qu_stop_24dp"
android:shortcutDisabledMessage="@string/toast_services_stop"
android:shortcutId="shortcuts_stop"
android:shortcutLongLabel="@string/toast_services_stop"
android:shortcutShortLabel="@string/toast_services_stop">
<!--suppress AndroidDomInspection -->
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.v2ray.ang.ui.ScStopActivity"
android:targetPackage="com.v2ray.ang.fdroid" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts> </shortcuts>
+12
View File
@@ -122,6 +122,18 @@
android:exported="false" android:exported="false"
android:process=":RunSoLibV2RayDaemon" android:process=":RunSoLibV2RayDaemon"
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" /> android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
<activity
android:name=".ui.ScStartActivity"
android:excludeFromRecents="true"
android:exported="false"
android:process=":RunSoLibV2RayDaemon"
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
<activity
android:name=".ui.ScStopActivity"
android:excludeFromRecents="true"
android:exported="false"
android:process=":RunSoLibV2RayDaemon"
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
<activity <activity
android:name=".ui.UrlSchemeActivity" android:name=".ui.UrlSchemeActivity"
@@ -36,7 +36,6 @@
}, },
{ {
"tag": "tun", "tag": "tun",
"port": 0,
"protocol": "tun", "protocol": "tun",
"settings": { "settings": {
"name": "xray0", "name": "xray0",
@@ -10,8 +10,8 @@ object AppConfig {
/** Directory names used in the app's file system. */ /** Directory names used in the app's file system. */
const val DIR_ASSETS = "assets" const val DIR_ASSETS = "assets"
const val WEBDAV_BACKUP_DIR = "backups" const val WEBDAV_BACKUP_DIR = "backups"
const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip" const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip"
/** Legacy configuration keys. */ /** Legacy configuration keys. */
const val ANG_CONFIG = "ang_config" const val ANG_CONFIG = "ang_config"
@@ -54,10 +54,14 @@ object AppConfig {
const val PREF_GROUP_ALL_DISPLAY = "pref_group_all_display" const val PREF_GROUP_ALL_DISPLAY = "pref_group_all_display"
const val PREF_LANGUAGE = "pref_language" const val PREF_LANGUAGE = "pref_language"
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night" const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
const val PREF_IPV6_ENABLED = "pref_ipv6_enabled"
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6" const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled" const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_ALLOW_INSECURE = "pref_allow_insecure" const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
const val PREF_SOCKS_PORT = "pref_socks_port" const val PREF_SOCKS_PORT = "pref_socks_port"
const val PREF_DYNAMIC_SOCKS_PORT = "pref_dynamic_socks_port"
const val PREF_SOCKS_USERNAME = "pref_socks_username"
const val PREF_SOCKS_PASSWORD = "pref_socks_password"
const val PREF_REMOTE_DNS = "pref_remote_dns" const val PREF_REMOTE_DNS = "pref_remote_dns"
const val PREF_DOMESTIC_DNS = "pref_domestic_dns" const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
const val PREF_DNS_HOSTS = "pref_dns_hosts" const val PREF_DNS_HOSTS = "pref_dns_hosts"
@@ -120,7 +124,8 @@ object AppConfig {
const val TG_CHANNEL_URL = "https://t.me/github_2dust" 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_URL = "https://www.gstatic.com/generate_204"
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204" const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
const val IP_API_URL = "https://api.ip.sb/geoip" const val IP_API_URL = "https://api.ip.sb/geoip"
/** DNS server addresses. */ /** DNS server addresses. */
@@ -274,4 +279,9 @@ object AppConfig {
"Chocolate4U/Iran-v2ray-rules" "Chocolate4U/Iran-v2ray-rules"
) )
val BUILTIN_OUTBOUND_TAGS = setOf(
TAG_PROXY,
TAG_DIRECT,
TAG_BLOCKED,
)
} }
@@ -2,7 +2,7 @@ package com.v2ray.ang.contracts
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
interface MainAdapterListener :BaseAdapterListener { interface MainAdapterListener : BaseAdapterListener {
fun onEdit(guid: String, position: Int, profile: ProfileItem) fun onEdit(guid: String, position: Int, profile: ProfileItem)
@@ -11,7 +11,7 @@ interface Tun2SocksControl {
* This initializes the VPN tunnel and connects it to the SOCKS proxy. * This initializes the VPN tunnel and connects it to the SOCKS proxy.
*/ */
fun startTun2Socks() fun startTun2Socks()
/** /**
* Stops the tun2socks process and cleans up resources. * Stops the tun2socks process and cleans up resources.
*/ */
@@ -36,7 +36,7 @@ data class ProfileItem(
var authority: String? = null, var authority: String? = null,
var xhttpMode: String? = null, var xhttpMode: String? = null,
var xhttpExtra: String? = null, var xhttpExtra: String? = null,
var finalMask: String? = null,
var security: String? = null, var security: String? = null,
var sni: String? = null, var sni: String? = null,
var alpn: String? = null, var alpn: String? = null,
@@ -4,6 +4,7 @@ data class RulesetItem(
var remarks: String? = "", var remarks: String? = "",
var ip: List<String>? = null, var ip: List<String>? = null,
var domain: List<String>? = null, var domain: List<String>? = null,
var process: List<String>? = null,
var outboundTag: String = "", var outboundTag: String = "",
var port: String? = null, var port: String? = null,
var network: String? = null, var network: String? = null,
@@ -31,7 +31,7 @@ data class V2rayConfig(
data class InboundBean( data class InboundBean(
var tag: String, var tag: String,
var port: Int, var port: Int?,
var protocol: String, var protocol: String,
var listen: String? = null, var listen: String? = null,
var settings: InSettingsBean? = null, var settings: InSettingsBean? = null,
@@ -44,10 +44,16 @@ data class V2rayConfig(
var auth: String? = null, var auth: String? = null,
var udp: Boolean? = null, var udp: Boolean? = null,
var userLevel: Int? = null, var userLevel: Int? = null,
var accounts: List<SocksAccountBean>? = null,
var name: String? = null, var name: String? = null,
@SerializedName("MTU")
var mtu: Int? = null var mtu: Int? = null
) ) {
data class SocksAccountBean(
var user: String = "",
var pass: String = ""
)
}
data class SniffingBean( data class SniffingBean(
var enabled: Boolean, var enabled: Boolean,
@@ -68,8 +74,6 @@ data class V2rayConfig(
) { ) {
data class OutSettingsBean( data class OutSettingsBean(
var vnext: List<VnextBean>? = null, var vnext: List<VnextBean>? = null,
var fragment: FragmentBean? = null,
var noises: List<NoiseBean>? = null,
var servers: List<ServersBean>? = null, var servers: List<ServersBean>? = null,
/*Blackhole*/ /*Blackhole*/
var response: Response? = null, var response: Response? = null,
@@ -108,18 +112,6 @@ data class V2rayConfig(
) )
} }
data class FragmentBean(
var packets: String? = null,
var length: String? = null,
var interval: String? = null
)
data class NoiseBean(
var type: String? = null,
var packet: String? = null,
var delay: String? = null
)
data class ServersBean( data class ServersBean(
var address: String = "", var address: String = "",
var method: String? = null, var method: String? = null,
@@ -162,7 +154,7 @@ data class V2rayConfig(
var realitySettings: TlsSettingsBean? = null, var realitySettings: TlsSettingsBean? = null,
var grpcSettings: GrpcSettingsBean? = null, var grpcSettings: GrpcSettingsBean? = null,
var hysteriaSettings: HysteriaSettingsBean? = null, var hysteriaSettings: HysteriaSettingsBean? = null,
var finalmask: FinalMaskBean? = null, var finalmask: Any? = null,
val dsSettings: Any? = null, val dsSettings: Any? = null,
var sockopt: SockoptBean? = null var sockopt: SockoptBean? = null
) { ) {
@@ -292,28 +284,51 @@ data class V2rayConfig(
data class HysteriaSettingsBean( data class HysteriaSettingsBean(
var version: Int, var version: Int,
var auth: String? = null, var auth: String? = null
var up: String? = null, )
var down: String? = null,
var udphop: HysteriaUdpHopBean? = null
) {
data class HysteriaUdpHopBean(
var port: String? = null,
var interval: Int? = null
)
}
//https://xtls.github.io/config/transport.html#finalmaskobject
data class FinalMaskBean( data class FinalMaskBean(
var tcp: List<MaskBean>? = null, var tcp: List<MaskBean>? = null,
var udp: List<MaskBean>? = null var udp: List<MaskBean>? = null,
var quicParams: QuicParamsBean? = null
) { ) {
data class MaskBean( data class MaskBean(
var type: String, var type: String,
var settings: MaskSettingsBean? = null var settings: MaskSettingsBean? = null
) { ) {
data class MaskSettingsBean( data class MaskSettingsBean(
var password: String? = null, val password: String? = null,
var domain: String? = null val domain: String? = null,
// fragment
val packets: String? = null,
val length: String? = null,
val delay: String? = null,
// val maxSplit: String? = null,
// noise
val reset: Int? = null,
val noise: List<NoiseMaskBean>? = null
) {
data class NoiseMaskBean(
val rand: String? = null,
// val randRange: String? = null,
// val type: String? = null,
// val packet: String? = null,
val delay: String? = null,
)
}
}
data class QuicParamsBean(
var congestion: String? = null,
var brutalUp: String? = null,
var brutalDown: String? = null,
var udpHop: UdpHopBean? = null,
) {
// Nested data class for the udpHop JSON object
data class UdpHopBean(
var ports: String? = null,
var interval: String? = null
) )
} }
} }
@@ -409,8 +424,9 @@ data class V2rayConfig(
data class RulesBean( data class RulesBean(
var type: String = "field", var type: String = "field",
var ip: ArrayList<String>? = null, var ip: List<String>? = null,
var domain: ArrayList<String>? = null, var domain: List<String>? = null,
var process: List<String>? = null,
var outboundTag: String? = null, var outboundTag: String? = null,
var balancerTag: String? = null, var balancerTag: String? = null,
var port: String? = null, var port: String? = null,
@@ -15,7 +15,7 @@ enum class EConfigType(val value: Int, val protocolScheme: String) {
HYSTERIA2(9, AppConfig.HYSTERIA2), HYSTERIA2(9, AppConfig.HYSTERIA2),
HYSTERIA(900, AppConfig.HYSTERIA), HYSTERIA(900, AppConfig.HYSTERIA),
HTTP(10, AppConfig.HTTP), HTTP(10, AppConfig.HTTP),
POLICYGROUP (101, AppConfig.CUSTOM); POLICYGROUP(101, AppConfig.CUSTOM);
companion object { companion object {
fun fromInt(value: Int) = entries.firstOrNull { it.value == value } fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
@@ -189,4 +189,15 @@ fun String.concatUrl(vararg paths: String): String {
} }
return builder.toString() return builder.toString()
}
/**
* Helper function to match text either by Regex or literal string.
*/
fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean = true): Boolean {
if (keyword.isNullOrEmpty()) {
return true
}
return regex?.containsMatchIn(this)
?: this.contains(keyword, ignoreCase = ignoreCase)
} }
@@ -12,7 +12,7 @@ object CustomFmt : FmtBase() {
* @param str the JSON string to parse * @param str the JSON string to parse
* @return the parsed ProfileItem object, or null if parsing fails * @return the parsed ProfileItem object, or null if parsing fails
*/ */
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem {
val config = ProfileItem.create(EConfigType.CUSTOM) val config = ProfileItem.create(EConfigType.CUSTOM)
val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java) val fullConfig = JsonUtil.fromJson(str, V2rayConfig::class.java)
@@ -67,6 +67,7 @@ open class FmtBase {
config.authority = queryParam["authority"] config.authority = queryParam["authority"]
config.xhttpMode = queryParam["mode"] config.xhttpMode = queryParam["mode"]
config.xhttpExtra = queryParam["extra"] config.xhttpExtra = queryParam["extra"]
config.finalMask = queryParam["fm"]
config.security = queryParam["security"] config.security = queryParam["security"]
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) { if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
@@ -110,6 +111,7 @@ open class FmtBase {
config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it } config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it }
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it } config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it } config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
config.finalMask?.nullIfBlank()?.let { dicQuery["fm"] = it }
// Add two keys for compatibility: "insecure" and "allowInsecure" // Add two keys for compatibility: "insecure" and "allowInsecure"
if (config.security == AppConfig.TLS) { if (config.security == AppConfig.TLS) {
val insecureFlag = if (config.insecure == true) "1" else "0" val insecureFlag = if (config.insecure == true) "1" else "0"
@@ -3,7 +3,6 @@ package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean.FinalMaskBean
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
@@ -21,7 +20,7 @@ object Hysteria2Fmt : FmtBase() {
* @param str the Hysteria2 URI string to parse * @param str the Hysteria2 URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails * @return the parsed ProfileItem object, or null if parsing fails
*/ */
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.HYSTERIA2) val config = ProfileItem.create(EConfigType.HYSTERIA2)
@@ -73,7 +72,37 @@ object Hysteria2Fmt : FmtBase() {
dicQuery["mport"] = config.portHopping.orEmpty() dicQuery["mport"] = config.portHopping.orEmpty()
} }
if (config.portHoppingInterval.isNotNullEmpty()) { if (config.portHoppingInterval.isNotNullEmpty()) {
dicQuery["mportHopInt"] = config.portHoppingInterval.orEmpty() val rawInterval = config.portHoppingInterval?.trim().nullIfBlank()
val interval = if (rawInterval == null) {
null
} else {
val singleValue = rawInterval.toIntOrNull()
if (singleValue != null) {
if (singleValue < 5) {
null
} else {
rawInterval
}
} else {
val parts = rawInterval.split('-')
if (parts.size == 2) {
val start = parts[0].trim().toIntOrNull()
val end = parts[1].trim().toIntOrNull()
if (start != null && end != null) {
val minStart = maxOf(5, start)
val minEnd = maxOf(minStart, end)
(minStart + minEnd) / 2
} else {
null
}
} else {
null
}
}
}
if (interval != null) {
dicQuery["mportHopInt"] = interval.toString()
}
} }
if (config.pinnedCA256.isNotNullEmpty()) { if (config.pinnedCA256.isNotNullEmpty()) {
dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty() dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
@@ -107,18 +136,6 @@ object Hysteria2Fmt : FmtBase() {
V2rayConfigManager.populateTlsSettings(it, profileItem, sni) V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
} }
if (profileItem.obfsPassword.isNotNullEmpty()) {
outboundBean.streamSettings?.finalmask = FinalMaskBean(
udp = listOf(
FinalMaskBean.MaskBean(
type = "salamander",
settings = FinalMaskBean.MaskBean.MaskSettingsBean(
password = profileItem.obfsPassword
)
)
)
)
}
return outboundBean return outboundBean
} }
} }
@@ -1,6 +1,5 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
@@ -8,6 +7,7 @@ import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -85,7 +85,7 @@ object ShadowsocksFmt : FmtBase() {
config.remarks = config.remarks =
Utils.decodeURIComponent(result.substring(indexSplit + 1, result.length)) Utils.decodeURIComponent(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e) LogUtil.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
} }
result = result.substring(0, indexSplit) result = result.substring(0, indexSplit)
@@ -18,7 +18,7 @@ object TrojanFmt : FmtBase() {
* @param str the Trojan URI string to parse * @param str the Trojan URI string to parse
* @return the parsed ProfileItem object, or null if parsing fails * @return the parsed ProfileItem object, or null if parsing fails
*/ */
fun parse(str: String): ProfileItem? { fun parse(str: String): ProfileItem {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false) var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.TROJAN) val config = ProfileItem.create(EConfigType.TROJAN)
@@ -1,7 +1,6 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
@@ -13,6 +12,7 @@ import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2rayConfigManager import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -34,7 +34,7 @@ object VmessFmt : FmtBase() {
var result = str.replace(EConfigType.VMESS.protocolScheme, "") var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result) result = Utils.decode(result)
if (TextUtils.isEmpty(result)) { if (TextUtils.isEmpty(result)) {
Log.w(AppConfig.TAG, "Toast decoding failed") LogUtil.w(AppConfig.TAG, "Toast decoding failed")
return null return null
} }
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java) ?: return null val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java) ?: return null
@@ -44,7 +44,7 @@ object VmessFmt : FmtBase() {
|| TextUtils.isEmpty(vmessQRCode.id) || TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net) || TextUtils.isEmpty(vmessQRCode.net)
) { ) {
Log.w(AppConfig.TAG, "Toast incorrect protocol") LogUtil.w(AppConfig.TAG, "Toast incorrect protocol")
return null return null
} }
@@ -46,7 +46,7 @@ object WireguardFmt : FmtBase() {
* @param str the Wireguard configuration file string to parse * @param str the Wireguard configuration file string to parse
* @return the parsed ProfileItem object, or null if parsing fails * @return the parsed ProfileItem object, or null if parsing fails
*/ */
fun parseWireguardConfFile(str: String): ProfileItem? { fun parseWireguardConfFile(str: String): ProfileItem {
val config = ProfileItem.create(EConfigType.WIREGUARD) val config = ProfileItem.create(EConfigType.WIREGUARD)
val interfaceParams: MutableMap<String, String> = mutableMapOf() val interfaceParams: MutableMap<String, String> = mutableMapOf()
@@ -3,7 +3,6 @@ package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2 import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -23,6 +22,7 @@ import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.net.URI import java.net.URI
@@ -47,7 +47,7 @@ object AngConfigManager {
Utils.setClipboard(context, conf) Utils.setClipboard(context, conf)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share config to clipboard", e) LogUtil.e(AppConfig.TAG, "Failed to share config to clipboard", e)
return -1 return -1
} }
return 0 return 0
@@ -76,7 +76,7 @@ object AngConfigManager {
} }
return sb.lines().count() - 1 return sb.lines().count() - 1
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e) LogUtil.e(AppConfig.TAG, "Failed to share non-custom configs to clipboard", e)
return -1 return -1
} }
} }
@@ -96,7 +96,7 @@ object AngConfigManager {
return QRCodeDecoder.createQRCode(conf) return QRCodeDecoder.createQRCode(conf)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share config as QR code", e) LogUtil.e(AppConfig.TAG, "Failed to share config as QR code", e)
return null return null
} }
} }
@@ -118,7 +118,7 @@ object AngConfigManager {
return -1 return -1
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share full content to clipboard", e) LogUtil.e(AppConfig.TAG, "Failed to share full content to clipboard", e)
return -1 return -1
} }
return 0 return 0
@@ -148,7 +148,7 @@ object AngConfigManager {
else -> {} else -> {}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e) LogUtil.e(AppConfig.TAG, "Failed to share config for GUID: $guid", e)
return "" return ""
} }
} }
@@ -167,7 +167,7 @@ object AngConfigManager {
count = parseBatchConfig(server, subid, append) count = parseBatchConfig(server, subid, append)
} }
if (count <= 0) { if (count <= 0) {
count = parseCustomConfigServer(server, subid) count = parseCustomConfigServer(server, subid, append)
} }
var countSub = parseBatchSubscription(server) var countSub = parseBatchSubscription(server)
@@ -203,7 +203,7 @@ object AngConfigManager {
} }
return count return count
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse batch subscription", e) LogUtil.e(AppConfig.TAG, "Failed to parse batch subscription", e)
} }
return 0 return 0
} }
@@ -221,21 +221,14 @@ object AngConfigManager {
if (servers == null) { if (servers == null) {
return 0 return 0
} }
val removedSelectedServer = // Find the currently selected server that matches the subscription ID
if (!TextUtils.isEmpty(subid) && !append) { val removedSelected = if (subid.isNotBlank() && !append) {
MmkvManager.decodeServerConfig( MmkvManager.getSelectServer()
MmkvManager.getSelectServer().orEmpty() .takeIf { it?.isNotBlank() == true }
)?.let { ?.let { MmkvManager.decodeServerConfig(it) }
if (it.subscriptionId == subid) { ?.takeIf { it.subscriptionId == subid }
return@let it } else {
} null
return@let null
}
} else {
null
}
if (!append) {
MmkvManager.removeServerViaSubid(subid)
} }
val subItem = MmkvManager.decodeSubscription(subid) val subItem = MmkvManager.decodeSubscription(subid)
@@ -254,23 +247,17 @@ object AngConfigManager {
// Batch save all parsed configs (only one serverList read/write) // Batch save all parsed configs (only one serverList read/write)
if (configs.isNotEmpty()) { if (configs.isNotEmpty()) {
val keys = batchSaveConfigs(configs, subid) if (!append) {
MmkvManager.removeServerViaSubid(subid)
// Handle removed selected server
removedSelectedServer?.let { removed ->
val matchKey = keys.find { key ->
val savedConfig = MmkvManager.decodeServerConfig(key)
savedConfig != null &&
savedConfig.server == removed.server &&
savedConfig.serverPort == removed.serverPort
}
matchKey?.let { MmkvManager.setSelectServer(it) }
} }
val keyToProfile = batchSaveConfigs(configs, subid)
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
matchKey?.let { MmkvManager.setSelectServer(it) }
} }
return configs.size return configs.size
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse batch config", e) LogUtil.e(AppConfig.TAG, "Failed to parse batch config", e)
} }
return 0 return 0
} }
@@ -281,10 +268,10 @@ object AngConfigManager {
* *
* @param configs The list of ProfileItem to save. * @param configs The list of ProfileItem to save.
* @param subid The subscription ID. * @param subid The subscription ID.
* @return The list of generated keys. * @return Map of generated keys to their corresponding ProfileItem.
*/ */
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): List<String> { private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): Map<String, ProfileItem> {
val keys = mutableListOf<String>() val keyToProfile = mutableMapOf<String, ProfileItem>()
// Read serverList once // Read serverList once
val serverList = MmkvManager.decodeServerList(subid) val serverList = MmkvManager.decodeServerList(subid)
@@ -302,12 +289,67 @@ object AngConfigManager {
needSetSelected = false needSetSelected = false
} }
} }
keys.add(key) keyToProfile[key] = config
} }
// Write serverList once // Write serverList once
MmkvManager.encodeServerList(serverList, subid) MmkvManager.encodeServerList(serverList, subid)
return keys return keyToProfile
}
/**
* Finds a matched profile key from the given key-profile map using multi-level matching.
* Matching priority (from highest to lowest):
* 1. Exact match: server + port + password
* 2. Match by remarks (exact match)
* 3. Match by server + port
* 4. Match by server only
*
* @param keyToProfile Map of server keys to their ProfileItem
* @param target Target profile to match
* @return Matched key or null
*/
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
if (keyToProfile.isEmpty() || target == null) return null
// Level 1: Match by remarks
if (target.remarks.isNotBlank()) {
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.remarks, target.remarks)
}?.key?.let { return it }
}
// Level 2: Exact match (server + port + password)
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.server, target.server) &&
isSameText(saved.serverPort, target.serverPort) &&
isSameText(saved.password, target.password)
}?.key?.let { return it }
// Level 3: Match by server + port
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.server, target.server) &&
isSameText(saved.serverPort, target.serverPort)
}?.key?.let { return it }
// Level 4: Match by server only
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.server, target.server)
}?.key?.let { return it }
return null
}
/**
* Case-insensitive trimmed string comparison.
*
* @param left First string
* @param right Second string
* @return True if both are non-empty and equal (case-insensitive, trimmed)
*/
private fun isSameText(left: String?, right: String?): Boolean {
if (left.isNullOrBlank() || right.isNullOrBlank()) return false
return left.trim().equals(right.trim(), ignoreCase = true)
} }
/** /**
@@ -315,9 +357,10 @@ object AngConfigManager {
* *
* @param server The server string. * @param server The server string.
* @param subid The subscription ID. * @param subid The subscription ID.
* @param append Whether to append the configurations.
* @return The number of configurations parsed. * @return The number of configurations parsed.
*/ */
private fun parseCustomConfigServer(server: String?, subid: String): Int { private fun parseCustomConfigServer(server: String?, subid: String, append: Boolean): Int {
if (server == null) { if (server == null) {
return 0 return 0
} }
@@ -330,6 +373,9 @@ object AngConfigManager {
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf() JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
if (serverList.isNotEmpty()) { if (serverList.isNotEmpty()) {
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
var count = 0 var count = 0
for (srv in serverList.reversed()) { for (srv in serverList.reversed()) {
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
@@ -342,7 +388,7 @@ object AngConfigManager {
return count return count
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e) LogUtil.e(AppConfig.TAG, "Failed to parse custom config server JSON array", e)
} }
try { try {
@@ -350,22 +396,28 @@ object AngConfigManager {
val config = CustomFmt.parse(server) ?: return 0 val config = CustomFmt.parse(server) ?: return 0
config.subscriptionId = subid config.subscriptionId = subid
config.description = generateDescription(config) config.description = generateDescription(config)
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
val key = MmkvManager.encodeServerConfig("", config) val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server) MmkvManager.encodeServerRaw(key, server)
return 1 return 1
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse custom config server as single config", e) LogUtil.e(AppConfig.TAG, "Failed to parse custom config server as single config", e)
} }
return 0 return 0
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) { } else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
try { try {
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
config.description = generateDescription(config) config.description = generateDescription(config)
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
val key = MmkvManager.encodeServerConfig("", config) val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server) MmkvManager.encodeServerRaw(key, server)
return 1 return 1
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse WireGuard config file", e) LogUtil.e(AppConfig.TAG, "Failed to parse WireGuard config file", e)
} }
return 0 return 0
} else { } else {
@@ -426,7 +478,7 @@ object AngConfigManager {
return config return config
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse config", e) LogUtil.e(AppConfig.TAG, "Failed to parse config", e)
return null return null
} }
} }
@@ -443,7 +495,7 @@ object AngConfigManager {
acc + updateConfigViaSub(subscription) acc + updateConfigViaSub(subscription)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e) LogUtil.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
SubscriptionUpdateResult() SubscriptionUpdateResult()
} }
} }
@@ -478,21 +530,21 @@ object AngConfigManager {
return SubscriptionUpdateResult(failureCount = 1) return SubscriptionUpdateResult(failureCount = 1)
} }
} }
Log.i(AppConfig.TAG, url) LogUtil.i(AppConfig.TAG, url)
val userAgent = it.subscription.userAgent val userAgent = it.subscription.userAgent
var configText = try { var configText = try {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort) HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e) LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
"" ""
} }
if (configText.isEmpty()) { if (configText.isEmpty()) {
configText = try { configText = try {
HttpUtil.getUrlContentWithUserAgent(url, userAgent) HttpUtil.getUrlContentWithUserAgent(url, userAgent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e) LogUtil.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
"" ""
} }
} }
@@ -504,7 +556,7 @@ object AngConfigManager {
if (count > 0) { if (count > 0) {
it.subscription.lastUpdated = System.currentTimeMillis() it.subscription.lastUpdated = System.currentTimeMillis()
MmkvManager.encodeSubscription(it.guid, it.subscription) MmkvManager.encodeSubscription(it.guid, it.subscription)
Log.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs") LogUtil.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
return SubscriptionUpdateResult( return SubscriptionUpdateResult(
configCount = count, configCount = count,
successCount = 1 successCount = 1
@@ -514,7 +566,7 @@ object AngConfigManager {
return SubscriptionUpdateResult(failureCount = 1) return SubscriptionUpdateResult(failureCount = 1)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to update config via subscription", e) LogUtil.e(AppConfig.TAG, "Failed to update config via subscription", e)
return SubscriptionUpdateResult(failureCount = 1) return SubscriptionUpdateResult(failureCount = 1)
} }
} }
@@ -533,7 +585,7 @@ object AngConfigManager {
count = parseBatchConfig(server, subid, append) count = parseBatchConfig(server, subid, append)
} }
if (count <= 0) { if (count <= 0) {
count = parseCustomConfigServer(server, subid) count = parseCustomConfigServer(server, subid, append)
} }
return count return count
} }
@@ -344,7 +344,7 @@ object MmkvManager {
//region Subscriptions //region Subscriptions
private fun getSubscriptionId(subscriptionId: String?):String { private fun getSubscriptionId(subscriptionId: String?): String {
return subscriptionId?.ifEmpty { DEFAULT_SUBSCRIPTION_ID } ?: DEFAULT_SUBSCRIPTION_ID return subscriptionId?.ifEmpty { DEFAULT_SUBSCRIPTION_ID } ?: DEFAULT_SUBSCRIPTION_ID
} }
@@ -374,7 +374,7 @@ object MmkvManager {
decodeSubsList().forEach { key -> decodeSubsList().forEach { key ->
val json = subStorage.decodeString(key) val json = subStorage.decodeString(key)
if (!json.isNullOrBlank()) { if (!json.isNullOrBlank()) {
val item = JsonUtil.fromJson(json, SubscriptionItem::class.java)?: SubscriptionItem() val item = JsonUtil.fromJson(json, SubscriptionItem::class.java) ?: SubscriptionItem()
subscriptions.add(SubscriptionCache(key, item)) subscriptions.add(SubscriptionCache(key, item))
} }
} }
@@ -442,7 +442,7 @@ object MmkvManager {
return if (json.isNullOrBlank()) { return if (json.isNullOrBlank()) {
mutableListOf() mutableListOf()
} else { } else {
JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList()?: mutableListOf() JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
} }
} }
@@ -460,7 +460,7 @@ object MmkvManager {
assetStorage.allKeys()?.forEach { key -> assetStorage.allKeys()?.forEach { key ->
val json = assetStorage.decodeString(key) val json = assetStorage.decodeString(key)
if (!json.isNullOrBlank()) { if (!json.isNullOrBlank()) {
val item = JsonUtil.fromJson(json, AssetUrlItem::class.java)?: AssetUrlItem() val item = JsonUtil.fromJson(json, AssetUrlItem::class.java) ?: AssetUrlItem()
assetUrlItems.add(AssetUrlCache(key, item)) assetUrlItems.add(AssetUrlCache(key, item))
} }
} }
@@ -510,7 +510,7 @@ object MmkvManager {
fun decodeRoutingRulesets(): MutableList<RulesetItem>? { fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET) val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
if (ruleset.isNullOrEmpty()) return null if (ruleset.isNullOrEmpty()) return null
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java)?.toMutableList()?: mutableListOf() return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java)?.toMutableList() ?: mutableListOf()
} }
/** /**
@@ -9,7 +9,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
@@ -17,6 +16,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toSpeedString import com.v2ray.ang.extension.toSpeedString
import com.v2ray.ang.ui.MainActivity import com.v2ray.ang.ui.MainActivity
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -57,7 +57,7 @@ object NotificationManager {
// If the query interval is too short, skip this round to avoid excessive CPU usage // If the query interval is too short, skip this round to avoid excessive CPU usage
if (sinceLastQueryIn < QUERY_INTERVAL_MS) { if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
Log.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping") LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
lastQueryTime = queryTime lastQueryTime = queryTime
delay(QUERY_INTERVAL_MS) delay(QUERY_INTERVAL_MS)
continue continue
@@ -2,8 +2,8 @@ package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.content.res.AssetManager import android.content.res.AssetManager
import android.os.Build
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
@@ -27,14 +27,19 @@ import com.v2ray.ang.handler.MmkvManager.decodeSubscription
import com.v2ray.ang.handler.MmkvManager.encodeSubscription import com.v2ray.ang.handler.MmkvManager.encodeSubscription
import com.v2ray.ang.handler.MmkvManager.removeSubscription import com.v2ray.ang.handler.MmkvManager.removeSubscription
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Collections import java.util.Collections
import java.util.Locale import java.util.Locale
import kotlin.random.Random
object SettingsManager { object SettingsManager {
@Volatile
private var runtimeSocksPort: Int? = null
fun initApp(context: Context) { fun initApp(context: Context) {
ensureDefaultSettings() ensureDefaultSettings()
//ensureDefaultSubscription() //ensureDefaultSubscription()
@@ -100,7 +105,7 @@ object SettingsManager {
resetRoutingRulesetsCommon(rulesetList) resetRoutingRulesetsCommon(rulesetList)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(ANG_PACKAGE, "Failed to reset routing rulesets", e) LogUtil.e(ANG_PACKAGE, "Failed to reset routing rulesets", e)
return false return false
} }
} }
@@ -219,7 +224,7 @@ object SettingsManager {
* @param toPosition The position to swap to. * @param toPosition The position to swap to.
*/ */
fun swapSubscriptions(fromPosition: Int, toPosition: Int) { fun swapSubscriptions(fromPosition: Int, toPosition: Int) {
val subsList = MmkvManager.decodeSubsList() val subsList = decodeSubsList()
if (subsList.isEmpty()) return if (subsList.isEmpty()) return
Collections.swap(subsList, fromPosition, toPosition) Collections.swap(subsList, fromPosition, toPosition)
@@ -249,7 +254,7 @@ object SettingsManager {
fun removeSubscriptionWithDefault(subid: String) { fun removeSubscriptionWithDefault(subid: String) {
// val subsList = decodeSubsList() // val subsList = decodeSubsList()
// if (subsList.size == 1 && subsList.first() == DEFAULT_SUBSCRIPTION_ID) { // if (subsList.size == 1 && subsList.first() == DEFAULT_SUBSCRIPTION_ID) {
// Log.i(ANG_PACKAGE,"Attempted to remove the only existing default subscription, operation ignored.") // LogUtil.i(ANG_PACKAGE,"Attempted to remove the only existing default subscription, operation ignored.")
// return // return
// } // }
@@ -273,7 +278,30 @@ object SettingsManager {
* @return The SOCKS port. * @return The SOCKS port.
*/ */
fun getSocksPort(): Int { fun getSocksPort(): Int {
return Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt()) val port =
if (IsDynamicSocksPort()) {
runtimeSocksPort ?: refreshRuntimeSocksPort()
} else {
Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
}
return port ?: AppConfig.PORT_SOCKS.toInt()
}
@Synchronized
fun refreshRuntimeSocksPort(): Int? {
if (IsDynamicSocksPort()) {
runtimeSocksPort = generateRandomSocksPort()
return runtimeSocksPort
}
return null
}
fun getSocksUsername(): String? {
return MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_USERNAME)?.trim()?.takeIf { it.isNotEmpty() }
}
fun getSocksPassword(): String? {
return MmkvManager.decodeSettingsString(AppConfig.PREF_SOCKS_PASSWORD)?.trim()?.takeIf { it.isNotEmpty() }
} }
/** /**
@@ -284,6 +312,18 @@ object SettingsManager {
return getSocksPort() + if (Utils.isXray()) 0 else 1 return getSocksPort() + if (Utils.isXray()) 0 else 1
} }
private fun IsDynamicSocksPort(): Boolean {
return MmkvManager.decodeSettingsBool(AppConfig.PREF_DYNAMIC_SOCKS_PORT, false)
}
private fun generateRandomSocksPort(): Int {
return if (Utils.isXray()) {
Random.nextInt(10000, 65536)
} else {
Random.nextInt(10000, 65535)
}
}
/** /**
* Initialize assets. * Initialize assets.
* @param context The application context. * @param context The application context.
@@ -304,10 +344,10 @@ object SettingsManager {
input.copyTo(output) input.copyTo(output)
} }
} }
Log.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}") LogUtil.i(AppConfig.TAG, "Copied from apk assets folder to ${target.absolutePath}")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e) LogUtil.e(ANG_PACKAGE, "asset copy failed", e)
} }
} }
@@ -433,12 +473,34 @@ object SettingsManager {
return mode == null || mode == VPN return mode == null || mode == VPN
} }
/**
* Check if process routing can be used.
*/
fun canUseProcessRouting(): Boolean {
// Android 10+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return false
}
// Must xray tun
if (isUsingHevTun()) {
return false
}
// Must have route only enabled
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false) == false) {
return false
}
return true
}
/** /**
* Ensure default settings are present in MMKV. * Ensure default settings are present in MMKV.
*/ */
private fun ensureDefaultSettings() { private fun ensureDefaultSettings() {
// Write defaults in the exact order requested by the user // Write defaults in the exact order requested by the user
ensureDefaultValue(AppConfig.PREF_MODE, AppConfig.VPN) ensureDefaultValue(AppConfig.PREF_MODE, VPN)
ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN) ensureDefaultValue(AppConfig.PREF_VPN_DNS, AppConfig.DNS_VPN)
ensureDefaultValue(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString()) ensureDefaultValue(AppConfig.PREF_VPN_MTU, AppConfig.VPN_MTU.toString())
ensureDefaultValue(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL) ensureDefaultValue(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL, AppConfig.SUBSCRIPTION_DEFAULT_UPDATE_INTERVAL)
@@ -2,12 +2,12 @@ package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.os.SystemClock import android.os.SystemClock
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.IPAPIInfo import com.v2ray.ang.dto.IPAPIInfo
import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import java.io.IOException import java.io.IOException
@@ -62,11 +62,11 @@ object SpeedtestManager {
socket.close() socket.close()
return time return time
} catch (e: UnknownHostException) { } catch (e: UnknownHostException) {
Log.e(AppConfig.TAG, "Unknown host: $url", e) LogUtil.e(AppConfig.TAG, "Unknown host: $url", e)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(AppConfig.TAG, "socketConnectTime IOException: $e") LogUtil.e(AppConfig.TAG, "socketConnectTime IOException: $e")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e) LogUtil.e(AppConfig.TAG, "Failed to establish socket connection to $url:$port", e)
} }
return -1 return -1
} }
@@ -108,10 +108,10 @@ object SpeedtestManager {
) )
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.e(AppConfig.TAG, "Connection test IOException", e) LogUtil.e(AppConfig.TAG, "Connection test IOException", e)
result = context.getString(R.string.connection_test_error, e.message) result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Connection test Exception", e) LogUtil.e(AppConfig.TAG, "Connection test Exception", e)
result = context.getString(R.string.connection_test_error, e.message) result = context.getString(R.string.connection_test_error, e.message)
} finally { } finally {
conn.disconnect() conn.disconnect()
@@ -5,13 +5,13 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.util.LogUtil
object SubscriptionUpdater { object SubscriptionUpdater {
@@ -34,7 +34,7 @@ object SubscriptionUpdater {
*/ */
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Log.i(AppConfig.TAG, "subscription automatic update starting") LogUtil.i(AppConfig.TAG, "subscription automatic update starting")
val subs = MmkvManager.decodeSubscriptions().filter { it.subscription.autoUpdate } val subs = MmkvManager.decodeSubscriptions().filter { it.subscription.autoUpdate }
@@ -52,7 +52,7 @@ object SubscriptionUpdater {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
notificationManager.notify(3, notification.build()) notificationManager.notify(3, notification.build())
Log.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}") LogUtil.i(AppConfig.TAG, "subscription automatic update: ---${subItem.remarks}")
AngConfigManager.updateConfigViaSub(sub) AngConfigManager.updateConfigViaSub(sub)
notification.setContentText("Updating ${subItem.remarks}") notification.setContentText("Updating ${subItem.remarks}")
} }
@@ -2,7 +2,6 @@ package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
import com.v2ray.ang.dto.CheckUpdateResult import com.v2ray.ang.dto.CheckUpdateResult
@@ -10,6 +9,7 @@ import com.v2ray.ang.dto.GitHubRelease
import com.v2ray.ang.extension.concatUrl import com.v2ray.ang.extension.concatUrl
import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@@ -42,7 +42,7 @@ object UpdateCheckerManager {
} }
val latestVersion = latestRelease.tagName.removePrefix("v") val latestVersion = latestRelease.tagName.removePrefix("v")
Log.i( LogUtil.i(
AppConfig.TAG, AppConfig.TAG,
"Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})" "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})"
) )
@@ -69,27 +69,27 @@ object UpdateCheckerManager {
try { try {
val apkFile = File(context.cacheDir, "update.apk") val apkFile = File(context.cacheDir, "update.apk")
Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}") LogUtil.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
FileOutputStream(apkFile).use { outputStream -> FileOutputStream(apkFile).use { outputStream ->
connection.inputStream.use { inputStream -> connection.inputStream.use { inputStream ->
inputStream.copyTo(outputStream) inputStream.copyTo(outputStream)
} }
} }
Log.i(AppConfig.TAG, "APK download completed") LogUtil.i(AppConfig.TAG, "APK download completed")
return@withContext apkFile return@withContext apkFile
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}") LogUtil.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
return@withContext null return@withContext null
} finally { } finally {
try { try {
connection.disconnect() connection.disconnect()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error closing connection: ${e.message}") LogUtil.e(AppConfig.TAG, "Error closing connection: ${e.message}")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}") LogUtil.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
return@withContext null return@withContext null
} }
} }
@@ -1,8 +1,8 @@
package com.v2ray.ang.handler package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import go.Seq import go.Seq
import libv2ray.CoreCallbackHandler import libv2ray.CoreCallbackHandler
@@ -32,14 +32,14 @@ object V2RayNativeManager {
val assetPath = Utils.userAssetPath(context) val assetPath = Utils.userAssetPath(context)
val deviceId = Utils.getDeviceIdForXUDPBaseKey() val deviceId = Utils.getDeviceIdForXUDPBaseKey()
Libv2ray.initCoreEnv(assetPath, deviceId) Libv2ray.initCoreEnv(assetPath, deviceId)
Log.i(AppConfig.TAG, "V2Ray core environment initialized successfully") LogUtil.i(AppConfig.TAG, "V2Ray core environment initialized successfully")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to initialize V2Ray core environment", e) LogUtil.e(AppConfig.TAG, "Failed to initialize V2Ray core environment", e)
initialized.set(false) initialized.set(false)
throw e throw e
} }
} else { } else {
Log.d(AppConfig.TAG, "V2Ray core environment already initialized, skipping") LogUtil.d(AppConfig.TAG, "V2Ray core environment already initialized, skipping")
} }
} }
@@ -53,7 +53,7 @@ object V2RayNativeManager {
return try { return try {
Libv2ray.checkVersionX() Libv2ray.checkVersionX()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to check V2Ray version", e) LogUtil.e(AppConfig.TAG, "Failed to check V2Ray version", e)
"Unknown" "Unknown"
} }
} }
@@ -69,7 +69,7 @@ object V2RayNativeManager {
return try { return try {
Libv2ray.measureOutboundDelay(config, testUrl) Libv2ray.measureOutboundDelay(config, testUrl)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to measure outbound delay", e) LogUtil.e(AppConfig.TAG, "Failed to measure outbound delay", e)
-1L -1L
} }
} }
@@ -84,7 +84,7 @@ object V2RayNativeManager {
return try { return try {
Libv2ray.newCoreController(handler) Libv2ray.newCoreController(handler)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to create core controller", e) LogUtil.e(AppConfig.TAG, "Failed to create core controller", e)
throw e throw e
} }
} }
@@ -5,8 +5,10 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.system.OsConstants
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -16,6 +18,7 @@ import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.service.V2RayProxyOnlyService import com.v2ray.ang.service.V2RayProxyOnlyService
import com.v2ray.ang.service.V2RayVpnService import com.v2ray.ang.service.V2RayVpnService
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -23,18 +26,26 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import libv2ray.CoreCallbackHandler import libv2ray.CoreCallbackHandler
import libv2ray.CoreController import libv2ray.CoreController
import libv2ray.ProcessFinder
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
import java.net.InetSocketAddress
object V2RayServiceManager { object V2RayServiceManager {
private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback()) private val coreController: CoreController = V2RayNativeManager.newCoreController(CoreCallback())
private val mMsgReceive = ReceiveMessageHandler() private val mMsgReceive = ReceiveMessageHandler()
private var currentConfig: ProfileItem? = null private var currentConfig: ProfileItem? = null
private var processFinder: XrayProcessFinder? = null
var serviceControl: SoftReference<ServiceControl>? = null var serviceControl: SoftReference<ServiceControl>? = null
set(value) { set(value) {
field = value field = value
V2RayNativeManager.initCoreEnv(value?.get()?.getService()) val service = value?.get()?.getService()
V2RayNativeManager.initCoreEnv(service)
if (service != null && processFinder == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
processFinder = XrayProcessFinder(service)
coreController.registerProcessFinder(processFinder)
}
} }
/** /**
@@ -57,7 +68,7 @@ object V2RayServiceManager {
* @param guid The GUID of the server configuration to use (optional). * @param guid The GUID of the server configuration to use (optional).
*/ */
fun startVService(context: Context, guid: String? = null) { fun startVService(context: Context, guid: String? = null) {
Log.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}") LogUtil.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
if (guid != null) { if (guid != null) {
MmkvManager.setSelectServer(guid) MmkvManager.setSelectServer(guid)
@@ -94,19 +105,19 @@ object V2RayServiceManager {
*/ */
private fun startContextService(context: Context) { private fun startContextService(context: Context) {
if (coreController.isRunning) { if (coreController.isRunning) {
Log.w(AppConfig.TAG, "StartCore-Manager: Core already running") LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return return
} }
val guid = MmkvManager.getSelectServer() val guid = MmkvManager.getSelectServer()
if (guid == null) { if (guid == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: No server selected") LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
return return
} }
val config = MmkvManager.decodeServerConfig(guid) val config = MmkvManager.decodeServerConfig(guid)
if (config == null) { if (config == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config") LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
return return
} }
@@ -115,9 +126,12 @@ object V2RayServiceManager {
&& !Utils.isValidUrl(config.server) && !Utils.isValidUrl(config.server)
&& !Utils.isPureIpAddress(config.server.orEmpty()) && !Utils.isPureIpAddress(config.server.orEmpty())
) { ) {
Log.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration") LogUtil.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
return return
} }
// refresh socks port when enabled dynamic socks port
SettingsManager.refreshRuntimeSocksPort()
// val result = V2rayConfigUtil.getV2rayConfig(context, guid) // val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) return // if (!result.status) return
@@ -129,17 +143,17 @@ object V2RayServiceManager {
val isVpnMode = SettingsManager.isVpnMode() val isVpnMode = SettingsManager.isVpnMode()
val intent = if (isVpnMode) { val intent = if (isVpnMode) {
Log.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
Intent(context.applicationContext, V2RayVpnService::class.java) Intent(context.applicationContext, V2RayVpnService::class.java)
} else { } else {
Log.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service")
Intent(context.applicationContext, V2RayProxyOnlyService::class.java) Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
} }
try { try {
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to start service", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to start service", e)
} }
} }
@@ -150,32 +164,33 @@ object V2RayServiceManager {
*/ */
fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean { fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean {
if (coreController.isRunning) { if (coreController.isRunning) {
Log.w(AppConfig.TAG, "StartCore-Manager: Core already running") LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return false return false
} }
val service = getService() val service = getService()
if (service == null) { if (service == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: Service is null") LogUtil.e(AppConfig.TAG, "StartCore-Manager: Service is null")
return false return false
} }
val guid = MmkvManager.getSelectServer() val guid = MmkvManager.getSelectServer()
if (guid == null) { if (guid == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: No server selected") LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
return false return false
} }
val config = MmkvManager.decodeServerConfig(guid) val config = MmkvManager.decodeServerConfig(guid)
if (config == null) { if (config == null) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config") LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
return false return false
} }
Log.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
val result = V2rayConfigManager.getV2rayConfig(service, guid) val result = V2rayConfigManager.getV2rayConfig(service, guid)
LogUtil.d(AppConfig.TAG, result.content)
if (!result.status) { if (!result.status) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to get V2Ray config") LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to get V2Ray config")
return false return false
} }
@@ -186,7 +201,7 @@ object V2RayServiceManager {
mFilter.addAction(Intent.ACTION_USER_PRESENT) mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags()) ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to register receiver", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to register receiver", e)
return false return false
} }
@@ -200,12 +215,12 @@ object V2RayServiceManager {
NotificationManager.showNotification(currentConfig) NotificationManager.showNotification(currentConfig)
coreController.startLoop(result.content, tunFd) coreController.startLoop(result.content, tunFd)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to start core loop", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to start core loop", e)
return false return false
} }
if (coreController.isRunning == false) { if (coreController.isRunning == false) {
Log.e(AppConfig.TAG, "StartCore-Manager: Core failed to start") LogUtil.e(AppConfig.TAG, "StartCore-Manager: Core failed to start")
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
NotificationManager.cancelNotification() NotificationManager.cancelNotification()
return false return false
@@ -214,9 +229,9 @@ object V2RayServiceManager {
try { try {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
NotificationManager.startSpeedNotification(currentConfig) NotificationManager.startSpeedNotification(currentConfig)
Log.i(AppConfig.TAG, "StartCore-Manager: Core started successfully") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to complete startup", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to complete startup", e)
return false return false
} }
return true return true
@@ -235,7 +250,7 @@ object V2RayServiceManager {
try { try {
coreController.stopLoop() coreController.stopLoop()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
} }
} }
} }
@@ -246,7 +261,7 @@ object V2RayServiceManager {
try { try {
service.unregisterReceiver(mMsgReceive) service.unregisterReceiver(mMsgReceive)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
} }
return true return true
@@ -280,14 +295,14 @@ object V2RayServiceManager {
try { try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl()) time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message" errorStr = e.message?.substringAfter("\":") ?: "empty message"
} }
if (time == -1L) { if (time == -1L) {
try { try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true)) time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message" errorStr = e.message?.substringAfter("\":") ?: "empty message"
} }
} }
@@ -339,7 +354,7 @@ object V2RayServiceManager {
serviceControl.stopService() serviceControl.stopService()
0 0
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e) LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
-1 -1
} }
} }
@@ -355,6 +370,43 @@ object V2RayServiceManager {
} }
} }
/**
* Process finder implementation for Xray core.
* Uses ConnectivityManager to find the owning UID of a connection based on network parameters.
*/
private class XrayProcessFinder(context: Context) : ProcessFinder {
private val cm: ConnectivityManager? = context.getSystemService(ConnectivityManager::class.java)
override fun findProcessByConnection(network: String, srcIP: String, srcPort: Long, destIP: String, destPort: Long): Long {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return -1L
if (cm == null) return -1L
val proto = when (network) {
"tcp" -> OsConstants.IPPROTO_TCP
"udp" -> OsConstants.IPPROTO_UDP
else -> return -1L
}
if (destIP.isBlank() || destPort == 0L) {
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to :$destPort, (no dest)")
return -1L
}
return try {
val uid = cm.getConnectionOwnerUid(
proto,
InetSocketAddress(srcIP, srcPort.toInt()),
InetSocketAddress(destIP, destPort.toInt())
).toLong()
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid")
//LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid,${PackageUidResolver.uidToPackageName(uid.toString())}")
uid
} catch (e: Exception) {
-1L
}
}
}
/** /**
* Broadcast receiver for handling messages sent to the service. * Broadcast receiver for handling messages sent to the service.
* Handles registration, service control, and screen events. * Handles registration, service control, and screen events.
@@ -386,12 +438,12 @@ object V2RayServiceManager {
} }
AppConfig.MSG_STATE_STOP -> { AppConfig.MSG_STATE_STOP -> {
Log.i(AppConfig.TAG, "StartCore-Manager: Stop service") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Stop service")
serviceControl.stopService() serviceControl.stopService()
} }
AppConfig.MSG_STATE_RESTART -> { AppConfig.MSG_STATE_RESTART -> {
Log.i(AppConfig.TAG, "StartCore-Manager: Restart service") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Restart service")
serviceControl.stopService() serviceControl.stopService()
Thread.sleep(500L) Thread.sleep(500L)
startVService(serviceControl.getService()) startVService(serviceControl.getService())
@@ -404,12 +456,12 @@ object V2RayServiceManager {
when (intent?.action) { when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> { Intent.ACTION_SCREEN_OFF -> {
Log.i(AppConfig.TAG, "StartCore-Manager: Screen off") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off")
NotificationManager.stopSpeedNotification(currentConfig) NotificationManager.stopSpeedNotification(currentConfig)
} }
Intent.ACTION_SCREEN_ON -> { Intent.ACTION_SCREEN_ON -> {
Log.i(AppConfig.TAG, "StartCore-Manager: Screen on") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on")
NotificationManager.startSpeedNotification(currentConfig) NotificationManager.startSpeedNotification(currentConfig)
} }
} }
@@ -2,9 +2,9 @@ package com.v2ray.ang.handler
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
import com.v2ray.ang.dto.ConfigResult import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
@@ -27,6 +27,8 @@ import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt import com.v2ray.ang.fmt.WireguardFmt
import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.PackageUidResolver
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
object V2rayConfigManager { object V2rayConfigManager {
@@ -53,7 +55,7 @@ object V2rayConfigManager {
getV2rayNormalConfig(context, guid, config) getV2rayNormalConfig(context, guid, config)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get V2ray config", e) LogUtil.e(AppConfig.TAG, "Failed to get V2ray config", e)
return ConfigResult(false) return ConfigResult(false)
} }
} }
@@ -77,7 +79,7 @@ object V2rayConfigManager {
getV2rayNormalConfig4Speedtest(context, guid, config) getV2rayNormalConfig4Speedtest(context, guid, config)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e) LogUtil.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e)
return ConfigResult(false) return ConfigResult(false)
} }
} }
@@ -186,7 +188,7 @@ object V2rayConfigManager {
val address = config.server ?: return result val address = config.server ?: return result
if (!Utils.isPureIpAddress(address)) { if (!Utils.isPureIpAddress(address)) {
if (!Utils.isValidUrl(address)) { if (!Utils.isValidUrl(address)) {
Log.w(AppConfig.TAG, "$address is an invalid ip or domain") LogUtil.w(AppConfig.TAG, "$address is an invalid ip or domain")
return result return result
} }
} }
@@ -200,7 +202,7 @@ object V2rayConfigManager {
getOutbounds(v2rayConfig, config) ?: return result getOutbounds(v2rayConfig, config) ?: return result
getMoreOutbounds(v2rayConfig, config.subscriptionId) getMoreOutbounds(v2rayConfig, config.subscriptionId)
getRouting(v2rayConfig) getRouting(context, v2rayConfig)
getFakeDns(v2rayConfig) getFakeDns(v2rayConfig)
@@ -233,7 +235,7 @@ object V2rayConfigManager {
.toList() .toList()
if (validConfigs.isEmpty()) { if (validConfigs.isEmpty()) {
Log.w(AppConfig.TAG, "All configs are invalid") LogUtil.w(AppConfig.TAG, "All configs are invalid")
return null return null
} }
@@ -257,7 +259,7 @@ object V2rayConfigManager {
outboundsList.addAll(v2rayConfig.outbounds) outboundsList.addAll(v2rayConfig.outbounds)
v2rayConfig.outbounds = ArrayList(outboundsList) v2rayConfig.outbounds = ArrayList(outboundsList)
getRouting(v2rayConfig) getRouting(context, v2rayConfig)
getFakeDns(v2rayConfig) getFakeDns(v2rayConfig)
@@ -295,7 +297,7 @@ object V2rayConfigManager {
val address = config.server ?: return result val address = config.server ?: return result
if (!Utils.isPureIpAddress(address)) { if (!Utils.isPureIpAddress(address)) {
if (!Utils.isValidUrl(address)) { if (!Utils.isValidUrl(address)) {
Log.w(AppConfig.TAG, "$address is an invalid ip or domain") LogUtil.w(AppConfig.TAG, "$address is an invalid ip or domain")
return result return result
} }
} }
@@ -373,12 +375,29 @@ object V2rayConfigManager {
private fun getInbounds(v2rayConfig: V2rayConfig): Boolean { private fun getInbounds(v2rayConfig: V2rayConfig): Boolean {
try { try {
val socksPort = SettingsManager.getSocksPort() val socksPort = SettingsManager.getSocksPort()
val socksUsername = SettingsManager.getSocksUsername()
val socksPassword = SettingsManager.getSocksPassword()
val inbound1 = v2rayConfig.inbounds[0] val inbound1 = v2rayConfig.inbounds[0]
if (inbound1.settings == null) {
inbound1.settings = V2rayConfig.InboundBean.InSettingsBean()
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
inbound1.listen = AppConfig.LOOPBACK inbound1.listen = AppConfig.LOOPBACK
} }
inbound1.port = socksPort inbound1.port = socksPort
if (socksUsername != null && socksPassword != null) {
inbound1.settings?.auth = "password"
inbound1.settings?.accounts = listOf(
V2rayConfig.InboundBean.InSettingsBean.SocksAccountBean(
user = socksUsername,
pass = socksPassword
)
)
} else {
inbound1.settings?.auth = "noauth"
inbound1.settings?.accounts = null
}
val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
val sniffAllTlsAndHttp = val sniffAllTlsAndHttp =
MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
@@ -406,7 +425,7 @@ object V2rayConfigManager {
inboundTun?.sniffing = inbound1.sniffing inboundTun?.sniffing = inbound1.sniffing
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure inbounds", e) LogUtil.e(AppConfig.TAG, "Failed to configure inbounds", e)
return false return false
} }
return true return true
@@ -427,6 +446,45 @@ object V2rayConfigManager {
} }
} }
/**
* Pre-pass: scans all routing rulesets for non-builtin outbound tags,
* looks up the matching profile by remarks, converts it to an OutboundBean,
* and appends it to v2rayConfig.outbounds. This must run before getRouting
* so that every custom tag is already present when routing rules are applied.
*
* @param v2rayConfig The V2ray configuration object to be modified
*/
private fun injectCustomOutbounds(v2rayConfig: V2rayConfig) {
val existingTags = v2rayConfig.outbounds.mapTo(mutableSetOf()) { it.tag }
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return
rulesetItems
.filter { it.enabled }
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
.filter { it !in BUILTIN_OUTBOUND_TAGS }
.distinct()
.forEach { tag ->
if (tag in existingTags) return@forEach
try {
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
LogUtil.w(AppConfig.TAG, "Custom outbound tag '$tag' not found by remarks, skipping")
return@forEach
}
val outbound = convertProfile2Outbound(profile) ?: run {
LogUtil.w(AppConfig.TAG, "Could not convert profile '$tag' to outbound, skipping")
return@forEach
}
updateOutboundWithGlobalSettings(outbound)
outbound.tag = tag
v2rayConfig.outbounds.add(outbound)
existingTags.add(tag)
LogUtil.d(AppConfig.TAG, "Injected custom outbound: tag='$tag'")
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to inject custom outbound for tag '$tag', skipping", e)
}
}
}
/** /**
* Configures routing settings for V2ray. * Configures routing settings for V2ray.
* *
@@ -435,19 +493,22 @@ object V2rayConfigManager {
* @param v2rayConfig The V2ray configuration object to be modified * @param v2rayConfig The V2ray configuration object to be modified
* @return true if routing configuration was successful, false otherwise * @return true if routing configuration was successful, false otherwise
*/ */
private fun getRouting(v2rayConfig: V2rayConfig): Boolean { private fun getRouting(context: Context, v2rayConfig: V2rayConfig): Boolean {
try { try {
v2rayConfig.routing.domainStrategy = v2rayConfig.routing.domainStrategy =
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
?: "AsIs" ?: "AsIs"
// Pre-pass: inject outbounds referenced by remarks in routing rules
injectCustomOutbounds(v2rayConfig)
val rulesetItems = MmkvManager.decodeRoutingRulesets() val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key -> rulesetItems?.forEach { key ->
getRoutingUserRule(key, v2rayConfig) getRoutingUserRule(context, key, v2rayConfig)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure routing", e) LogUtil.e(AppConfig.TAG, "Failed to configure routing", e)
return false return false
} }
return true return true
@@ -459,7 +520,7 @@ object V2rayConfigManager {
* @param item The ruleset item to add * @param item The ruleset item to add
* @param v2rayConfig The V2ray configuration object to be modified * @param v2rayConfig The V2ray configuration object to be modified
*/ */
private fun getRoutingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) { private fun getRoutingUserRule(context: Context, item: RulesetItem?, v2rayConfig: V2rayConfig) {
try { try {
if (item == null || !item.enabled) { if (item == null || !item.enabled) {
return return
@@ -480,10 +541,32 @@ object V2rayConfigManager {
rule.ip = updatedIpList rule.ip = updatedIpList
} }
if (SettingsManager.canUseProcessRouting()) {
// Convert process package names to UIDs
rule.process?.let { processList ->
if (processList.isNotEmpty()) {
val uids = PackageUidResolver.packageNamesToUids(context, processList)
rule.process = uids.ifEmpty { null }
}
}
} else {
rule.process = null
}
// If the outbound tag is a custom one that failed to inject, fall back to proxy
val outboundTag = rule.outboundTag
if (!outboundTag.isNullOrBlank()
&& outboundTag !in BUILTIN_OUTBOUND_TAGS
&& v2rayConfig.outbounds.none { it.tag == outboundTag }
) {
LogUtil.w(AppConfig.TAG, "Outbound tag '$outboundTag' not found, falling back to '${AppConfig.TAG_PROXY}'")
rule.outboundTag = AppConfig.TAG_PROXY
}
v2rayConfig.routing.rules.add(rule) v2rayConfig.routing.rules.add(rule)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to apply routing user rule", e) LogUtil.e(AppConfig.TAG, "Failed to apply routing user rule", e)
} }
} }
@@ -538,7 +621,7 @@ object V2rayConfigManager {
) )
} }
if(SettingsManager.isVpnMode()) { if (SettingsManager.isVpnMode()) {
if (SettingsManager.isUsingHevTun()) { if (SettingsManager.isUsingHevTun()) {
//hev-socks5-tunnel dns routing //hev-socks5-tunnel dns routing
v2rayConfig.routing.rules.add( v2rayConfig.routing.rules.add(
@@ -572,7 +655,7 @@ object V2rayConfigManager {
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure custom local DNS", e) LogUtil.e(AppConfig.TAG, "Failed to configure custom local DNS", e)
return false return false
} }
return true return true
@@ -653,7 +736,7 @@ object V2rayConfigManager {
if (userHostsMap != null) hosts.putAll(userHostsMap) if (userHostsMap != null) hosts.putAll(userHostsMap)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e) LogUtil.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
} }
// DNS dns // DNS dns
@@ -679,7 +762,7 @@ object V2rayConfigManager {
) )
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure DNS", e) LogUtil.e(AppConfig.TAG, "Failed to configure DNS", e)
return false return false
} }
return true return true
@@ -710,8 +793,6 @@ object V2rayConfigManager {
} else { } else {
v2rayConfig.outbounds.add(outbound) v2rayConfig.outbounds.add(outbound)
} }
updateOutboundFragment(v2rayConfig)
return true return true
} }
@@ -764,7 +845,7 @@ object V2rayConfigManager {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure more outbounds", e) LogUtil.e(AppConfig.TAG, "Failed to configure more outbounds", e)
return false return false
} }
@@ -815,7 +896,7 @@ object V2rayConfigManager {
} else { } else {
outbound.settings?.address as List<*> outbound.settings?.address as List<*>
} }
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) != true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) != true) {
localTunAddr = listOf(localTunAddr.first()) localTunAddr = listOf(localTunAddr.first())
} }
outbound.settings?.address = localTunAddr outbound.settings?.address = localTunAddr
@@ -845,7 +926,7 @@ object V2rayConfigManager {
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to update outbound with global settings", e) LogUtil.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
return false return false
} }
return true return true
@@ -860,13 +941,13 @@ object V2rayConfigManager {
private fun getBalance(v2rayConfig: V2rayConfig, config: ProfileItem) { private fun getBalance(v2rayConfig: V2rayConfig, config: ProfileItem) {
try { try {
v2rayConfig.routing.rules.forEach { rule -> v2rayConfig.routing.rules.forEach { rule ->
if (rule.outboundTag == "proxy") { if (rule.outboundTag == AppConfig.TAG_PROXY) {
rule.outboundTag = null rule.outboundTag = null
rule.balancerTag = AppConfig.TAG_BALANCER rule.balancerTag = AppConfig.TAG_BALANCER
} }
} }
val lstSelector = listOf("proxy-") val lstSelector = listOf("proxy-")
when (config.policyGroupType) { when (config.policyGroupType) {
// Least Ping goto else // Least Ping goto else
"1" -> { "1" -> {
@@ -889,6 +970,7 @@ object V2rayConfigManager {
) )
) )
} }
"2" -> { "2" -> {
// Random // Random
val balancer = V2rayConfig.RoutingBean.BalancerBean( val balancer = V2rayConfig.RoutingBean.BalancerBean(
@@ -900,6 +982,7 @@ object V2rayConfigManager {
) )
v2rayConfig.routing.balancers = listOf(balancer) v2rayConfig.routing.balancers = listOf(balancer)
} }
"3" -> { "3" -> {
// Round Robin // Round Robin
val balancer = V2rayConfig.RoutingBean.BalancerBean( val balancer = V2rayConfig.RoutingBean.BalancerBean(
@@ -911,6 +994,7 @@ object V2rayConfigManager {
) )
v2rayConfig.routing.balancers = listOf(balancer) v2rayConfig.routing.balancers = listOf(balancer)
} }
else -> { else -> {
// Default: Least Ping // Default: Least Ping
val balancer = V2rayConfig.RoutingBean.BalancerBean( val balancer = V2rayConfig.RoutingBean.BalancerBean(
@@ -946,7 +1030,7 @@ object V2rayConfigManager {
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure balance", e) LogUtil.e(AppConfig.TAG, "Failed to configure balance", e)
} }
} }
@@ -955,70 +1039,82 @@ object V2rayConfigManager {
* *
* Configures packet fragmentation for TLS and REALITY protocols if enabled. * Configures packet fragmentation for TLS and REALITY protocols if enabled.
* *
* @param v2rayConfig The V2ray configuration object to be modified * @param streamSettings The streamSettings object to be modified
* @return true if fragment configuration was successful, false otherwise * @return true if fragment configuration was successful, false otherwise
*/ */
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean { private fun updateOutboundFragment(streamSettings: StreamSettingsBean): Boolean {
try { try {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
return true return true
} }
if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS if (streamSettings.security != AppConfig.TLS
&& v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY && streamSettings.security != AppConfig.REALITY
) { ) {
return true return true
} }
if (streamSettings.sockopt?.dialerProxy.isNotNullEmpty()) {
val fragmentOutbound = return true
OutboundBean( }
protocol = AppConfig.PROTOCOL_FREEDOM,
tag = AppConfig.TAG_FRAGMENT,
mux = null
)
var packets = var packets =
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello" MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY if (streamSettings.security == AppConfig.REALITY
&& packets == "tlshello" && packets == "tlshello"
) { ) {
packets = "1-3" packets = "1-3"
} else if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.TLS } else if (streamSettings.security == AppConfig.TLS
&& packets != "tlshello" && packets != "tlshello"
) { ) {
packets = "tlshello" packets = "tlshello"
} }
fragmentOutbound.settings = OutSettingsBean( val fragmentMask = StreamSettingsBean.FinalMaskBean.MaskBean(
fragment = OutSettingsBean.FragmentBean( type = "fragment",
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
packets = packets, packets = packets,
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH) length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
?: "50-100", ?: "50-100",
interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL) delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
?: "10-20" ?: "10-20"
), )
noises = listOf( )
OutSettingsBean.NoiseBean( val noiseMask = StreamSettingsBean.FinalMaskBean.MaskBean(
type = "rand", type = "noise",
packet = "10-20", settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
delay = "10-16", noise = listOf(
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean.NoiseMaskBean(
rand = "10-20",
delay = "10-16",
)
) )
),
)
fragmentOutbound.streamSettings = StreamSettingsBean(
sockopt = StreamSettingsBean.SockoptBean(
TcpNoDelay = true,
mark = 255
) )
) )
v2rayConfig.outbounds.add(fragmentOutbound)
//proxy chain val finalMaskObj = streamSettings.finalmask?.let { existingFinalMask ->
v2rayConfig.outbounds[0].streamSettings?.sockopt = JsonUtil.parseString(JsonUtil.toJson(existingFinalMask))
StreamSettingsBean.SockoptBean( } ?: com.google.gson.JsonObject()
dialerProxy = AppConfig.TAG_FRAGMENT
) // finalmask.tcp / finalmask.udp are arrays; prepend mask at index 0.
fun prependMask(scope: String, mask: StreamSettingsBean.FinalMaskBean.MaskBean) {
val current = finalMaskObj.get(scope)
if (current != null && current.isJsonArray && current.asJsonArray.size() > 0) {
return
}
val newArray = JsonArray()
newArray.add(JsonUtil.parseString(JsonUtil.toJson(mask)))
if (current != null && current.isJsonArray) {
current.asJsonArray.forEach { newArray.add(it) }
}
finalMaskObj.add(scope, newArray)
}
prependMask("tcp", fragmentMask)
prependMask("udp", noiseMask)
streamSettings.finalmask = finalMaskObj
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to update outbound fragment", e) LogUtil.e(AppConfig.TAG, "Failed to update outbound fragment", e)
return false return false
} }
return true return true
@@ -1174,7 +1270,7 @@ object V2rayConfigManager {
val authority = profileItem.authority val authority = profileItem.authority
val xhttpMode = profileItem.xhttpMode val xhttpMode = profileItem.xhttpMode
val xhttpExtra = profileItem.xhttpExtra val xhttpExtra = profileItem.xhttpExtra
val finalMask = profileItem.finalMask
var sni: String? = null var sni: String? = null
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type } streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
when (streamSettings.network) { when (streamSettings.network) {
@@ -1204,28 +1300,34 @@ object V2rayConfigManager {
headerType == "wechat-video" -> "header-wechat" headerType == "wechat-video" -> "header-wechat"
else -> "header-$headerType" else -> "header-$headerType"
} }
udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean( udpMaskList.add(
type = kcpHeaderType, StreamSettingsBean.FinalMaskBean.MaskBean(
settings = if (headerType == "dns" && !host.isNullOrEmpty()) { type = kcpHeaderType,
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean( settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
domain = host StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
) domain = host
} else { )
null } else {
} null
)) }
)
)
} }
if (seed.isNullOrEmpty()) { if (seed.isNullOrEmpty()) {
udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean( udpMaskList.add(
type = "mkcp-original" StreamSettingsBean.FinalMaskBean.MaskBean(
)) type = "mkcp-original"
} else {
udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean(
type = "mkcp-aes128gcm",
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
password = seed
) )
)) )
} else {
udpMaskList.add(
StreamSettingsBean.FinalMaskBean.MaskBean(
type = "mkcp-aes128gcm",
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
password = seed
)
)
)
} }
streamSettings.finalmask = StreamSettingsBean.FinalMaskBean( streamSettings.finalmask = StreamSettingsBean.FinalMaskBean(
udp = udpMaskList.toList() udp = udpMaskList.toList()
@@ -1290,21 +1392,69 @@ object V2rayConfigManager {
val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean( val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean(
version = 2, version = 2,
auth = profileItem.password.orEmpty(), auth = profileItem.password.orEmpty(),
up = profileItem.bandwidthUp?.ifEmpty { "0" }.orEmpty(),
down = profileItem.bandwidthDown?.ifEmpty { "0" }.orEmpty(),
udphop = null
) )
val quicParams = StreamSettingsBean.FinalMaskBean.QuicParamsBean(
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
)
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
if (profileItem.portHopping.isNotNullEmpty()) { if (profileItem.portHopping.isNotNullEmpty()) {
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean( val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
port = profileItem.portHopping, val interval = if (rawInterval == null) {
interval = profileItem.portHoppingInterval "30"
?.trim() } else {
?.toIntOrNull() val singleValue = rawInterval.toIntOrNull()
?.takeIf { it >= 5 } if (singleValue != null) {
?: 30 if (singleValue < 5) {
"30"
} else {
rawInterval
}
} else {
val parts = rawInterval.split('-')
if (parts.size == 2) {
val start = parts[0].trim().toIntOrNull()
val end = parts[1].trim().toIntOrNull()
if (start != null && end != null) {
val minStart = maxOf(5, start)
val minEnd = maxOf(minStart, end)
"$minStart-$minEnd"
} else {
"30"
}
} else {
"30"
}
}
}
quicParams.udpHop = StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
ports = profileItem.portHopping,
interval = interval
)
}
val finalmask = StreamSettingsBean.FinalMaskBean(
quicParams = quicParams
)
if (profileItem.obfsPassword.isNotNullEmpty()) {
finalmask.udp = listOf(
StreamSettingsBean.FinalMaskBean.MaskBean(
type = "salamander",
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
password = profileItem.obfsPassword.orEmpty()
)
)
) )
} }
streamSettings.hysteriaSettings = hysteriaSetting streamSettings.hysteriaSettings = hysteriaSetting
streamSettings.finalmask = finalmask
}
}
finalMask?.let {
val parsedFinalMask = JsonUtil.parseString(finalMask)
if (parsedFinalMask != null) {
streamSettings.finalmask = parsedFinalMask
} else {
LogUtil.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
} }
} }
return sni return sni
@@ -1338,7 +1488,7 @@ object V2rayConfigManager {
allowInsecure = allowInsecure, allowInsecure = allowInsecure,
serverName = sni.nullIfBlank(), serverName = sni.nullIfBlank(),
fingerprint = profileItem.fingerPrint.nullIfBlank(), fingerprint = profileItem.fingerPrint.nullIfBlank(),
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() }, alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
echConfigList = profileItem.echConfigList.nullIfBlank(), echConfigList = profileItem.echConfigList.nullIfBlank(),
echForceQuery = profileItem.echForceQuery.nullIfBlank(), echForceQuery = profileItem.echForceQuery.nullIfBlank(),
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(), pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
@@ -1354,6 +1504,10 @@ object V2rayConfigManager {
streamSettings.tlsSettings = null streamSettings.tlsSettings = null
streamSettings.realitySettings = tlsSetting streamSettings.realitySettings = tlsSetting
} }
if (profileItem.finalMask.isNullOrEmpty()) {
updateOutboundFragment(streamSettings)
}
} }
//endregion //endregion
@@ -1,8 +1,8 @@
package com.v2ray.ang.handler package com.v2ray.ang.handler
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.WebDavConfig import com.v2ray.ang.dto.WebDavConfig
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Credentials import okhttp3.Credentials
@@ -67,14 +67,14 @@ object WebDavManager {
cl.newCall(req).execute().use { resp -> cl.newCall(req).execute().use { resp ->
val success = resp.isSuccessful val success = resp.isSuccessful
if (success) { if (success) {
Log.i(AppConfig.TAG, "WebDAV upload success: $remote") LogUtil.i(AppConfig.TAG, "WebDAV upload success: $remote")
} else { } else {
Log.e(AppConfig.TAG, "WebDAV upload failed: $remote (HTTP ${resp.code})") LogUtil.e(AppConfig.TAG, "WebDAV upload failed: $remote (HTTP ${resp.code})")
} }
return@withContext success return@withContext success
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV upload exception: $remote", e) LogUtil.e(AppConfig.TAG, "WebDAV upload exception: $remote", e)
return@withContext false return@withContext false
} }
} }
@@ -93,7 +93,7 @@ object WebDavManager {
val req = applyAuth(Request.Builder().url(remote).get()).build() val req = applyAuth(Request.Builder().url(remote).get()).build()
cl.newCall(req).execute().use { resp -> cl.newCall(req).execute().use { resp ->
if (!resp.isSuccessful) { if (!resp.isSuccessful) {
Log.e(AppConfig.TAG, "WebDAV download failed: $remote (HTTP ${resp.code})") LogUtil.e(AppConfig.TAG, "WebDAV download failed: $remote (HTTP ${resp.code})")
return@withContext false return@withContext false
} }
@@ -104,11 +104,11 @@ object WebDavManager {
} }
} }
Log.i(AppConfig.TAG, "WebDAV download success: $remote") LogUtil.i(AppConfig.TAG, "WebDAV download success: $remote")
return@withContext true return@withContext true
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV download exception: $remote", e) LogUtil.e(AppConfig.TAG, "WebDAV download exception: $remote", e)
return@withContext false return@withContext false
} }
} }
@@ -169,7 +169,7 @@ object WebDavManager {
cl.newCall(req).execute().use { resp -> cl.newCall(req).execute().use { resp ->
// 201 Created or 405 Method Not Allowed (already exists) are acceptable // 201 Created or 405 Method Not Allowed (already exists) are acceptable
if (resp.code != 201 && resp.code != 405 && resp.code != 409) { if (resp.code != 201 && resp.code != 405 && resp.code != 409) {
Log.w(AppConfig.TAG, "WebDAV MKCOL $mkUrl returned ${resp.code}") LogUtil.w(AppConfig.TAG, "WebDAV MKCOL $mkUrl returned ${resp.code}")
} }
} }
} catch (_: Exception) { } catch (_: Exception) {
@@ -177,7 +177,7 @@ object WebDavManager {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV ensureRemoteDirs error", e) LogUtil.e(AppConfig.TAG, "WebDAV ensureRemoteDirs error", e)
} }
} }
} }
@@ -3,13 +3,13 @@ package com.v2ray.ang.helper
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.LogUtil
/** /**
* Helper for choosing and creating files using Android Storage Access Framework. * Helper for choosing and creating files using Android Storage Access Framework.
@@ -58,7 +58,7 @@ class FileChooserHelper(private val activity: AppCompatActivity) {
Intent.createChooser(intent, activity.getString(R.string.title_file_chooser)) Intent.createChooser(intent, activity.getString(R.string.title_file_chooser))
) )
} catch (ex: ActivityNotFoundException) { } catch (ex: ActivityNotFoundException) {
Log.e(AppConfig.TAG, "File chooser activity not found", ex) LogUtil.e(AppConfig.TAG, "File chooser activity not found", ex)
activity.toast(R.string.toast_require_file_manager) activity.toast(R.string.toast_require_file_manager)
fileChooserCallback?.invoke(null) fileChooserCallback?.invoke(null)
fileChooserCallback = null fileChooserCallback = null
@@ -79,7 +79,7 @@ class FileChooserHelper(private val activity: AppCompatActivity) {
try { try {
documentCreateLauncher.launch(fileName) documentCreateLauncher.launch(fileName)
} catch (ex: ActivityNotFoundException) { } catch (ex: ActivityNotFoundException) {
Log.e(AppConfig.TAG, "Document creator activity not found", ex) LogUtil.e(AppConfig.TAG, "Document creator activity not found", ex)
activity.toast(R.string.toast_require_file_manager) activity.toast(R.string.toast_require_file_manager)
documentCreateCallback?.invoke(null) documentCreateCallback?.invoke(null)
documentCreateCallback = null documentCreateCallback = null
@@ -5,6 +5,7 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.LogUtil
/** /**
* PreferenceDataStore implementation that bridges AndroidX Preference framework to MMKV storage. * PreferenceDataStore implementation that bridges AndroidX Preference framework to MMKV storage.
@@ -73,6 +74,10 @@ class MmkvPreferenceDataStore : PreferenceDataStore() {
// Internal helper: notify other modules about setting changes // Internal helper: notify other modules about setting changes
private fun notifySettingChanged(key: String) { private fun notifySettingChanged(key: String) {
if (key == AppConfig.PREF_LOGLEVEL) {
LogUtil.refreshLogLevel()
}
// Call SettingsManager.setNightMode if UI mode changed // Call SettingsManager.setNightMode if UI mode changed
if (key == AppConfig.PREF_UI_MODE_NIGHT) { if (key == AppConfig.PREF_UI_MODE_NIGHT) {
SettingsManager.setNightMode() SettingsManager.setNightMode()
@@ -3,10 +3,10 @@ package com.v2ray.ang.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.handler.V2RayServiceManager
import com.v2ray.ang.util.LogUtil
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
/** /**
@@ -18,24 +18,24 @@ class BootReceiver : BroadcastReceiver() {
* @param intent The Intent being received. * @param intent The Intent being received.
*/ */
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
Log.i(AppConfig.TAG, "BootReceiver received: ${intent?.action}") LogUtil.i(AppConfig.TAG, "BootReceiver received: ${intent?.action}")
if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) { if (context == null || intent?.action != Intent.ACTION_BOOT_COMPLETED) {
Log.w(AppConfig.TAG, "BootReceiver: Invalid context or action") LogUtil.w(AppConfig.TAG, "BootReceiver: Invalid context or action")
return return
} }
if (!MmkvManager.decodeStartOnBoot()) { if (!MmkvManager.decodeStartOnBoot()) {
Log.i(AppConfig.TAG, "BootReceiver: Auto-start on boot is disabled") LogUtil.i(AppConfig.TAG, "BootReceiver: Auto-start on boot is disabled")
return return
} }
if (MmkvManager.getSelectServer().isNullOrEmpty()) { if (MmkvManager.getSelectServer().isNullOrEmpty()) {
Log.w(AppConfig.TAG, "BootReceiver: No server selected") LogUtil.w(AppConfig.TAG, "BootReceiver: No server selected")
return return
} }
Log.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service") LogUtil.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service")
V2RayServiceManager.startVService(context) V2RayServiceManager.startVService(context)
} }
} }
@@ -6,6 +6,7 @@ import android.content.Intent
import android.text.TextUtils import android.text.TextUtils
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.handler.V2RayServiceManager
import com.v2ray.ang.util.LogUtil
class TaskerReceiver : BroadcastReceiver() { class TaskerReceiver : BroadcastReceiver() {
@@ -35,7 +36,7 @@ class TaskerReceiver : BroadcastReceiver() {
V2RayServiceManager.stopVService(context) V2RayServiceManager.stopVService(context)
} }
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e(AppConfig.TAG, "Error processing Tasker broadcast", e) LogUtil.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
} }
} }
} }
@@ -1,8 +1,8 @@
package com.v2ray.ang.service package com.v2ray.ang.service
import android.content.Context import android.content.Context
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -16,7 +16,7 @@ class ProcessService {
* @param cmd The command to run. * @param cmd The command to run.
*/ */
fun runProcess(context: Context, cmd: MutableList<String>) { fun runProcess(context: Context, cmd: MutableList<String>) {
Log.i(AppConfig.TAG, cmd.toString()) LogUtil.i(AppConfig.TAG, cmd.toString())
try { try {
val proBuilder = ProcessBuilder(cmd) val proBuilder = ProcessBuilder(cmd)
@@ -27,14 +27,14 @@ class ProcessService {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(50L) Thread.sleep(50L)
Log.i(AppConfig.TAG, "runProcess check") LogUtil.i(AppConfig.TAG, "runProcess check")
process?.waitFor() process?.waitFor()
Log.i(AppConfig.TAG, "runProcess exited") LogUtil.i(AppConfig.TAG, "runProcess exited")
} }
Log.i(AppConfig.TAG, process.toString()) LogUtil.i(AppConfig.TAG, process.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, e.toString(), e) LogUtil.e(AppConfig.TAG, e.toString(), e)
} }
} }
@@ -43,10 +43,10 @@ class ProcessService {
*/ */
fun stopProcess() { fun stopProcess() {
try { try {
Log.i(AppConfig.TAG, "runProcess destroy") LogUtil.i(AppConfig.TAG, "runProcess destroy")
process?.destroy() process?.destroy()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to destroy process", e) LogUtil.e(AppConfig.TAG, "Failed to destroy process", e)
} }
} }
} }
@@ -7,11 +7,11 @@ import android.content.IntentFilter
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.handler.V2RayServiceManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
@@ -63,7 +63,7 @@ class QSTileService : TileService() {
applicationContext.unregisterReceiver(mMsgReceive) applicationContext.unregisterReceiver(mMsgReceive)
mMsgReceive = null mMsgReceive = null
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to unregister receiver", e) LogUtil.e(AppConfig.TAG, "Failed to unregister receiver", e)
} }
} }
@@ -2,11 +2,11 @@ package com.v2ray.ang.service
import android.content.Context import android.content.Context
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.contracts.Tun2SocksControl import com.v2ray.ang.contracts.Tun2SocksControl
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.LogUtil
import java.io.File import java.io.File
/** /**
@@ -22,9 +22,11 @@ class TProxyService(
@JvmStatic @JvmStatic
@Suppress("FunctionName") @Suppress("FunctionName")
private external fun TProxyStartService(configPath: String, fd: Int) private external fun TProxyStartService(configPath: String, fd: Int)
@JvmStatic @JvmStatic
@Suppress("FunctionName") @Suppress("FunctionName")
private external fun TProxyStopService() private external fun TProxyStopService()
@JvmStatic @JvmStatic
@Suppress("FunctionName") @Suppress("FunctionName")
private external fun TProxyGetStats(): LongArray? private external fun TProxyGetStats(): LongArray?
@@ -38,32 +40,36 @@ class TProxyService(
* Starts the tun2socks process with the appropriate parameters. * Starts the tun2socks process with the appropriate parameters.
*/ */
override fun startTun2Socks() { override fun startTun2Socks() {
// Log.i(AppConfig.TAG, "Starting HevSocks5Tunnel via JNI") // LogUtil.i(AppConfig.TAG, "Starting HevSocks5Tunnel via JNI")
val configContent = buildConfig() val configContent = buildConfig()
val configFile = File(context.filesDir, "hev-socks5-tunnel.yaml").apply { val configFile = File(context.filesDir, "hev-socks5-tunnel.yaml").apply {
writeText(configContent) writeText(configContent)
} }
// Log.i(AppConfig.TAG, "Config file created: ${configFile.absolutePath}") // LogUtil.i(AppConfig.TAG, "Config file created: ${configFile.absolutePath}")
Log.d(AppConfig.TAG, "HevSocks5Tunnel Config content:\n$configContent") LogUtil.d(AppConfig.TAG, "HevSocks5Tunnel Config content:\n$configContent")
try { try {
// Log.i(AppConfig.TAG, "TProxyStartService...") // LogUtil.i(AppConfig.TAG, "TProxyStartService...")
TProxyStartService(configFile.absolutePath, vpnInterface.fd) TProxyStartService(configFile.absolutePath, vpnInterface.fd)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "HevSocks5Tunnel exception: ${e.message}") LogUtil.e(AppConfig.TAG, "HevSocks5Tunnel exception: ${e.message}")
} }
} }
private fun buildConfig(): String { private fun buildConfig(): String {
val socksPort = SettingsManager.getSocksPort() val socksPort = SettingsManager.getSocksPort()
val socksUsername = SettingsManager.getSocksUsername()
val socksPassword = SettingsManager.getSocksPassword()
val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig() val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
val escapedSocksUsername = socksUsername?.replace("'", "''")
val escapedSocksPassword = socksPassword?.replace("'", "''")
return buildString { return buildString {
appendLine("tunnel:") appendLine("tunnel:")
appendLine(" mtu: ${SettingsManager.getVpnMtu()}") appendLine(" mtu: ${SettingsManager.getVpnMtu()}")
appendLine(" ipv4: ${vpnConfig.ipv4Client}") appendLine(" ipv4: ${vpnConfig.ipv4Client}")
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED)) {
appendLine(" ipv6: '${vpnConfig.ipv6Client}'") appendLine(" ipv6: '${vpnConfig.ipv6Client}'")
} }
@@ -71,6 +77,10 @@ class TProxyService(
appendLine(" port: ${socksPort}") appendLine(" port: ${socksPort}")
appendLine(" address: ${AppConfig.LOOPBACK}") appendLine(" address: ${AppConfig.LOOPBACK}")
appendLine(" udp: 'udp'") appendLine(" udp: 'udp'")
if (escapedSocksUsername != null && escapedSocksPassword != null) {
appendLine(" username: '${escapedSocksUsername}'")
appendLine(" password: '${escapedSocksPassword}'")
}
// Read-write timeout settings // Read-write timeout settings
val timeoutSetting = MmkvManager.decodeSettingsString(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) ?: AppConfig.HEVTUN_RW_TIMEOUT val timeoutSetting = MmkvManager.decodeSettingsString(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) ?: AppConfig.HEVTUN_RW_TIMEOUT
@@ -92,10 +102,10 @@ class TProxyService(
*/ */
override fun stopTun2Socks() { override fun stopTun2Socks() {
try { try {
Log.i(AppConfig.TAG, "TProxyStopService...") LogUtil.i(AppConfig.TAG, "TProxyStopService...")
TProxyStopService() TProxyStopService()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to stop hev-socks5-tunnel", e) LogUtil.e(AppConfig.TAG, "Failed to stop hev-socks5-tunnel", e)
} }
} }
} }
@@ -4,11 +4,11 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.contracts.ServiceControl import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.handler.V2RayServiceManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.MyContextWrapper
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
@@ -18,7 +18,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.i(AppConfig.TAG, "StartCore-Proxy: Service created") LogUtil.i(AppConfig.TAG, "StartCore-Proxy: Service created")
V2RayServiceManager.serviceControl = SoftReference(this) V2RayServiceManager.serviceControl = SoftReference(this)
} }
@@ -30,7 +30,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
* @return The start mode. * @return The start mode.
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(AppConfig.TAG, "StartCore-Proxy: Service command received") LogUtil.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
V2RayServiceManager.startCoreLoop(null) V2RayServiceManager.startCoreLoop(null)
return START_STICKY return START_STICKY
} }
@@ -14,7 +14,6 @@ import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.os.StrictMode import android.os.StrictMode
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
@@ -25,6 +24,7 @@ import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.NotificationManager import com.v2ray.ang.handler.NotificationManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.handler.V2RayServiceManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MyContextWrapper import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
@@ -74,14 +74,14 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.i(AppConfig.TAG, "StartCore-VPN: Service created") LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service created")
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy) StrictMode.setThreadPolicy(policy)
V2RayServiceManager.serviceControl = SoftReference(this) V2RayServiceManager.serviceControl = SoftReference(this)
} }
override fun onRevoke() { override fun onRevoke() {
Log.w(AppConfig.TAG, "StartCore-VPN: Permission revoked") LogUtil.w(AppConfig.TAG, "StartCore-VPN: Permission revoked")
stopAllService() stopAllService()
} }
@@ -92,12 +92,27 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Log.i(AppConfig.TAG, "StartCore-VPN: Service destroyed") LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service destroyed")
// Ensure VPN interface is properly closed when the service is destroyed without
// going through stopAllService() (e.g. when killed unexpectedly). isRunning is
// set to false at the start of stopAllService(), so this guard prevents a double-close.
if (isRunning) {
try {
if (::mInterface.isInitialized) {
mInterface.close()
LogUtil.i(AppConfig.TAG, "StartCore-VPN: VPN interface closed in onDestroy")
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to close interface in onDestroy", e)
}
}
NotificationManager.cancelNotification() NotificationManager.cancelNotification()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(AppConfig.TAG, "StartCore-VPN: Service command received") LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service command received")
setupVpnService() setupVpnService()
startService() startService()
return START_STICKY return START_STICKY
@@ -110,11 +125,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
override fun startService() { override fun startService() {
if (!::mInterface.isInitialized) { if (!::mInterface.isInitialized) {
Log.e(AppConfig.TAG, "StartCore-VPN: Interface not initialized") LogUtil.e(AppConfig.TAG, "StartCore-VPN: Interface not initialized")
return return
} }
if (!V2RayServiceManager.startCoreLoop(mInterface)) { if (!V2RayServiceManager.startCoreLoop(mInterface)) {
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to start core loop") LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to start core loop")
stopAllService() stopAllService()
return return
} }
@@ -142,13 +157,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
private fun setupVpnService() { private fun setupVpnService() {
val prepare = prepare(this) val prepare = prepare(this)
if (prepare != null) { if (prepare != null) {
Log.e(AppConfig.TAG, "StartCore-VPN: Permission not granted") LogUtil.e(AppConfig.TAG, "StartCore-VPN: Permission not granted")
stopSelf() stopSelf()
return return
} }
if (configureVpnService() != true) { if (configureVpnService() != true) {
Log.e(AppConfig.TAG, "StartCore-VPN: Configuration failed") LogUtil.e(AppConfig.TAG, "StartCore-VPN: Configuration failed")
stopSelf() stopSelf()
return return
} }
@@ -175,7 +190,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
mInterface.close() mInterface.close()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(AppConfig.TAG, "Failed to close old interface", e) LogUtil.w(AppConfig.TAG, "Failed to close old interface", e)
} }
// Configure platform-specific features // Configure platform-specific features
@@ -187,7 +202,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
isRunning = true isRunning = true
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to establish VPN interface", e) LogUtil.e(AppConfig.TAG, "Failed to establish VPN interface", e)
stopAllService() stopAllService()
} }
return false return false
@@ -218,7 +233,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
} }
// Configure IPv6 if enabled // Configure IPv6 if enabled
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) == true) {
builder.addAddress(vpnConfig.ipv6Client, 126) builder.addAddress(vpnConfig.ipv6Client, 126)
if (bypassLan) { if (bypassLan) {
builder.addRoute("2000::", 3) // Currently only 1/8 of total IPv6 is in use builder.addRoute("2000::", 3) // Currently only 1/8 of total IPv6 is in use
@@ -252,7 +267,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
try { try {
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback) connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to request network", e) LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to request network", e)
} }
} }
@@ -305,7 +320,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
builder.addAllowedApplication(it) builder.addAllowedApplication(it)
} }
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to configure app", e) LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to configure app", e)
} }
} }
} }
@@ -339,7 +354,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
try { try {
connectivity.unregisterNetworkCallback(defaultNetworkCallback) connectivity.unregisterNetworkCallback(defaultNetworkCallback)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(AppConfig.TAG, "StartCore-VPN: Failed to unregister callback", e) LogUtil.w(AppConfig.TAG, "StartCore-VPN: Failed to unregister callback", e)
} }
} }
@@ -356,12 +371,22 @@ class V2RayVpnService : VpnService(), ServiceControl {
//which means the first v2ray core somehow failed to stop and release the port. //which means the first v2ray core somehow failed to stop and release the port.
stopSelf() stopSelf()
// Add a small delay to allow the async core stop operation to complete
// before closing the VPN interface, preventing a race condition that can
// leave the VPN icon in the status bar after stopping the service.
try {
Thread.sleep(100)
} catch (e: InterruptedException) {
LogUtil.w(AppConfig.TAG, "StartCore-VPN: Sleep interrupted", e)
}
try { try {
if (::mInterface.isInitialized) { if (::mInterface.isInitialized) {
mInterface.close() mInterface.close()
LogUtil.i(AppConfig.TAG, "StartCore-VPN: VPN interface closed")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-VPN: Failed to close interface", e) LogUtil.e(AppConfig.TAG, "StartCore-VPN: Failed to close interface", e)
} }
} }
} }
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
@@ -20,6 +19,7 @@ import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.WebDavManager import com.v2ray.ang.handler.WebDavManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.ZipUtil import com.v2ray.ang.util.ZipUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -38,7 +38,7 @@ class BackupActivity : HelperBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
//setContentView(binding.root) //setContentView(binding.root)
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_configuration_backup_restore)) setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_configuration_backup_restore))
binding.layoutBackup.setOnClickListener { binding.layoutBackup.setOnClickListener {
@@ -149,7 +149,7 @@ class BackupActivity : HelperBaseActivity() {
toastError(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error during file restore", e) LogUtil.e(AppConfig.TAG, "Error during file restore", e)
toastError(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
@@ -180,7 +180,7 @@ class BackupActivity : HelperBaseActivity() {
toastError(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to backup configuration", e) LogUtil.e(AppConfig.TAG, "Failed to backup configuration", e)
toastError(R.string.toast_failure) toastError(R.string.toast_failure)
} }
} }
@@ -217,7 +217,7 @@ class BackupActivity : HelperBaseActivity() {
val ok = try { val ok = try {
WebDavManager.uploadFile(tempFile, WEBDAV_BACKUP_FILE_NAME) WebDavManager.uploadFile(tempFile, WEBDAV_BACKUP_FILE_NAME)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV upload error", e) LogUtil.e(AppConfig.TAG, "WebDAV upload error", e)
false false
} }
@@ -225,7 +225,7 @@ class BackupActivity : HelperBaseActivity() {
if (ok) toastSuccess(R.string.toast_success) else toastError(R.string.toast_failure) if (ok) toastSuccess(R.string.toast_success) else toastError(R.string.toast_failure)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV backup error", e) LogUtil.e(AppConfig.TAG, "WebDAV backup error", e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
toastError(R.string.toast_failure) toastError(R.string.toast_failure)
} }
@@ -272,7 +272,7 @@ class BackupActivity : HelperBaseActivity() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "WebDAV download error", e) LogUtil.e(AppConfig.TAG, "WebDAV download error", e)
withContext(Dispatchers.Main) { toastError(R.string.toast_failure) } withContext(Dispatchers.Main) { toastError(R.string.toast_failure) }
} finally { } finally {
try { try {
@@ -39,7 +39,7 @@ abstract class BaseFragment<VB : ViewBinding> : Fragment() {
* @param drawableResId The resource ID of the drawable to be used as the divider. * @param drawableResId The resource ID of the drawable to be used as the divider.
* @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL). * @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL).
*/ */
fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) { fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) {
// Get the drawable from resources // Get the drawable from resources
val drawable = ContextCompat.getDrawable(requireContext(), drawableResId) val drawable = ContextCompat.getDrawable(requireContext(), drawableResId)
requireNotNull(drawable) { "Drawable resource not found" } requireNotNull(drawable) { "Drawable resource not found" }
@@ -1,7 +1,6 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
@@ -15,6 +14,7 @@ import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.UpdateCheckerManager import com.v2ray.ang.handler.UpdateCheckerManager
import com.v2ray.ang.handler.V2RayNativeManager import com.v2ray.ang.handler.V2RayNativeManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -56,10 +56,9 @@ class CheckUpdateActivity : BaseActivity() {
toastSuccess(R.string.update_already_latest_version) toastSuccess(R.string.update_already_latest_version)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}") LogUtil.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
toastError(e.message ?: getString(R.string.toast_failure)) toastError(e.message ?: getString(R.string.toast_failure))
} } finally {
finally {
hideLoading() hideLoading()
} }
} }
@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -11,6 +10,7 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.contracts.MainAdapterListener import com.v2ray.ang.contracts.MainAdapterListener
@@ -24,11 +24,13 @@ import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.viewmodel.MainViewModel import com.v2ray.ang.viewmodel.MainViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() { class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
SwipeRefreshLayout.OnRefreshListener {
private val ownerActivity: MainActivity private val ownerActivity: MainActivity
get() = requireActivity() as MainActivity get() = requireActivity() as MainActivity
private val mainViewModel: MainViewModel by activityViewModels() private val mainViewModel: MainViewModel by activityViewModels()
@@ -68,15 +70,20 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false)) itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false))
itemTouchHelper?.attachToRecyclerView(binding.recyclerView) itemTouchHelper?.attachToRecyclerView(binding.recyclerView)
binding.refreshLayout.isEnabled = false
// binding.refreshLayout.setOnRefreshListener(this)
// // Set the distance to trigger sync to 160dp
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index -> mainViewModel.updateListAction.observe(viewLifecycleOwner) { index ->
if (mainViewModel.subscriptionId != subId) { if (mainViewModel.subscriptionId != subId) {
return@observe return@observe
} }
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId") // LogUtil.d(TAG, "GroupServerFragment updateListAction subId=$subId")
adapter.setData(mainViewModel.serversCache, index) adapter.setData(mainViewModel.serversCache, index)
} }
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId") // LogUtil.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
} }
override fun onResume() { override fun onResume() {
@@ -105,7 +112,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
else -> ownerActivity.toast("else") else -> ownerActivity.toast("else")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error when sharing server", e) LogUtil.e(AppConfig.TAG, "Error when sharing server", e)
} }
}.show() }.show()
} }
@@ -142,7 +149,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
* @param guid The server unique identifier * @param guid The server unique identifier
*/ */
private fun shareFullContent(guid: String) { private fun shareFullContent(guid: String) {
ownerActivity.lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val result = AngConfigManager.shareFullContent2Clipboard(ownerActivity, guid) val result = AngConfigManager.shareFullContent2Clipboard(ownerActivity, guid)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
if (result == 0) { if (result == 0) {
@@ -212,7 +219,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
* @param position The position in the list * @param position The position in the list
*/ */
private fun removeServerSub(guid: String, position: Int) { private fun removeServerSub(guid: String, position: Int) {
ownerActivity.mainViewModel.removeServer(guid) mainViewModel.removeServer(guid)
adapter.removeServerSub(guid, position) adapter.removeServerSub(guid, position)
} }
@@ -271,4 +278,43 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
shareServer(guid, profile, position, shareOptions, skip) shareServer(guid, profile, position, shareOptions, skip)
} }
} }
override fun onRefresh() {
ownerActivity.importConfigViaSub()
//binding.refreshLayout.isRefreshing = false
}
/**
* Scrolls to the currently selected server in the RecyclerView
*/
fun scrollToSelectedServer() {
val selectedGuid = MmkvManager.getSelectServer()
if (selectedGuid.isNullOrEmpty()) {
ownerActivity.toast(R.string.title_file_chooser)
return
}
// Find the position of the selected server
val serversCache = mainViewModel.serversCache
val position = serversCache.indexOfFirst { it.guid == selectedGuid }
val recyclerView = binding.recyclerView
if (position >= 0) {
// Get the layout manager
val layoutManager = recyclerView.layoutManager as? GridLayoutManager
if (layoutManager != null) {
// Scroll to position with offset to center it on screen
// First scroll to position, then adjust to center
recyclerView.post {
layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 3)
}
} else {
// Fallback to smooth scroll if layout manager is not GridLayoutManager
recyclerView.smoothScrollToPosition(position)
}
} else {
ownerActivity.toast(R.string.toast_server_not_found_in_group)
}
}
} }
@@ -20,9 +20,9 @@ import com.v2ray.ang.helper.QRCodeScannerHelper
* - Provide QR code scanning helpers with camera permission handling. * - Provide QR code scanning helpers with camera permission handling.
*/ */
abstract class HelperBaseActivity : BaseActivity() { abstract class HelperBaseActivity : BaseActivity() {
private lateinit var fileChooser : FileChooserHelper private lateinit var fileChooser: FileChooserHelper
private lateinit var permissionRequester : PermissionHelper private lateinit var permissionRequester: PermissionHelper
private lateinit var qrCodeScanner : QRCodeScannerHelper private lateinit var qrCodeScanner: QRCodeScannerHelper
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -1,11 +1,11 @@
package com.v2ray.ang.ui package com.v2ray.ang.ui
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding import com.v2ray.ang.databinding.ItemRecyclerLogcatBinding
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.viewmodel.LogcatViewModel import com.v2ray.ang.viewmodel.LogcatViewModel
class LogcatRecyclerAdapter( class LogcatRecyclerAdapter(
@@ -33,7 +33,7 @@ class LogcatRecyclerAdapter(
onLongClick?.invoke(log) ?: false onLongClick?.invoke(log) ?: false
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error binding log view data", e) LogUtil.e(AppConfig.TAG, "Error binding log view data", e)
} }
} }
@@ -5,7 +5,6 @@ import android.content.res.ColorStateList
import android.net.Uri import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -33,6 +32,7 @@ import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.V2RayServiceManager import com.v2ray.ang.handler.V2RayServiceManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel import com.v2ray.ang.viewmodel.MainViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -179,7 +179,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
binding.tvTestState.text = content binding.tvTestState.text = content
} }
private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) { private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) {
if (isLoading) { if (isLoading) {
binding.fab.setImageResource(R.drawable.ic_fab_check) binding.fab.setImageResource(R.drawable.ic_fab_check)
return return
@@ -339,6 +339,10 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
true true
} }
R.id.locate_selected_config -> {
locateSelectedServer()
true
}
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
@@ -381,7 +385,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
val clipboard = Utils.getClipboard(this) val clipboard = Utils.getClipboard(this)
importBatchConfig(clipboard) importBatchConfig(clipboard)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to import config from clipboard", e) LogUtil.e(AppConfig.TAG, "Failed to import config from clipboard", e)
return false return false
} }
return true return true
@@ -411,7 +415,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
toastError(R.string.toast_failure) toastError(R.string.toast_failure)
hideLoading() hideLoading()
} }
Log.e(AppConfig.TAG, "Failed to import batch config", e) LogUtil.e(AppConfig.TAG, "Failed to import batch config", e)
} }
} }
} }
@@ -423,7 +427,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
try { try {
showFileChooser() showFileChooser()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to import config from local file", e) LogUtil.e(AppConfig.TAG, "Failed to import config from local file", e)
return false return false
} }
return true return true
@@ -433,7 +437,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
/** /**
* import config from sub * import config from sub
*/ */
private fun importConfigViaSub(): Boolean { fun importConfigViaSub(): Boolean {
showLoading() showLoading()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@@ -565,7 +569,48 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
importBatchConfig(input?.bufferedReader()?.readText()) importBatchConfig(input?.bufferedReader()?.readText())
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to read content from URI", e) LogUtil.e(AppConfig.TAG, "Failed to read content from URI", e)
}
}
/**
* Locates and scrolls to the currently selected server.
* If the selected server is in a different group, automatically switches to that group first.
*/
private fun locateSelectedServer() {
val targetSubscriptionId = mainViewModel.findSubscriptionIdBySelect()
if (targetSubscriptionId.isNullOrEmpty()) {
toast(R.string.title_file_chooser)
return
}
val targetGroupIndex = groupPagerAdapter.groups.indexOfFirst { it.id == targetSubscriptionId }
if (targetGroupIndex < 0) {
toast(R.string.toast_server_not_found_in_group)
return
}
// Switch to target group if needed, then scroll to the server
if (binding.viewPager.currentItem != targetGroupIndex) {
binding.viewPager.setCurrentItem(targetGroupIndex, true)
binding.viewPager.postDelayed({ scrollToSelectedServer(targetGroupIndex) }, 1000)
} else {
scrollToSelectedServer(targetGroupIndex)
}
}
/**
* Scrolls to the selected server in the specified fragment.
* @param groupIndex The index of the group/fragment to scroll in
*/
private fun scrollToSelectedServer(groupIndex: Int) {
val itemId = groupPagerAdapter.getItemId(groupIndex)
val fragment = supportFragmentManager.findFragmentByTag("f$itemId") as? GroupServerFragment
if (fragment?.isAdded == true && fragment.view != null) {
fragment.scrollToSelectedServer()
} else {
toast(R.string.toast_fragment_not_available)
} }
} }
@@ -113,7 +113,7 @@ class MainRecyclerAdapter(
adapterListener?.onSelectServer(guid) adapterListener?.onSelectServer(guid)
} }
} }
} }
/** /**
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
@@ -23,6 +22,7 @@ import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.AppManagerUtil import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.PerAppProxyViewModel import com.v2ray.ang.viewmodel.PerAppProxyViewModel
import es.dmoral.toasty.Toasty import es.dmoral.toasty.Toasty
@@ -99,7 +99,7 @@ class PerAppProxyActivity : BaseActivity() {
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
} catch (e: Exception) { } catch (e: Exception) {
Log.e(ANG_PACKAGE, "Error loading apps", e) LogUtil.e(ANG_PACKAGE, "Error loading apps", e)
} finally { } finally {
hideLoading() hideLoading()
} }
@@ -133,6 +133,7 @@ class PerAppProxyActivity : BaseActivity() {
allowPerAppProxy() allowPerAppProxy()
true true
} }
R.id.invert_selection -> { R.id.invert_selection -> {
invertSelection() invertSelection()
allowPerAppProxy() allowPerAppProxy()
@@ -194,7 +195,7 @@ class PerAppProxyActivity : BaseActivity() {
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: "" content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
} }
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
//Log.i(AppConfig.TAG, content) //LogUtil.i(AppConfig.TAG, content)
selectProxyApp(content, true) selectProxyApp(content, true)
toastSuccess(R.string.toast_success) toastSuccess(R.string.toast_success)
hideLoading() hideLoading()
@@ -258,7 +259,7 @@ class PerAppProxyActivity : BaseActivity() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error selecting proxy app", e) LogUtil.e(AppConfig.TAG, "Error selecting proxy app", e)
return false return false
} }
return true return true
@@ -11,7 +11,7 @@ import com.v2ray.ang.viewmodel.PerAppProxyViewModel
class PerAppProxyAdapter( class PerAppProxyAdapter(
val apps: List<AppInfo>, val apps: List<AppInfo>,
val viewModel: PerAppProxyViewModel val viewModel: PerAppProxyViewModel
) :RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() { ) : RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
companion object { companion object {
private const val VIEW_TYPE_HEADER = 0 private const val VIEW_TYPE_HEADER = 0
@@ -3,14 +3,18 @@ package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingEditBinding import com.v2ray.ang.databinding.ActivityRoutingEditBinding
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.nullIfBlank import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -20,21 +24,49 @@ class RoutingEditActivity : BaseActivity() {
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) } private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
private val position by lazy { intent.getIntExtra("position", -1) } private val position by lazy { intent.getIntExtra("position", -1) }
private val outbound_tag: Array<out String> by lazy {
resources.getStringArray(R.array.outbound_tag)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
//setContentView(binding.root)
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title)) setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title))
setupOutboundTagInput()
val rulesetItem = SettingsManager.getRoutingRuleset(position) val rulesetItem = SettingsManager.getRoutingRuleset(position)
if (rulesetItem != null) { if (rulesetItem != null) {
bindingServer(rulesetItem) bindingServer(rulesetItem)
} else { } else {
clearServer() clearServer()
} }
SettingsManager.canUseProcessRouting().let { canUse ->
binding.etProcess.isEnabled = canUse
}
}
/**
* Sets up the AutoCompleteTextView for outbound tag:
* suggestions = built-in tags (proxy/direct/block) + all existing profile remarks.
* The dropdown button triggers showing the full list without typing.
*/
private fun setupOutboundTagInput() {
val profileRemarks = MmkvManager.decodeAllServerList()
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
.filter { it.isNotBlank() }
val suggestions = (BUILTIN_OUTBOUND_TAGS.toList() + profileRemarks).distinct()
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
binding.spOutboundTag.setAdapter(adapter)
// threshold=0 means show all suggestions even before typing; still need focus+request
binding.spOutboundTag.threshold = 0
// Dropdown arrow button shows the full suggestion list
binding.btnOutboundTagDropdown.setOnClickListener {
binding.spOutboundTag.requestFocus()
binding.spOutboundTag.showDropDown()
}
// Also show on field click when it already has focus
binding.spOutboundTag.setOnClickListener {
binding.spOutboundTag.showDropDown()
}
} }
private fun bindingServer(rulesetItem: RulesetItem): Boolean { private fun bindingServer(rulesetItem: RulesetItem): Boolean {
@@ -42,18 +74,18 @@ class RoutingEditActivity : BaseActivity() {
binding.chkLocked.isChecked = rulesetItem.locked == true binding.chkLocked.isChecked = rulesetItem.locked == true
binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(",")) binding.etDomain.text = Utils.getEditable(rulesetItem.domain?.joinToString(","))
binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(",")) binding.etIp.text = Utils.getEditable(rulesetItem.ip?.joinToString(","))
binding.etProcess.text = Utils.getEditable(rulesetItem.process?.joinToString(","))
binding.etPort.text = Utils.getEditable(rulesetItem.port) binding.etPort.text = Utils.getEditable(rulesetItem.port)
binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(",")) binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
binding.etNetwork.text = Utils.getEditable(rulesetItem.network) binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag) // Set text directly; filter won't fire because we're not using setText(filter=true)
binding.spOutboundTag.setSelection(outbound) binding.spOutboundTag.setText(rulesetItem.outboundTag, false)
return true return true
} }
private fun clearServer(): Boolean { private fun clearServer(): Boolean {
binding.etRemarks.text = null binding.etRemarks.text = null
binding.spOutboundTag.setSelection(0) binding.spOutboundTag.setText(BUILTIN_OUTBOUND_TAGS.first(), false)
return true return true
} }
@@ -65,10 +97,11 @@ class RoutingEditActivity : BaseActivity() {
locked = binding.chkLocked.isChecked locked = binding.chkLocked.isChecked
domain = binding.etDomain.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } domain = binding.etDomain.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
ip = binding.etIp.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } ip = binding.etIp.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
process = binding.etProcess.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
protocol = binding.etProtocol.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() } protocol = binding.etProtocol.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
port = binding.etPort.text.toString().nullIfBlank() port = binding.etPort.text.toString().nullIfBlank()
network = binding.etNetwork.text.toString().nullIfBlank() network = binding.etNetwork.text.toString().nullIfBlank()
outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition] outboundTag = binding.spOutboundTag.text.toString().trim().ifEmpty { TAG_PROXY }
} }
if (rulesetItem.remarks.isNullOrEmpty()) { if (rulesetItem.remarks.isNullOrEmpty()) {
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -21,6 +20,7 @@ import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -92,7 +92,7 @@ class RoutingSettingActivity : HelperBaseActivity() {
MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value) MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
binding.tvDomainStrategySummary.text = value binding.tvDomainStrategySummary.text = value
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to set domain strategy", e) LogUtil.e(AppConfig.TAG, "Failed to set domain strategy", e)
} }
}.show() }.show()
} }
@@ -110,7 +110,7 @@ class RoutingSettingActivity : HelperBaseActivity() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to import predefined ruleset", e) LogUtil.e(AppConfig.TAG, "Failed to import predefined ruleset", e)
} }
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
@@ -126,7 +126,7 @@ class RoutingSettingActivity : HelperBaseActivity() {
val clipboard = try { val clipboard = try {
Utils.getClipboard(this) Utils.getClipboard(this)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get clipboard content", e) LogUtil.e(AppConfig.TAG, "Failed to get clipboard content", e)
toastError(R.string.toast_failure) toastError(R.string.toast_failure)
return@setPositiveButton return@setPositiveButton
} }
@@ -25,7 +25,7 @@ class RoutingSettingRecyclerAdapter(
val ruleset = rulesets[position] val ruleset = rulesets[position]
holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks holder.itemRoutingSettingBinding.remarks.text = ruleset.remarks
holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.port)?.toString() holder.itemRoutingSettingBinding.domainIp.text = (ruleset.domain ?: ruleset.ip ?: ruleset.process ?: ruleset.port)?.toString()
holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag holder.itemRoutingSettingBinding.outboundTag.text = ruleset.outboundTag
holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled holder.itemRoutingSettingBinding.chkEnable.isChecked = ruleset.enabled
holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true holder.itemRoutingSettingBinding.imgLocked.isVisible = ruleset.locked == true
@@ -0,0 +1,20 @@
package com.v2ray.ang.ui
import android.os.Bundle
import com.v2ray.ang.R
import com.v2ray.ang.handler.V2RayServiceManager
class ScStartActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
moveTaskToBack(true)
setContentView(R.layout.activity_none)
if (!V2RayServiceManager.isRunning()) {
V2RayServiceManager.startVServiceFromToggle(this)
}
finish()
}
}
@@ -0,0 +1,19 @@
package com.v2ray.ang.ui
import android.os.Bundle
import com.v2ray.ang.R
import com.v2ray.ang.handler.V2RayServiceManager
class ScStopActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
moveTaskToBack(true)
setContentView(R.layout.activity_none)
if (V2RayServiceManager.isRunning()) {
V2RayServiceManager.stopVService(this)
}
finish()
}
}
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
@@ -11,6 +10,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityNoneBinding import com.v2ray.ang.databinding.ActivityNoneBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.QRCodeDecoder
import io.github.g00fy2.quickie.QRResult import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanCustomCode import io.github.g00fy2.quickie.ScanCustomCode
@@ -18,7 +18,7 @@ import io.github.g00fy2.quickie.config.BarcodeFormat
import io.github.g00fy2.quickie.config.ScannerConfig import io.github.g00fy2.quickie.config.ScannerConfig
class ScannerActivity : HelperBaseActivity() { class ScannerActivity : HelperBaseActivity() {
private val binding by lazy { ActivityNoneBinding.inflate(layoutInflater) } private val binding by lazy { ActivityNoneBinding.inflate(layoutInflater) }
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult) private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
@@ -95,7 +95,7 @@ class ScannerActivity : HelperBaseActivity() {
finished(text) finished(text)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e) LogUtil.e(AppConfig.TAG, "Failed to decode QR code from file", e)
toast(R.string.toast_decoding_failed) toast(R.string.toast_decoding_failed)
} }
} }
@@ -24,6 +24,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
@@ -132,6 +133,7 @@ class ServerActivity : BaseActivity() {
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) } private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) } private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) } private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
private val et_fm: EditText? by lazy { findViewById(R.id.et_fm) }
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) } private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
private val et_ech_config_list: EditText? by lazy { findViewById(R.id.et_ech_config_list) } private val et_ech_config_list: EditText? by lazy { findViewById(R.id.et_ech_config_list) }
private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) } private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
@@ -241,6 +243,7 @@ class ServerActivity : BaseActivity() {
else -> null else -> null
}.orEmpty() }.orEmpty()
) )
et_fm?.text = Utils.getEditable(config?.finalMask)
layout_extra?.visibility = layout_extra?.visibility =
when (networks[position]) { when (networks[position]) {
@@ -489,6 +492,13 @@ class ServerActivity : BaseActivity() {
} }
} }
if (et_fm?.text?.toString().isNotNullEmpty()) {
if (JsonUtil.parseString(et_fm?.text?.toString()) == null) {
toast(R.string.server_lab_final_mask)
return false
}
}
saveCommon(config) saveCommon(config)
saveStreamSettings(config) saveStreamSettings(config)
saveTls(config) saveTls(config)
@@ -498,7 +508,7 @@ class ServerActivity : BaseActivity() {
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty() config.subscriptionId = subscriptionId.orEmpty()
} }
//Log.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "") //LogUtil.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
toastSuccess(R.string.toast_success) toastSuccess(R.string.toast_success)
finish() finish()
@@ -533,7 +543,7 @@ class ServerActivity : BaseActivity() {
} else if (config.configType == EConfigType.HYSTERIA2) { } else if (config.configType == EConfigType.HYSTERIA2) {
config.obfsPassword = et_obfs_password?.text?.toString() config.obfsPassword = et_obfs_password?.text?.toString()
config.portHopping = et_port_hop?.text?.toString() config.portHopping = et_port_hop?.text?.toString()
config.portHoppingInterval = et_port_hop_interval?.text?.toString() config.portHoppingInterval = et_port_hop_interval?.text?.toString()?.trim()
config.bandwidthDown = et_bandwidth_down?.text?.toString() config.bandwidthDown = et_bandwidth_down?.text?.toString()
config.bandwidthUp = et_bandwidth_up?.text?.toString() config.bandwidthUp = et_bandwidth_up?.text?.toString()
} }
@@ -557,7 +567,8 @@ class ServerActivity : BaseActivity() {
profileItem.serviceName = path profileItem.serviceName = path
profileItem.authority = requestHost profileItem.authority = requestHost
profileItem.xhttpMode = transportTypes(networks[network])[type] profileItem.xhttpMode = transportTypes(networks[network])[type]
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim() profileItem.xhttpExtra = et_extra?.text?.toString()?.trim().nullIfBlank()
profileItem.finalMask = et_fm?.text?.toString()?.trim()?.nullIfBlank()
} }
private fun saveTls(config: ProfileItem) { private fun saveTls(config: ProfileItem) {
@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@@ -18,6 +17,7 @@ import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
class ServerCustomConfigActivity : BaseActivity() { class ServerCustomConfigActivity : BaseActivity() {
@@ -79,7 +79,7 @@ class ServerCustomConfigActivity : BaseActivity() {
val profileItem = try { val profileItem = try {
CustomFmt.parse(binding.editor.text.toString()) CustomFmt.parse(binding.editor.text.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse custom configuration", e) LogUtil.e(AppConfig.TAG, "Failed to parse custom configuration", e)
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}") toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
return false return false
} }
@@ -97,7 +97,7 @@ class ServerGroupActivity : BaseActivity() {
config.subscriptionId = subscriptionId.orEmpty() config.subscriptionId = subscriptionId.orEmpty()
} }
config.description = "${binding.spPolicyGroupType.selectedItem} - ${binding.spPolicyGroupSubId.selectedItem} - ${config.policyGroupFilter}" config.description = "${binding.spPolicyGroupType.selectedItem} - ${binding.spPolicyGroupSubId.selectedItem} - ${config.policyGroupFilter}"
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
toastSuccess(R.string.toast_success) toastSuccess(R.string.toast_success)
@@ -51,6 +51,8 @@ class SettingsActivity : BaseActivity() {
private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) } private val autoUpdateCheck by lazy { findPreference<CheckBoxPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE) }
private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) } private val autoUpdateInterval by lazy { findPreference<EditTextPreference>(AppConfig.SUBSCRIPTION_AUTO_UPDATE_INTERVAL) }
private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) } private val mode by lazy { findPreference<ListPreference>(AppConfig.PREF_MODE) }
private val socksPort by lazy { findPreference<EditTextPreference>(AppConfig.PREF_SOCKS_PORT) }
private val dynamicSocksPort by lazy { findPreference<CheckBoxPreference>(AppConfig.PREF_DYNAMIC_SOCKS_PORT) }
private val hevTunLogLevel by lazy { findPreference<ListPreference>(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) } private val hevTunLogLevel by lazy { findPreference<ListPreference>(AppConfig.PREF_HEV_TUNNEL_LOGLEVEL) }
private val hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) } private val hevTunRwTimeout by lazy { findPreference<EditTextPreference>(AppConfig.PREF_HEV_TUNNEL_RW_TIMEOUT) }
@@ -112,15 +114,28 @@ class SettingsActivity : BaseActivity() {
updateHevTunSettings(newValue as Boolean) updateHevTunSettings(newValue as Boolean)
true true
} }
dynamicSocksPort?.setOnPreferenceChangeListener { _, newValue ->
updateDynamicSocksPort(newValue as Boolean)
true
}
} }
private fun initPreferenceSummaries() { private fun initPreferenceSummaries() {
fun updateSummary(pref: androidx.preference.Preference) { fun updateSummary(pref: androidx.preference.Preference) {
when (pref) { when (pref) {
is EditTextPreference -> { is EditTextPreference -> {
pref.summary = pref.text.orEmpty() if (pref.key == AppConfig.PREF_SOCKS_PASSWORD) {
pref.summary = if (pref.text.isNullOrEmpty()) "" else "******"
} else {
pref.summary = pref.text.orEmpty()
}
pref.setOnPreferenceChangeListener { p, newValue -> pref.setOnPreferenceChangeListener { p, newValue ->
p.summary = (newValue as? String).orEmpty() if (p.key == AppConfig.PREF_SOCKS_PASSWORD) {
p.summary = if ((newValue as? String).isNullOrEmpty()) "" else "******"
} else {
p.summary = (newValue as? String).orEmpty()
}
true true
} }
} }
@@ -167,6 +182,8 @@ class SettingsActivity : BaseActivity() {
// Initialize auto-update interval state // Initialize auto-update interval state
autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false) autoUpdateInterval?.isEnabled = MmkvManager.decodeSettingsBool(AppConfig.SUBSCRIPTION_AUTO_UPDATE, false)
updateDynamicSocksPort(MmkvManager.decodeSettingsBool(AppConfig.PREF_DYNAMIC_SOCKS_PORT, false))
} }
private fun updateMode(value: String?) { private fun updateMode(value: String?) {
@@ -258,6 +275,10 @@ class SettingsActivity : BaseActivity() {
fragmentInterval?.isEnabled = enabled fragmentInterval?.isEnabled = enabled
} }
private fun updateDynamicSocksPort(enabled: Boolean) {
socksPort?.isEnabled = !enabled
}
private fun updateHevTunSettings(enabled: Boolean) { private fun updateHevTunSettings(enabled: Boolean) {
hevTunLogLevel?.isEnabled = enabled hevTunLogLevel?.isEnabled = enabled
hevTunRwTimeout?.isEnabled = enabled hevTunRwTimeout?.isEnabled = enabled
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -21,6 +20,7 @@ import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.QRCodeDecoder import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.SubscriptionsViewModel import com.v2ray.ang.viewmodel.SubscriptionsViewModel
@@ -91,6 +91,7 @@ class SubSettingActivity : BaseActivity() {
) )
} }
hideLoading() hideLoading()
refreshData()
} }
} }
@@ -155,7 +156,7 @@ class SubSettingActivity : BaseActivity() {
else -> ownerActivity.toast("else") else -> ownerActivity.toast("else")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Share subscription failed", e) LogUtil.e(AppConfig.TAG, "Share subscription failed", e)
} }
}.show() }.show()
} }
@@ -5,9 +5,7 @@ import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.contracts.BaseAdapterListener import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
import com.v2ray.ang.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperAdapter
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -13,6 +12,7 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityTaskerBinding import com.v2ray.ang.databinding.ActivityTaskerBinding
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.LogUtil
class TaskerActivity : BaseActivity() { class TaskerActivity : BaseActivity() {
private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) } private val binding by lazy { ActivityTaskerBinding.inflate(layoutInflater) }
@@ -62,7 +62,7 @@ class TaskerActivity : BaseActivity() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to initialize Tasker settings", e) LogUtil.e(AppConfig.TAG, "Failed to initialize Tasker settings", e)
} }
} }
@@ -3,7 +3,6 @@ package com.v2ray.ang.ui
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
@@ -11,6 +10,7 @@ import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -55,7 +55,7 @@ class UrlSchemeActivity : BaseActivity() {
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
finish() finish()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error processing URL scheme", e) LogUtil.e(AppConfig.TAG, "Error processing URL scheme", e)
} }
} }
@@ -63,7 +63,7 @@ class UrlSchemeActivity : BaseActivity() {
if (uriString.isNullOrEmpty()) { if (uriString.isNullOrEmpty()) {
return return
} }
Log.i(AppConfig.TAG, uriString) LogUtil.i(AppConfig.TAG, uriString)
var decodedUrl = URLDecoder.decode(uriString, "UTF-8") var decodedUrl = URLDecoder.decode(uriString, "UTF-8")
val uri = Uri.parse(decodedUrl) val uri = Uri.parse(decodedUrl)
@@ -71,7 +71,7 @@ class UrlSchemeActivity : BaseActivity() {
if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) { if (uri.fragment.isNullOrEmpty() && !fragment.isNullOrEmpty()) {
decodedUrl += "#${fragment}" decodedUrl += "#${fragment}"
} }
Log.i(AppConfig.TAG, decodedUrl) LogUtil.i(AppConfig.TAG, decodedUrl)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false) val (count, countSub) = AngConfigManager.importBatchConfig(decodedUrl, "", false)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -6,7 +6,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -22,6 +21,7 @@ import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.UserAssetViewModel import com.v2ray.ang.viewmodel.UserAssetViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -84,7 +84,7 @@ class UserAssetActivity : HelperBaseActivity() {
MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value) MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
binding.tvGeoFilesSourcesSummary.text = value binding.tvGeoFilesSourcesSummary.text = value
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to set geo files sources", e) LogUtil.e(AppConfig.TAG, "Failed to set geo files sources", e)
} }
}.show() }.show()
} }
@@ -136,7 +136,7 @@ class UserAssetActivity : HelperBaseActivity() {
}.also { cursor.close() } }.also { cursor.close() }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get cursor name", e) LogUtil.e(AppConfig.TAG, "Failed to get cursor name", e)
null null
} }
@@ -162,7 +162,7 @@ class UserAssetActivity : HelperBaseActivity() {
.putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url) .putExtra(UserAssetUrlActivity.ASSET_URL_QRCODE, url)
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to import asset from URL", e) LogUtil.e(AppConfig.TAG, "Failed to import asset from URL", e)
return false return false
} }
return true return true
@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@@ -13,6 +12,7 @@ import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.io.File import java.io.File
@@ -80,7 +80,7 @@ class UserAssetUrlActivity : BaseActivity() {
try { try {
file.delete() file.delete()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e) LogUtil.e(AppConfig.TAG, "Failed to delete asset file: ${file.path}", e)
} }
} }
} else { } else {
@@ -1,6 +1,5 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig import com.v2ray.ang.BuildConfig
@@ -89,11 +88,11 @@ object HttpUtil {
val ipList = sortedAddresses.mapNotNull { it.hostAddress } val ipList = sortedAddresses.mapNotNull { it.hostAddress }
Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}") LogUtil.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}")
return ipList return ipList
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to resolve host to IP", e) LogUtil.e(AppConfig.TAG, "Failed to resolve host to IP", e)
return null return null
} }
} }
@@ -128,7 +127,7 @@ object HttpUtil {
* @throws IOException If an I/O error occurs. * @throws IOException If an I/O error occurs.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun getUrlContentWithUserAgent(url: String?, userAgent: String?, timeout: Int = 15000, httpPort: Int = 0): String { fun getUrlContentWithUserAgent(url: String?, userAgent: String?, timeout: Int = 15000, httpPort: Int = 0): String {
var currentUrl = url var currentUrl = url
var redirects = 0 var redirects = 0
val maxRedirects = 3 val maxRedirects = 3
@@ -219,7 +218,7 @@ object HttpUtil {
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to create proxy connection", e) LogUtil.e(AppConfig.TAG, "Failed to create proxy connection", e)
// If an exception occurs, close the connection and return null // If an exception occurs, close the connection and return null
conn?.disconnect() conn?.disconnect()
return null return null
@@ -1,6 +1,5 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonObject import com.google.gson.JsonObject
@@ -72,7 +71,7 @@ object JsonUtil {
try { try {
return JsonParser.parseString(src).getAsJsonObject() return JsonParser.parseString(src).getAsJsonObject()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to parse JSON string", e) LogUtil.e(AppConfig.TAG, "Failed to parse JSON string", e)
return null return null
} }
} }
@@ -0,0 +1,78 @@
package com.v2ray.ang.util
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager
import java.util.Locale
object LogUtil {
private const val DEFAULT_LEVEL = "warning"
private const val CACHE_UNSET = Int.MIN_VALUE
@Volatile
private var cachedMinPriority: Int = CACHE_UNSET
private fun parsePriority(level: String?): Int {
return when ((level ?: DEFAULT_LEVEL).lowercase(Locale.US)) {
"verbose" -> Log.VERBOSE
"debug" -> Log.DEBUG
"info" -> Log.INFO
"warn", "warning" -> Log.WARN
"error" -> Log.ERROR
"none", "off" -> Int.MAX_VALUE
else -> Log.WARN
}
}
@Suppress("unused")
fun refreshLogLevel() {
cachedMinPriority = parsePriority(MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL, DEFAULT_LEVEL))
}
private fun minPriority(): Int {
val cached = cachedMinPriority
if (cached != CACHE_UNSET) {
return cached
}
return synchronized(this) {
val current = cachedMinPriority
if (current != CACHE_UNSET) {
current
} else {
parsePriority(MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL, DEFAULT_LEVEL)).also {
cachedMinPriority = it
}
}
}
}
private fun isEnabled(priority: Int): Boolean {
return priority >= minPriority()
}
private fun log(priority: Int, tag: String, message: String, throwable: Throwable? = null) {
if (!isEnabled(priority)) return
when {
throwable == null -> Log.println(priority, tag, message)
priority >= Log.ERROR -> Log.e(tag, message, throwable)
priority == Log.WARN -> Log.w(tag, message, throwable)
priority == Log.INFO -> Log.i(tag, message, throwable)
priority == Log.DEBUG -> Log.d(tag, message, throwable)
else -> Log.v(tag, message, throwable)
}
}
fun d(tag: String = AppConfig.TAG, message: String) = log(Log.DEBUG, tag, message)
fun i(tag: String = AppConfig.TAG, message: String) = log(Log.INFO, tag, message)
fun w(tag: String = AppConfig.TAG, message: String) = log(Log.WARN, tag, message)
fun e(tag: String = AppConfig.TAG, message: String) = log(Log.ERROR, tag, message)
fun d(tag: String = AppConfig.TAG, message: String, throwable: Throwable) = log(Log.DEBUG, tag, message, throwable)
fun i(tag: String = AppConfig.TAG, message: String, throwable: Throwable) = log(Log.INFO, tag, message, throwable)
fun w(tag: String = AppConfig.TAG, message: String, throwable: Throwable) = log(Log.WARN, tag, message, throwable)
fun e(tag: String = AppConfig.TAG, message: String, throwable: Throwable) = log(Log.ERROR, tag, message, throwable)
}
@@ -3,7 +3,6 @@ package com.v2ray.ang.util
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.TestServiceMessage import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.service.V2RayTestService import com.v2ray.ang.service.V2RayTestService
@@ -47,7 +46,7 @@ object MessageUtil {
intent.putExtra("content", message) intent.putExtra("content", message)
ctx.startService(intent) ctx.startService(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to send message to test service", e) LogUtil.e(AppConfig.TAG, "Failed to send message to test service", e)
} }
} }
@@ -68,7 +67,7 @@ object MessageUtil {
intent.putExtra("content", content) intent.putExtra("content", content)
ctx.sendBroadcast(intent) ctx.sendBroadcast(intent)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to send message with action: $action", e) LogUtil.e(AppConfig.TAG, "Failed to send message with action: $action", e)
} }
} }
} }
@@ -0,0 +1,44 @@
package com.v2ray.ang.util
import android.content.Context
import android.content.pm.PackageManager
import java.util.concurrent.ConcurrentHashMap
object PackageUidResolver {
private const val TAG = "PackageUidResolver"
// In-process cache to avoid resolving the same package UID repeatedly.
private val packageUidCache = ConcurrentHashMap<String, String>()
val packageUidMap: Map<String, String>
get() = packageUidCache
fun packageNamesToUids(context: Context, packageNames: List<String>): List<String> {
return packageNames.mapNotNull { pkg ->
packageUidCache[pkg] ?: resolveUid(context, pkg)?.also { uid ->
packageUidCache[pkg] = uid
}
}
}
fun uidsToPackageNames(uids: List<String>): List<String> {
if (uids.isEmpty()) return emptyList()
return packageUidCache.filterValues { it in uids }.keys.toList()
}
fun uidToPackageName(uid: String): String? {
return packageUidCache.entries.firstOrNull { it.value == uid }?.key
}
private fun resolveUid(context: Context, packageName: String): String? {
return try {
val uid = context.packageManager.getPackageUid(packageName, 0).toString()
LogUtil.d(TAG, "Package: $packageName -> UID: $uid")
uid
} catch (_: PackageManager.NameNotFoundException) {
LogUtil.w(TAG, "Package not found: $packageName")
null
}
}
}
@@ -11,7 +11,6 @@ import android.os.LocaleList
import android.provider.Settings import android.provider.Settings
import android.text.Editable import android.text.Editable
import android.util.Base64 import android.util.Base64
import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -79,7 +78,7 @@ object Utils {
val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cmb.primaryClip?.getItemAt(0)?.text.toString() cmb.primaryClip?.getItemAt(0)?.text.toString()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get clipboard content", e) LogUtil.e(AppConfig.TAG, "Failed to get clipboard content", e)
"" ""
} }
} }
@@ -96,7 +95,7 @@ object Utils {
val clipData = ClipData.newPlainText(null, content) val clipData = ClipData.newPlainText(null, content)
cmb.setPrimaryClip(clipData) cmb.setPrimaryClip(clipData)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to set clipboard content", e) LogUtil.e(AppConfig.TAG, "Failed to set clipboard content", e)
} }
} }
@@ -122,12 +121,12 @@ object Utils {
try { try {
return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8) return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode standard base64", e) LogUtil.e(AppConfig.TAG, "Failed to decode standard base64", e)
} }
try { try {
return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8) return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode URL-safe base64", e) LogUtil.e(AppConfig.TAG, "Failed to decode URL-safe base64", e)
} }
return null return null
} }
@@ -139,7 +138,7 @@ object Utils {
* @param removePadding * @param removePadding
* @return The base64 encoded string, or an empty string if encoding fails. * @return The base64 encoded string, or an empty string if encoding fails.
*/ */
fun encode(text: String, removePadding : Boolean = false): String { fun encode(text: String, removePadding: Boolean = false): String {
return try { return try {
var encoded = Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) var encoded = Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
if (removePadding) { if (removePadding) {
@@ -147,7 +146,7 @@ object Utils {
} }
encoded encoded
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to encode text to base64", e) LogUtil.e(AppConfig.TAG, "Failed to encode text to base64", e)
"" ""
} }
} }
@@ -190,7 +189,7 @@ object Utils {
return isIpv6Address(addr) return isIpv6Address(addr)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to validate IP address", e) LogUtil.e(AppConfig.TAG, "Failed to validate IP address", e)
return false return false
} }
} }
@@ -271,7 +270,7 @@ object Utils {
Patterns.DOMAIN_NAME.matcher(value).matches() || Patterns.DOMAIN_NAME.matcher(value).matches() ||
URLUtil.isValidUrl(value) URLUtil.isValidUrl(value)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to validate URL", e) LogUtil.e(AppConfig.TAG, "Failed to validate URL", e)
false false
} }
} }
@@ -287,7 +286,7 @@ object Utils {
val uri = uriString.toUri() val uri = uriString.toUri()
context.startActivity(Intent(Intent.ACTION_VIEW, uri)) context.startActivity(Intent(Intent.ACTION_VIEW, uri))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to open URI", e) LogUtil.e(AppConfig.TAG, "Failed to open URI", e)
} }
} }
@@ -300,7 +299,7 @@ object Utils {
return try { return try {
UUID.randomUUID().toString().replace("-", "") UUID.randomUUID().toString().replace("-", "")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to generate UUID", e) LogUtil.e(AppConfig.TAG, "Failed to generate UUID", e)
"" ""
} }
} }
@@ -315,7 +314,7 @@ object Utils {
return try { return try {
URLDecoder.decode(url, Charsets.UTF_8.toString()) URLDecoder.decode(url, Charsets.UTF_8.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode URL", e) LogUtil.e(AppConfig.TAG, "Failed to decode URL", e)
url url
} }
} }
@@ -330,7 +329,7 @@ object Utils {
return try { return try {
URLEncoder.encode(url, Charsets.UTF_8.toString()) URLEncoder.encode(url, Charsets.UTF_8.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to encode URL", e) LogUtil.e(AppConfig.TAG, "Failed to encode URL", e)
url url
} }
} }
@@ -349,7 +348,7 @@ object Utils {
// to avoid cross-language interoperability issues. // to avoid cross-language interoperability issues.
URLDecoder.decode(url.replace("+", "%2B"), Charsets.UTF_8.toString()) URLDecoder.decode(url.replace("+", "%2B"), Charsets.UTF_8.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode encodeURIComponent", e) LogUtil.e(AppConfig.TAG, "Failed to decode encodeURIComponent", e)
url url
} }
} }
@@ -365,7 +364,7 @@ object Utils {
// Replace '+' with '%20' to conform to encodeURIComponent semantics. // Replace '+' with '%20' to conform to encodeURIComponent semantics.
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20") URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to encode encodeURIComponent", e) LogUtil.e(AppConfig.TAG, "Failed to encode encodeURIComponent", e)
url url
} }
} }
@@ -387,7 +386,7 @@ object Utils {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e) LogUtil.e(AppConfig.TAG, "Failed to read asset file: $fileName", e)
"" ""
} }
} }
@@ -405,7 +404,7 @@ object Utils {
context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath
?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath ?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to get user asset path", e) LogUtil.e(AppConfig.TAG, "Failed to get user asset path", e)
"" ""
} }
} }
@@ -420,7 +419,7 @@ object Utils {
val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8) val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8)
Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE)) Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to generate device ID", e) LogUtil.e(AppConfig.TAG, "Failed to generate device ID", e)
"" ""
} }
} }
@@ -512,7 +511,7 @@ object Utils {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e) LogUtil.e(AppConfig.TAG, "Failed to validate subscription URL", e)
} }
return false return false
} }
@@ -582,7 +581,7 @@ object Utils {
// Check if they're in the same subnet // Check if they're in the same subnet
return (ipLong and mask) == (cidrIpLong and mask) return (ipLong and mask) == (cidrIpLong and mask)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e) LogUtil.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e)
return false return false
} }
} }
@@ -599,7 +598,7 @@ object Utils {
val sdf = SimpleDateFormat(pattern, locale) val sdf = SimpleDateFormat(pattern, locale)
sdf.format(Date(ts)) sdf.format(Date(ts))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to format timestamp", e) LogUtil.e(AppConfig.TAG, "Failed to format timestamp", e)
"" ""
} }
} }
@@ -1,6 +1,5 @@
package com.v2ray.ang.util package com.v2ray.ang.util
import android.util.Log
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
@@ -63,7 +62,7 @@ object ZipUtil {
zos.closeEntry() zos.closeEntry()
zos.close() zos.close()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to zip folder", e) LogUtil.e(AppConfig.TAG, "Failed to zip folder", e)
return false return false
} }
return true return true
@@ -99,7 +98,7 @@ object ZipUtil {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to unzip file", e) LogUtil.e(AppConfig.TAG, "Failed to unzip file", e)
return false return false
} }
return true return true
@@ -3,6 +3,7 @@ package com.v2ray.ang.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.util.LogUtil
import java.io.IOException import java.io.IOException
class LogcatViewModel : ViewModel() { class LogcatViewModel : ViewModel() {
@@ -28,7 +29,7 @@ class LogcatViewModel : ViewModel() {
logsetsAll.addAll(allText) logsetsAll.addAll(allText)
applyFilter() applyFilter()
} catch (e: IOException) { } catch (e: IOException) {
android.util.Log.e(AppConfig.TAG, "Failed to get logcat", e) LogUtil.e(AppConfig.TAG, "Failed to get logcat", e)
} }
} }
@@ -43,7 +44,7 @@ class LogcatViewModel : ViewModel() {
logsetsAll.clear() logsetsAll.clear()
filteredLogs = emptyList() filteredLogs = emptyList()
} catch (e: IOException) { } catch (e: IOException) {
android.util.Log.e(AppConfig.TAG, "Failed to clear logcat", e) LogUtil.e(AppConfig.TAG, "Failed to clear logcat", e)
} }
} }
@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.res.AssetManager import android.content.res.AssetManager
import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -19,6 +18,7 @@ import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.SubscriptionCache import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionUpdateResult import com.v2ray.ang.dto.SubscriptionUpdateResult
import com.v2ray.ang.dto.TestServiceMessage import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.extension.matchesPattern
import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
@@ -26,6 +26,7 @@ import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.SpeedtestManager import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MessageUtil import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -35,6 +36,7 @@ import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Collections import java.util.Collections
import java.util.regex.PatternSyntaxException
class MainViewModel(application: Application) : AndroidViewModel(application) { class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList() private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList()
@@ -64,7 +66,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
getApplication<AngApplication>().unregisterReceiver(mMsgReceiver) getApplication<AngApplication>().unregisterReceiver(mMsgReceiver)
tcpingTestScope.coroutineContext[Job]?.cancelChildren() tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestManager.closeAllTcpSockets() SpeedtestManager.closeAllTcpSockets()
Log.i(AppConfig.TAG, "Main ViewModel is cleared") LogUtil.i(AppConfig.TAG, "Main ViewModel is cleared")
super.onCleared() super.onCleared()
} }
@@ -117,7 +119,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
@Synchronized @Synchronized
fun updateCache() { fun updateCache() {
serversCache.clear() serversCache.clear()
val kw = keywordFilter.trim().lowercase() val kw = keywordFilter.trim()
val searchRegex = try {
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
} catch (e: PatternSyntaxException) {
null // Fallback to literal search if regex is invalid
}
for (guid in serverList) { for (guid in serverList) {
val profile = MmkvManager.decodeServerConfig(guid) ?: continue val profile = MmkvManager.decodeServerConfig(guid) ?: continue
if (kw.isEmpty()) { if (kw.isEmpty()) {
@@ -125,11 +132,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
continue continue
} }
val remarks = profile.remarks.lowercase() val remarks = profile.remarks
val description = profile.description.orEmpty().lowercase() val description = profile.description.orEmpty()
val server = profile.server.orEmpty().lowercase() val server = profile.server.orEmpty()
val protocol = profile.configType.name
if (remarks.contains(kw) || description.contains(kw) || server.contains(kw)) { if (remarks.matchesPattern(searchRegex, kw)
|| description.matchesPattern(searchRegex, kw)
|| server.matchesPattern(searchRegex, kw)
|| protocol.matchesPattern(searchRegex, kw)
) {
serversCache.add(ServersCache(guid, profile)) serversCache.add(ServersCache(guid, profile))
} }
} }
@@ -402,6 +413,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
reloadServerList() reloadServerList()
} }
fun findSubscriptionIdBySelect(): String? {
// Get the selected server GUID
val selectedGuid = MmkvManager.getSelectServer()
if (selectedGuid.isNullOrEmpty()) {
return null
}
val config = MmkvManager.decodeServerConfig(selectedGuid)
return config?.subscriptionId
}
fun onTestsFinished() { fun onTestsFinished() {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST)) { if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST)) {
@@ -1,6 +1,5 @@
package com.v2ray.ang.viewmodel package com.v2ray.ang.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.AssetUrlCache import com.v2ray.ang.dto.AssetUrlCache
@@ -8,6 +7,7 @@ import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.extension.concatUrl import com.v2ray.ang.extension.concatUrl
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -95,7 +95,7 @@ class UserAssetViewModel : ViewModel() {
return true return true
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e) LogUtil.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
} finally { } finally {
conn.disconnect() conn.disconnect()
} }
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<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="?attr/colorControlNormal"
android:pathData="M7,10l5,5 5,-5z" />
</vector>
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
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" />
<path
android:fillColor="#FF000000"
android:pathData="M213.333333 789.461333V234.538667C213.333333 168.533333 285.013333 127.530667 341.930667 160.981333l471.68 277.461334c56.106667 32.981333 56.106667 114.133333 0 147.114666L341.930667 863.018667C285.056 896.469333 213.333333 855.466667 213.333333 789.461333z" />
</vector>
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
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" />
<path
android:fillColor="#FF000000"
android:pathData="M256 192h512c35.392 0 64 28.608 64 64v512c0 35.392-28.608 64-64 64H256c-35.392 0-64-28.608-64-64V256c0-35.392 28.608-64 64-64z" />
</vector>
@@ -8,7 +8,7 @@
tools:context=".ui.LogcatActivity"> tools:context=".ui.LogcatActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refreshLayout" android:id="@+id/refresh_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -94,6 +94,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginBottom="-16dp"
android:layout_marginEnd="@dimen/padding_spacing_dp16"> android:layout_marginEnd="@dimen/padding_spacing_dp16">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
@@ -108,6 +109,7 @@
android:nextFocusLeft="@+id/layout_test" android:nextFocusLeft="@+id/layout_test"
android:src="@drawable/ic_play_24dp" android:src="@drawable/ic_play_24dp"
app:tint="@color/colorWhite" app:tint="@color/colorWhite"
app:useCompatPadding="true"
app:layout_anchorGravity="bottom|right|end" /> app:layout_anchorGravity="bottom|right|end" />
</FrameLayout> </FrameLayout>
@@ -103,6 +103,26 @@
android:maxLines="10" /> android:maxLines="10" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/routing_settings_process" />
<EditText
android:id="@+id/et_process"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/routing_settings_tips"
android:inputType="textMultiLine"
android:maxLines="10" />
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -170,13 +190,33 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/routing_settings_outbound_tag" /> android:text="@string/routing_settings_outbound_tag" />
<Spinner <RelativeLayout
android:id="@+id/sp_outbound_tag"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8" android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8" android:layout_marginBottom="@dimen/padding_spacing_dp8">
android:entries="@array/outbound_tag" />
<AutoCompleteTextView
android:id="@+id/sp_outbound_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:completionThreshold="0"
android:hint="@string/routing_settings_outbound_tag_hint"
android:inputType="text"
android:imeOptions="actionDone"
android:paddingEnd="40dp" />
<ImageButton
android:id="@+id/btn_outbound_tag_dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/routing_settings_outbound_tag"
android:src="@drawable/ic_arrow_drop_down" />
</RelativeLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -85,8 +85,7 @@
<EditText <EditText
android:id="@+id/et_port_hop_interval" android:id="@+id/et_port_hop_interval"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:inputType="number" />
</LinearLayout> </LinearLayout>
@@ -3,10 +3,17 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/recycler_view" android:id="@+id/refresh_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:scrollbars="vertical" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout> </FrameLayout>
@@ -118,4 +118,25 @@
android:minLines="4" /> android:minLines="4" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/layout_fm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_final_mask" />
<EditText
android:id="@+id/et_fm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:maxLines="20"
android:minLines="4" />
</LinearLayout>
</LinearLayout> </LinearLayout>
@@ -98,6 +98,10 @@
android:id="@+id/sort_by_test_results" android:id="@+id/sort_by_test_results"
android:title="@string/title_sort_by_test_results" android:title="@string/title_sort_by_test_results"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/locate_selected_config"
android:title="@string/title_locate_selected_config"
app:showAsAction="never" />
<item <item
android:id="@+id/sub_update" android:id="@+id/sub_update"
android:title="@string/title_sub_update" android:title="@string/title_sub_update"
+26 -2
View File
@@ -113,6 +113,7 @@
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string> <string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -185,8 +186,11 @@
<string name="title_pref_fake_dns_enabled">تفعيل DNS وهمي</string> <string name="title_pref_fake_dns_enabled">تفعيل DNS وهمي</string>
<string name="summary_pref_fake_dns_enabled">يعيد DNS المحلي عنوان IP وهمي (أسرع، ولكنه قد لا يعمل مع بعض التطبيقات)</string> <string name="summary_pref_fake_dns_enabled">يعيد DNS المحلي عنوان IP وهمي (أسرع، ولكنه قد لا يعمل مع بعض التطبيقات)</string>
<string name="title_pref_ipv6_enabled">تفعيل IPv6</string>
<string name="summary_pref_ipv6_enabled">إضافة عنوان IPv6 والمسارات إلى واجهة VPN</string>
<string name="title_pref_prefer_ipv6">تفضيل IPv6</string> <string name="title_pref_prefer_ipv6">تفضيل IPv6</string>
<string name="summary_pref_prefer_ipv6">تفضيل عنوان IPv6 وطرق التوجيه</string> <string name="summary_pref_prefer_ipv6">تفضيل عناوين IPv6 عند حل أسماء النطاقات</string>
<string name="title_pref_remote_dns">DNS البعيد (udp/tcp/https/quic) (اختياري)</string> <string name="title_pref_remote_dns">DNS البعيد (udp/tcp/https/quic) (اختياري)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -218,6 +222,12 @@
<string name="title_pref_socks_port">منفذ بروكسي Local</string> <string name="title_pref_socks_port">منفذ بروكسي Local</string>
<string name="summary_pref_socks_port">منفذ بروكسي Local</string> <string name="summary_pref_socks_port">منفذ بروكسي Local</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
<string name="title_pref_socks_username">Local proxy username (optional)</string>
<string name="summary_pref_socks_username">Username</string>
<string name="title_pref_socks_password">Local proxy password (optional)</string>
<string name="summary_pref_socks_password">Password</string>
<string name="title_pref_local_dns_port">منفذ DNS المحلي</string> <string name="title_pref_local_dns_port">منفذ DNS المحلي</string>
<string name="summary_pref_local_dns_port">منفذ DNS المحلي</string> <string name="summary_pref_local_dns_port">منفذ DNS المحلي</string>
@@ -242,6 +252,7 @@
<string name="summary_pref_tg_group">الانضمام إلى مجموعة Telegram</string> <string name="summary_pref_tg_group">الانضمام إلى مجموعة Telegram</string>
<string name="toast_tg_app_not_found">لم يتم العثور على تطبيق Telegram</string> <string name="toast_tg_app_not_found">لم يتم العثور على تطبيق Telegram</string>
<string name="title_privacy_policy">سياسة الخصوصية</string> <string name="title_privacy_policy">سياسة الخصوصية</string>
<string name="title_qr_code">QR code</string>
<string name="title_about">حول\nترجمة م. ابراهيم قاسم</string> <string name="title_about">حول\nترجمة م. ابراهيم قاسم</string>
<string name="title_source_code">الكود المصدري</string> <string name="title_source_code">الكود المصدري</string>
<string name="title_oss_license">Open Source licenses</string> <string name="title_oss_license">Open Source licenses</string>
@@ -299,13 +310,17 @@
<string name="title_update_config_count">Update %d configurations</string> <string name="title_update_config_count">Update %d configurations</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string> <string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string> <string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
<string name="toast_fragment_not_available">Unable to locate current view</string>
<string name="title_locate_selected_config">Locate the selected config</string>
<string name="tasker_start_service">بدء الخدمة</string> <string name="tasker_start_service">بدء الخدمة</string>
<string name="tasker_setting_confirm">تأكيد</string> <string name="tasker_setting_confirm">تأكيد</string>
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">استراتيجية النطاق</string> <string name="routing_settings_domain_strategy">استراتيجية النطاق</string>
<string name="routing_settings_title">إعدادات التوجيه</string> <string name="routing_settings_title">إعدادات التوجيه</string>
<string name="routing_settings_tips">مفصولة بفواصل (،)، تذكر الحفظ</string> <string name="routing_settings_tips">افصل بعلامة الفاصلة (,) واختر أحد: domain أو ip أو process</string>
<string name="routing_settings_save">حفظ</string> <string name="routing_settings_save">حفظ</string>
<string name="routing_settings_delete">مسح</string> <string name="routing_settings_delete">مسح</string>
<string name="routing_settings_rule_title">Routing Rule Settings</string> <string name="routing_settings_rule_title">Routing Rule Settings</string>
@@ -316,6 +331,15 @@
<string name="routing_settings_import_rulesets_from_qrcode">استيراد مجموعة قواعد من رمز الاستجابة السريعة</string> <string name="routing_settings_import_rulesets_from_qrcode">استيراد مجموعة قواعد من رمز الاستجابة السريعة</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string> <string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string> <string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_domain">domain</string>
<string name="routing_settings_ip">ip</string>
<string name="routing_settings_process">process (Package Name — only supported when using Xray TUN, routeOnly enabled, and Android 10+)</string>
<string name="routing_settings_port">port</string>
<string name="routing_settings_protocol">protocol</string>
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
<string name="routing_settings_network">network</string>
<string name="routing_settings_network_tip">[udp|tcp]</string>
<string name="routing_settings_outbound_tag">outboundTag</string>
<string name="connection_test_pending">التحقق من الاتصال</string> <string name="connection_test_pending">التحقق من الاتصال</string>
<string name="connection_test_testing">يجري الاختبار…</string> <string name="connection_test_testing">يجري الاختبار…</string>
+26 -2
View File
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string> <string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -185,8 +186,11 @@
<string name="title_pref_fake_dns_enabled">ভুয়া DNS সক্রিয় করুন</string> <string name="title_pref_fake_dns_enabled">ভুয়া DNS সক্রিয় করুন</string>
<string name="summary_pref_fake_dns_enabled">স্থানীয় DNS মিথ্যা আইপি ঠিকানা ফেরত দেয় (দ্রুত, তবে এটি কিছু অ্যাপের জন্য কাজ নাও করতে পারে)</string> <string name="summary_pref_fake_dns_enabled">স্থানীয় DNS মিথ্যা আইপি ঠিকানা ফেরত দেয় (দ্রুত, তবে এটি কিছু অ্যাপের জন্য কাজ নাও করতে পারে)</string>
<string name="title_pref_ipv6_enabled">IPv6 সক্রিয় করুন</string>
<string name="summary_pref_ipv6_enabled">VPN ইন্টারফেসে IPv6 ঠিকানা এবং রুট যোগ করুন</string>
<string name="title_pref_prefer_ipv6">IPv6 অগ্রাধিকার দিন</string> <string name="title_pref_prefer_ipv6">IPv6 অগ্রাধিকার দিন</string>
<string name="summary_pref_prefer_ipv6">IPv6 ঠিকানা এবং রুটকে অগ্রাধিকার দিন</string> <string name="summary_pref_prefer_ipv6">ডোমেইন নাম রেজোলিউশনে IPv6 ঠিকানাকে অগ্রাধিকার দিন</string>
<string name="title_pref_remote_dns">রিমোট DNS (udp/tcp/https/quic)(ঐচ্ছিক)</string> <string name="title_pref_remote_dns">রিমোট DNS (udp/tcp/https/quic)(ঐচ্ছিক)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -218,6 +222,12 @@
<string name="title_pref_socks_port">Local প্রক্সি পোর্ট</string> <string name="title_pref_socks_port">Local প্রক্সি পোর্ট</string>
<string name="summary_pref_socks_port">Local প্রক্সি পোর্ট</string> <string name="summary_pref_socks_port">Local প্রক্সি পোর্ট</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
<string name="title_pref_socks_username">Local proxy username (optional)</string>
<string name="summary_pref_socks_username">Username</string>
<string name="title_pref_socks_password">Local proxy password (optional)</string>
<string name="summary_pref_socks_password">Password</string>
<string name="title_pref_local_dns_port">স্থানীয় DNS পোর্ট</string> <string name="title_pref_local_dns_port">স্থানীয় DNS পোর্ট</string>
<string name="summary_pref_local_dns_port">স্থানীয় DNS পোর্ট</string> <string name="summary_pref_local_dns_port">স্থানীয় DNS পোর্ট</string>
@@ -242,6 +252,7 @@
<string name="summary_pref_tg_group">টেলিগ্রাম গ্রুপে যোগদান করুন</string> <string name="summary_pref_tg_group">টেলিগ্রাম গ্রুপে যোগদান করুন</string>
<string name="toast_tg_app_not_found">টেলিগ্রাম অ্যাপ পাওয়া যায়নি</string> <string name="toast_tg_app_not_found">টেলিগ্রাম অ্যাপ পাওয়া যায়নি</string>
<string name="title_privacy_policy">গোপনীয়তা নীতি</string> <string name="title_privacy_policy">গোপনীয়তা নীতি</string>
<string name="title_qr_code">QR code</string>
<string name="title_about">সম্পর্কিত</string> <string name="title_about">সম্পর্কিত</string>
<string name="title_source_code">সোর্স কোড</string> <string name="title_source_code">সোর্স কোড</string>
<string name="title_oss_license">Open Source licenses</string> <string name="title_oss_license">Open Source licenses</string>
@@ -298,13 +309,17 @@
<string name="title_update_config_count">Update %d configurations</string> <string name="title_update_config_count">Update %d configurations</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string> <string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string> <string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
<string name="toast_fragment_not_available">Unable to locate current view</string>
<string name="title_locate_selected_config">Locate the selected config</string>
<string name="tasker_start_service">সার্ভিস শুরু করুন</string> <string name="tasker_start_service">সার্ভিস শুরু করুন</string>
<string name="tasker_setting_confirm">নিশ্চিত করুন</string> <string name="tasker_setting_confirm">নিশ্চিত করুন</string>
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">ডোমেইন কৌশল</string> <string name="routing_settings_domain_strategy">ডোমেইন কৌশল</string>
<string name="routing_settings_title">রাউটিং সেটিংস</string> <string name="routing_settings_title">রাউটিং সেটিংস</string>
<string name="routing_settings_tips">কমা (,) দ্বারা আলাদা করুন, মনে রাখবেন সেভ করতে</string> <string name="routing_settings_tips">কমা (,) দিয়ে আলাদা করুন। domain, ip বা process-এ একটি নির্বাচন করুন</string>
<string name="routing_settings_save">সেভ করুন</string> <string name="routing_settings_save">সেভ করুন</string>
<string name="routing_settings_delete">মুছে ফেলুন</string> <string name="routing_settings_delete">মুছে ফেলুন</string>
<string name="routing_settings_rule_title">Routing Rule Settings</string> <string name="routing_settings_rule_title">Routing Rule Settings</string>
@@ -315,6 +330,15 @@
<string name="routing_settings_import_rulesets_from_qrcode">QRcode থেকে রুলসেট আমদানি করুন</string> <string name="routing_settings_import_rulesets_from_qrcode">QRcode থেকে রুলসেট আমদানি করুন</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string> <string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string> <string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_domain">domain</string>
<string name="routing_settings_ip">ip</string>
<string name="routing_settings_process">process (Package Name — only supported when using Xray TUN, routeOnly enabled, and Android 10+)</string>
<string name="routing_settings_port">port</string>
<string name="routing_settings_protocol">protocol</string>
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
<string name="routing_settings_network">network</string>
<string name="routing_settings_network_tip">[udp|tcp]</string>
<string name="routing_settings_outbound_tag">outboundTag</string>
<string name="connection_test_pending">সংযোগ পরীক্ষা করুন</string> <string name="connection_test_pending">সংযোগ পরীক্ষা করুন</string>
<string name="connection_test_testing">পরীক্ষা চলছে…</string> <string name="connection_test_testing">পরীক্ষা চলছে…</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string> <string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string>
<string name="server_lab_xhttp_mode">هالت XHTTP</string> <string name="server_lab_xhttp_mode">هالت XHTTP</string>
<string name="server_lab_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string> <string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string>
<string name="server_lab_ech_force_query">پورس وو جۊ اجباری Ech</string> <string name="server_lab_ech_force_query">پورس وو جۊ اجباری Ech</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string> <string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -185,8 +186,11 @@
<string name="title_pref_fake_dns_enabled">ره وندن DNS جئلی</string> <string name="title_pref_fake_dns_enabled">ره وندن DNS جئلی</string>
<string name="summary_pref_fake_dns_enabled">DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه)</string> <string name="summary_pref_fake_dns_enabled">DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه)</string>
<string name="title_pref_ipv6_enabled">ره وندن IPv6</string>
<string name="summary_pref_ipv6_enabled">نشۊویا IPv6 وو تورا IPv6 ن ز رابط VPN ره وند کۊنین</string>
<string name="title_pref_prefer_ipv6">ترجی داڌن IPv6</string> <string name="title_pref_prefer_ipv6">ترجی داڌن IPv6</string>
<string name="summary_pref_prefer_ipv6">تورا IPv6 ن فعال کۊنین وو نشۊویا IPv6 ن ترجی بڌین</string> <string name="summary_pref_prefer_ipv6">مون حل کردن نوم دامنه، نشۊویا IPv6 ن ترجی بڌین</string>
<string name="title_pref_remote_dns">DNS ز ره دیر (اختیاری) (udp/tcp/https/quic) (اختیاری)</string> <string name="title_pref_remote_dns">DNS ز ره دیر (اختیاری) (udp/tcp/https/quic) (اختیاری)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -218,6 +222,12 @@
<string name="title_pref_socks_port">پورت پروکسی مهلی</string> <string name="title_pref_socks_port">پورت پروکسی مهلی</string>
<string name="summary_pref_socks_port">پورت پروکسی مهلی</string> <string name="summary_pref_socks_port">پورت پروکسی مهلی</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
<string name="title_pref_socks_username">Local proxy username (optional)</string>
<string name="summary_pref_socks_username">Username</string>
<string name="title_pref_socks_password">Local proxy password (optional)</string>
<string name="summary_pref_socks_password">Password</string>
<string name="title_pref_local_dns_port">پورت DNS مهلی</string> <string name="title_pref_local_dns_port">پورت DNS مهلی</string>
<string name="summary_pref_local_dns_port">پورت DNS مهلی</string> <string name="summary_pref_local_dns_port">پورت DNS مهلی</string>
@@ -242,6 +252,7 @@
<string name="summary_pref_tg_group">ٱووڌن من بونکۊ تلگرام</string> <string name="summary_pref_tg_group">ٱووڌن من بونکۊ تلگرام</string>
<string name="toast_tg_app_not_found">برنومه تلگرامن نجوست</string> <string name="toast_tg_app_not_found">برنومه تلگرامن نجوست</string>
<string name="title_privacy_policy">هریم سیخومی</string> <string name="title_privacy_policy">هریم سیخومی</string>
<string name="title_qr_code">QR code</string>
<string name="title_about">زبار</string> <string name="title_about">زبار</string>
<string name="title_source_code">کود بونچک</string> <string name="title_source_code">کود بونچک</string>
<string name="title_oss_license">موجوزا کود بونچک</string> <string name="title_oss_license">موجوزا کود بونچک</string>
@@ -299,13 +310,17 @@
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string> <string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
<string name="title_update_subscription_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string> <string name="title_update_subscription_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string>
<string name="title_update_subscription_no_subscription">بؽ اشتراک</string> <string name="title_update_subscription_no_subscription">بؽ اشتراک</string>
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
<string name="toast_fragment_not_available">Unable to locate current view</string>
<string name="title_locate_selected_config">Locate the selected config</string>
<string name="tasker_start_service">ره وندن خدمات</string> <string name="tasker_start_service">ره وندن خدمات</string>
<string name="tasker_setting_confirm">قوۊل</string> <string name="tasker_setting_confirm">قوۊل</string>
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">نشقه دامنه</string> <string name="routing_settings_domain_strategy">نشقه دامنه</string>
<string name="routing_settings_title">سامووا تور جوستن</string> <string name="routing_settings_title">سامووا تور جوستن</string>
<string name="routing_settings_tips">وا کاما ز یک جوڌا ابۊن (،) پسند دامنه یا آی پی</string> <string name="routing_settings_tips">وا کاما ز یک جوڌا ابۊن (،)؛ یکی را وارد کنید: domain، ip یا process</string>
<string name="routing_settings_save">زفت کردن</string> <string name="routing_settings_save">زفت کردن</string>
<string name="routing_settings_delete">روفتن</string> <string name="routing_settings_delete">روفتن</string>
<string name="routing_settings_rule_title">سامووا قانۉݩ تور جوستن</string> <string name="routing_settings_rule_title">سامووا قانۉݩ تور جوستن</string>
+17 -2
View File
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">افزایش پهنای باند (واحد)</string> <string name="server_lab_bandwidth_up">افزایش پهنای باند (واحد)</string>
<string name="server_lab_xhttp_mode">حالت XHTTP</string> <string name="server_lab_xhttp_mode">حالت XHTTP</string>
<string name="server_lab_xhttp_extra">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">لیست کانفیگ Ech</string> <string name="server_lab_ech_config_list">لیست کانفیگ Ech</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string> <string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -183,8 +184,11 @@
<string name="title_pref_fake_dns_enabled">فعال کردن DNS جعلی</string> <string name="title_pref_fake_dns_enabled">فعال کردن DNS جعلی</string>
<string name="summary_pref_fake_dns_enabled">دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)</string> <string name="summary_pref_fake_dns_enabled">دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)</string>
<string name="title_pref_ipv6_enabled">فعال کردن IPv6</string>
<string name="summary_pref_ipv6_enabled">آدرس و مسیرهای IPv6 را به رابط VPN اضافه کنید</string>
<string name="title_pref_prefer_ipv6">ترجیح دادن IPV6</string> <string name="title_pref_prefer_ipv6">ترجیح دادن IPV6</string>
<string name="summary_pref_prefer_ipv6">مسیرهای IPv6 را فعال کنید و آدرس‌های IPv6 را ترجیح دهید</string> <string name="summary_pref_prefer_ipv6">هنگام بازیابی نام دامنه، آدرس‌های IPv6 را ترجیح دهید</string>
<string name="title_pref_remote_dns">DNS از راه دور (اختیاری) (udp/tcp/https/quic)</string> <string name="title_pref_remote_dns">DNS از راه دور (اختیاری) (udp/tcp/https/quic)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -216,6 +220,12 @@
<string name="title_pref_socks_port">پورت پروکسی محلی</string> <string name="title_pref_socks_port">پورت پروکسی محلی</string>
<string name="summary_pref_socks_port">پورت پروکسی محلی</string> <string name="summary_pref_socks_port">پورت پروکسی محلی</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
<string name="title_pref_socks_username">Local proxy username (optional)</string>
<string name="summary_pref_socks_username">Username</string>
<string name="title_pref_socks_password">Local proxy password (optional)</string>
<string name="summary_pref_socks_password">Password</string>
<string name="title_pref_local_dns_port">پورت DNS محلی</string> <string name="title_pref_local_dns_port">پورت DNS محلی</string>
<string name="summary_pref_local_dns_port">پورت DNS محلی</string> <string name="summary_pref_local_dns_port">پورت DNS محلی</string>
@@ -241,6 +251,7 @@
<string name="toast_tg_app_not_found">برنامه تلگرام پیدا نشد</string> <string name="toast_tg_app_not_found">برنامه تلگرام پیدا نشد</string>
<string name="title_privacy_policy">حریم خصوصی</string> <string name="title_privacy_policy">حریم خصوصی</string>
<string name="title_qr_code">QR code</string>
<string name="title_about">درباره</string> <string name="title_about">درباره</string>
<string name="title_source_code">کد منبع</string> <string name="title_source_code">کد منبع</string>
<string name="title_oss_license">مجوز های منبع باز</string> <string name="title_oss_license">مجوز های منبع باز</string>
@@ -296,13 +307,17 @@
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string> <string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string> <string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string> <string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
<string name="toast_fragment_not_available">Unable to locate current view</string>
<string name="title_locate_selected_config">Locate the selected config</string>
<string name="tasker_start_service">شروع خدمات</string> <string name="tasker_start_service">شروع خدمات</string>
<string name="tasker_setting_confirm">تایید</string> <string name="tasker_setting_confirm">تایید</string>
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">استراتژی دامنه</string> <string name="routing_settings_domain_strategy">استراتژی دامنه</string>
<string name="routing_settings_title">تنظیمات مسیریابی</string> <string name="routing_settings_title">تنظیمات مسیریابی</string>
<string name="routing_settings_tips">با کاما (،) از هم جدا شوند، ذخیره کردن فراموش نشود</string> <string name="routing_settings_tips">با کاما (,) جدا کنید. یکی از موارد domain، ip یا process را وارد کنید</string>
<string name="routing_settings_save">ذخیره</string> <string name="routing_settings_save">ذخیره</string>
<string name="routing_settings_delete">حذف</string> <string name="routing_settings_delete">حذف</string>
<string name="routing_settings_rule_title">تنظیمات قانون مسیریابی</string> <string name="routing_settings_rule_title">تنظیمات قانون مسیریابی</string>
+16 -2
View File
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">Режим XHTTP</string> <string name="server_lab_xhttp_mode">Режим XHTTP</string>
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
<string name="server_lab_final_mask">Необработанный JSON FinalMask, формат: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string> <string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
@@ -184,8 +185,11 @@
<string name="title_pref_fake_dns_enabled">Использовать поддельную DNS</string> <string name="title_pref_fake_dns_enabled">Использовать поддельную DNS</string>
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string> <string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
<string name="title_pref_ipv6_enabled">Включить IPv6</string>
<string name="summary_pref_ipv6_enabled">Добавить IPv6-адрес и маршруты в VPN-интерфейс</string>
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string> <string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
<string name="summary_pref_prefer_ipv6">Использовать маршрутизацию IPv6 предпочитать IPv6-адреса</string> <string name="summary_pref_prefer_ipv6">Предпочитать IPv6-адреса при разрешении доменных имён</string>
<string name="title_pref_remote_dns">Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string> <string name="title_pref_remote_dns">Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -217,6 +221,12 @@
<string name="title_pref_socks_port">Порт локального прокси</string> <string name="title_pref_socks_port">Порт локального прокси</string>
<string name="summary_pref_socks_port">Порт локального прокси</string> <string name="summary_pref_socks_port">Порт локального прокси</string>
<string name="title_pref_dynamic_socks_port">Динамически менять порт</string>
<string name="summary_pref_dynamic_socks_port">Использовать случайный порт локального прокси при каждом создании соединения</string>
<string name="title_pref_socks_username">Пользователь локального прокси (необязательно)</string>
<string name="summary_pref_socks_username">Имя пользователя</string>
<string name="title_pref_socks_password">Пароль локального прокси (необязательно)</string>
<string name="summary_pref_socks_password">Пароль</string>
<string name="title_pref_local_dns_port">Порт локальной DNS</string> <string name="title_pref_local_dns_port">Порт локальной DNS</string>
<string name="summary_pref_local_dns_port">Порт локальной DNS</string> <string name="summary_pref_local_dns_port">Порт локальной DNS</string>
@@ -241,6 +251,7 @@
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string> <string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string>
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string> <string name="toast_tg_app_not_found">Приложение Telegram не найдено</string>
<string name="title_privacy_policy">Политика конфиденциальности</string> <string name="title_privacy_policy">Политика конфиденциальности</string>
<string name="title_qr_code">QR-код</string>
<string name="title_about">О приложении</string> <string name="title_about">О приложении</string>
<string name="title_source_code">Исходный код</string> <string name="title_source_code">Исходный код</string>
<string name="title_oss_license">Лицензии открытого исходного кода</string> <string name="title_oss_license">Лицензии открытого исходного кода</string>
@@ -297,6 +308,9 @@
<string name="title_update_config_count">Обновлено профилей: %d</string> <string name="title_update_config_count">Обновлено профилей: %d</string>
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string> <string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
<string name="title_update_subscription_no_subscription">Нет подписок</string> <string name="title_update_subscription_no_subscription">Нет подписок</string>
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
<string name="toast_fragment_not_available">Фрагмент недоступен</string>
<string name="title_locate_selected_config">Найти выбранный профиль</string>
<string name="tasker_start_service">Запуск службы</string> <string name="tasker_start_service">Запуск службы</string>
<string name="tasker_setting_confirm">Подтвердить</string> <string name="tasker_setting_confirm">Подтвердить</string>
@@ -304,7 +318,7 @@
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">Доменная стратегия</string> <string name="routing_settings_domain_strategy">Доменная стратегия</string>
<string name="routing_settings_title">Маршрутизация</string> <string name="routing_settings_title">Маршрутизация</string>
<string name="routing_settings_tips">Введите требуемые домены/IP через запятую</string> <string name="routing_settings_tips">Разделяйте запятой (,). Укажите только одно: domain, ip или process</string>
<string name="routing_settings_save">Сохранить</string> <string name="routing_settings_save">Сохранить</string>
<string name="routing_settings_delete">Очистить</string> <string name="routing_settings_delete">Очистить</string>
<string name="routing_settings_rule_title">Настройка правил маршрутизации</string> <string name="routing_settings_rule_title">Настройка правил маршрутизации</string>
+26 -2
View File
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string> <string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -185,8 +186,11 @@
<string name="title_pref_fake_dns_enabled">Bật FakeDNS</string> <string name="title_pref_fake_dns_enabled">Bật FakeDNS</string>
<string name="summary_pref_fake_dns_enabled">FakeDNS lấy tên miền mục tiêu bằng cách giả mạo DNS (Nhanh hơn, nhưng có thể không hoạt động cho một số ứng dụng).</string> <string name="summary_pref_fake_dns_enabled">FakeDNS lấy tên miền mục tiêu bằng cách giả mạo DNS (Nhanh hơn, nhưng có thể không hoạt động cho một số ứng dụng).</string>
<string name="title_pref_ipv6_enabled">Bật IPv6</string>
<string name="summary_pref_ipv6_enabled">Thêm địa chỉ và tuyến IPv6 vào giao diện VPN</string>
<string name="title_pref_prefer_ipv6">Ưu tiên IPv6</string> <string name="title_pref_prefer_ipv6">Ưu tiên IPv6</string>
<string name="summary_pref_prefer_ipv6">Ưu tiên sử dụng địa chỉ IPv6 cho kết nối và định tuyến.</string> <string name="summary_pref_prefer_ipv6">Ưu tiên địa chỉ IPv6 khi phân giải tên miền</string>
<string name="title_pref_remote_dns">DNS ngoại quốc (UDP / TCP / HTTPS / QUIC) (Không bắt buộc)</string> <string name="title_pref_remote_dns">DNS ngoại quốc (UDP / TCP / HTTPS / QUIC) (Không bắt buộc)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -218,6 +222,12 @@
<string name="title_pref_socks_port">Cổng Proxy Local</string> <string name="title_pref_socks_port">Cổng Proxy Local</string>
<string name="summary_pref_socks_port">Cổng Proxy Local</string> <string name="summary_pref_socks_port">Cổng Proxy Local</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
<string name="title_pref_socks_username">Tên người dùng Proxy Local (Không bắt buộc)</string>
<string name="summary_pref_socks_username">Tên người dùng</string>
<string name="title_pref_socks_password">Mật khẩu Proxy Local (Không bắt buộc)</string>
<string name="summary_pref_socks_password">Mật khẩu</string>
<string name="title_pref_local_dns_port">Cổng Local DNS</string> <string name="title_pref_local_dns_port">Cổng Local DNS</string>
<string name="summary_pref_local_dns_port">Cổng Local DNS</string> <string name="summary_pref_local_dns_port">Cổng Local DNS</string>
@@ -243,6 +253,7 @@
<string name="toast_tg_app_not_found">Không tìm thấy ứng dụng Telegram!</string> <string name="toast_tg_app_not_found">Không tìm thấy ứng dụng Telegram!</string>
<string name="title_privacy_policy">Chính sách bảo mật</string> <string name="title_privacy_policy">Chính sách bảo mật</string>
<string name="title_qr_code">Mã QR</string>
<string name="title_about">Giới thiệu</string> <string name="title_about">Giới thiệu</string>
<string name="title_source_code">Mã nguồn</string> <string name="title_source_code">Mã nguồn</string>
<string name="title_oss_license">Open Source licenses</string> <string name="title_oss_license">Open Source licenses</string>
@@ -299,13 +310,17 @@
<string name="title_update_config_count">Update %d configurations</string> <string name="title_update_config_count">Update %d configurations</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string> <string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string> <string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
<string name="toast_fragment_not_available">Unable to locate current view</string>
<string name="title_locate_selected_config">Locate the selected config</string>
<string name="tasker_start_service">Khởi động v2rayNG</string> <string name="tasker_start_service">Khởi động v2rayNG</string>
<string name="tasker_setting_confirm">Xác nhận</string> <string name="tasker_setting_confirm">Xác nhận</string>
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">Chiến lược tên miền (DomainStrategy)</string> <string name="routing_settings_domain_strategy">Chiến lược tên miền (DomainStrategy)</string>
<string name="routing_settings_title">Cài đặt định tuyến</string> <string name="routing_settings_title">Cài đặt định tuyến</string>
<string name="routing_settings_tips">Phân cách bằng dấu phẩy (,). Có thể tải xuống Rules mặc định để tham khảo ở menu ba chấm.</string> <string name="routing_settings_tips">Phân cách bằng dấu phẩy (,). Chọn một trong: domain, ip hoặc process</string>
<string name="routing_settings_save">Lưu lại</string> <string name="routing_settings_save">Lưu lại</string>
<string name="routing_settings_delete">Xoá</string> <string name="routing_settings_delete">Xoá</string>
<string name="routing_settings_rule_title">Routing Rule Settings</string> <string name="routing_settings_rule_title">Routing Rule Settings</string>
@@ -316,6 +331,15 @@
<string name="routing_settings_import_rulesets_from_qrcode">Nhập bộ quy tắc từ QRcode</string> <string name="routing_settings_import_rulesets_from_qrcode">Nhập bộ quy tắc từ QRcode</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string> <string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string> <string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_domain">domain</string>
<string name="routing_settings_ip">ip</string>
<string name="routing_settings_process">process (Package Name — only supported when using Xray TUN, routeOnly enabled, and Android 10+)</string>
<string name="routing_settings_port">port</string>
<string name="routing_settings_protocol">protocol</string>
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
<string name="routing_settings_network">network</string>
<string name="routing_settings_network_tip">[udp|tcp]</string>
<string name="routing_settings_outbound_tag">outboundTag</string>
<string name="connection_test_pending">Kiểm tra kết nối</string> <string name="connection_test_pending">Kiểm tra kết nối</string>
<string name="connection_test_testing">Đang kiểm tra kết nối mạng...</string> <string name="connection_test_testing">Đang kiểm tra kết nối mạng...</string>
@@ -15,8 +15,8 @@
<string name="toast_permission_denied">无法取得权限</string> <string name="toast_permission_denied">无法取得权限</string>
<string name="toast_permission_denied_notification">无法取得通知权限</string> <string name="toast_permission_denied_notification">无法取得通知权限</string>
<string name="notification_action_more">点击了解更多</string> <string name="notification_action_more">点击了解更多</string>
<string name="toast_services_start">启动服务中</string> <string name="toast_services_start">启动服务</string>
<string name="toast_services_stop">关闭中</string> <string name="toast_services_stop">关闭服务</string>
<string name="toast_services_success">启动服务成功</string> <string name="toast_services_success">启动服务成功</string>
<string name="toast_services_failure">启动服务失败</string> <string name="toast_services_failure">启动服务失败</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string> <string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP 模式</string> <string name="server_lab_xhttp_mode">XHTTP 模式</string>
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string> <string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
@@ -182,8 +183,11 @@
<string name="title_pref_fake_dns_enabled">启用虚拟 DNS</string> <string name="title_pref_fake_dns_enabled">启用虚拟 DNS</string>
<string name="summary_pref_fake_dns_enabled">本地返回虚构解析结果 (减低延时 但个别应用可能无法使用)</string> <string name="summary_pref_fake_dns_enabled">本地返回虚构解析结果 (减低延时 但个别应用可能无法使用)</string>
<string name="title_pref_ipv6_enabled">启用 IPv6</string>
<string name="summary_pref_ipv6_enabled">为 VPN 接口添加 IPv6 地址和路由</string>
<string name="title_pref_prefer_ipv6">IPv6 优先</string> <string name="title_pref_prefer_ipv6">IPv6 优先</string>
<string name="summary_pref_prefer_ipv6">App 优先使用 IPv6 地址连接服务器, 同时开启 VPN 的 IPv6 路由</string> <string name="summary_pref_prefer_ipv6">解析域名时优先使用 IPv6 地址</string>
<string name="title_pref_remote_dns">远程 DNS (udp/tcp/https/quic)(可选)</string> <string name="title_pref_remote_dns">远程 DNS (udp/tcp/https/quic)(可选)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -213,10 +217,16 @@
<string name="title_pref_allow_insecure">跳过证书验证 (allowInsecure)</string> <string name="title_pref_allow_insecure">跳过证书验证 (allowInsecure)</string>
<string name="summary_pref_allow_insecure">传输层安全选 tls 时,默认跳过证书验证 (allowInsecure)</string> <string name="summary_pref_allow_insecure">传输层安全选 tls 时,默认跳过证书验证 (allowInsecure)</string>
<string name="title_pref_socks_port">本地代理端口</string> <string name="title_pref_socks_port">本地代理端口</string>
<string name="summary_pref_socks_port">本地代理端口</string> <string name="summary_pref_socks_port">本地代理端口</string>
<string name="title_pref_dynamic_socks_port">启用动态本地代理端口</string>
<string name="summary_pref_dynamic_socks_port">每次生成 inbound 时随机生成一个本地代理端口</string>
<string name="title_pref_socks_username">本地代理用户名 (可选)</string>
<string name="summary_pref_socks_username">用户名</string>
<string name="title_pref_socks_password">本地代理密码 (可选)</string>
<string name="summary_pref_socks_password">密码</string>
<string name="title_pref_local_dns_port">本地 DNS 端口</string> <string name="title_pref_local_dns_port">本地 DNS 端口</string>
<string name="summary_pref_local_dns_port">本地 DNS 端口</string> <string name="summary_pref_local_dns_port">本地 DNS 端口</string>
<string name="title_pref_confirm_remove">删除配置确认</string> <string name="title_pref_confirm_remove">删除配置确认</string>
@@ -240,6 +250,7 @@
<string name="summary_pref_tg_group">加入 Telegram Group</string> <string name="summary_pref_tg_group">加入 Telegram Group</string>
<string name="toast_tg_app_not_found">未找到 Telegram app</string> <string name="toast_tg_app_not_found">未找到 Telegram app</string>
<string name="title_privacy_policy">隐私权政策</string> <string name="title_privacy_policy">隐私权政策</string>
<string name="title_qr_code">二维码</string>
<string name="title_about">关于</string> <string name="title_about">关于</string>
<string name="title_source_code">源代码</string> <string name="title_source_code">源代码</string>
<string name="title_oss_license">Open Source licenses</string> <string name="title_oss_license">Open Source licenses</string>
@@ -297,13 +308,17 @@
<string name="title_update_config_count">更新 %d 个配置</string> <string name="title_update_config_count">更新 %d 个配置</string>
<string name="title_update_subscription_result">更新了 %1$d 个配置(%2$d 个成功,%3$d 个失败,%4$d 个跳过)</string> <string name="title_update_subscription_result">更新了 %1$d 个配置(%2$d 个成功,%3$d 个失败,%4$d 个跳过)</string>
<string name="title_update_subscription_no_subscription">无订阅</string> <string name="title_update_subscription_no_subscription">无订阅</string>
<string name="toast_server_not_found_in_group">当前分组中未找到选中的服务器</string>
<string name="toast_fragment_not_available">无法定位当前视图</string>
<string name="title_locate_selected_config">定位所选配置</string>
<string name="tasker_start_service">启动服务</string> <string name="tasker_start_service">启动服务</string>
<string name="tasker_setting_confirm">确定</string> <string name="tasker_setting_confirm">确定</string>
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">域名策略</string> <string name="routing_settings_domain_strategy">域名策略</string>
<string name="routing_settings_title">路由设置</string> <string name="routing_settings_title">路由设置</string>
<string name="routing_settings_tips">用逗号(,)隔开, domain 和 ip 二选一填写</string> <string name="routing_settings_tips">用逗号(,)分隔,选择填写 domain、ip 或 process 中的一项</string>
<string name="routing_settings_save">保存</string> <string name="routing_settings_save">保存</string>
<string name="routing_settings_delete">清空</string> <string name="routing_settings_delete">清空</string>
<string name="routing_settings_rule_title">路由规则设置</string> <string name="routing_settings_rule_title">路由规则设置</string>
@@ -314,6 +329,15 @@
<string name="routing_settings_import_rulesets_from_qrcode">从 QRcode 导入规则集</string> <string name="routing_settings_import_rulesets_from_qrcode">从 QRcode 导入规则集</string>
<string name="routing_settings_export_rulesets_to_clipboard">导出规则集至剪贴板</string> <string name="routing_settings_export_rulesets_to_clipboard">导出规则集至剪贴板</string>
<string name="routing_settings_locked">锁定中,导入预设时不删除此规则</string> <string name="routing_settings_locked">锁定中,导入预设时不删除此规则</string>
<string name="routing_settings_domain">domain</string>
<string name="routing_settings_ip">ip</string>
<string name="routing_settings_process">process (包名 — 仅在使用 Xray 的 TUN、开启 routeOnly 且 Android 10+ 时可用)</string>
<string name="routing_settings_port">port</string>
<string name="routing_settings_protocol">protocol</string>
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
<string name="routing_settings_network">network</string>
<string name="routing_settings_network_tip">[udp|tcp]</string>
<string name="routing_settings_outbound_tag">outboundTag</string>
<string name="connection_test_pending">"检查网络连接"</string> <string name="connection_test_pending">"检查网络连接"</string>
<string name="connection_test_testing">"测试中…"</string> <string name="connection_test_testing">"测试中…"</string>
@@ -112,6 +112,7 @@
<string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string> <string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP 模式</string> <string name="server_lab_xhttp_mode">XHTTP 模式</string>
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</string>
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string> <string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
@@ -184,8 +185,11 @@
<string name="title_pref_fake_dns_enabled">啟用假 DNS</string> <string name="title_pref_fake_dns_enabled">啟用假 DNS</string>
<string name="summary_pref_fake_dns_enabled">本機退回假解析結果 (減低延時,但個別應用可能無法使用)</string> <string name="summary_pref_fake_dns_enabled">本機退回假解析結果 (減低延時,但個別應用可能無法使用)</string>
<string name="title_pref_ipv6_enabled">啟用 IPv6</string>
<string name="summary_pref_ipv6_enabled">為 VPN 介面新增 IPv6 位址和路由</string>
<string name="title_pref_prefer_ipv6">IPv6 偏好</string> <string name="title_pref_prefer_ipv6">IPv6 偏好</string>
<string name="summary_pref_prefer_ipv6">App 優先使用 IPv6 位址連線伺服器,同時開啟 VPN 的 IPv6 路由</string> <string name="summary_pref_prefer_ipv6">解析網域名稱時優先使用 IPv6 位址</string>
<string name="title_pref_remote_dns">遠端 DNS (udp/tcp/https/quic)(可選)</string> <string name="title_pref_remote_dns">遠端 DNS (udp/tcp/https/quic)(可選)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -216,6 +220,12 @@
<string name="title_pref_socks_port">本地代理埠</string> <string name="title_pref_socks_port">本地代理埠</string>
<string name="summary_pref_socks_port">本地代理埠</string> <string name="summary_pref_socks_port">本地代理埠</string>
<string name="title_pref_dynamic_socks_port">啟用動態本地代理埠</string>
<string name="summary_pref_dynamic_socks_port">每次產生 inbound 時隨機產生一個本地代理埠</string>
<string name="title_pref_socks_username">本地代理使用者名稱 (選擇)</string>
<string name="summary_pref_socks_username">使用者名稱</string>
<string name="title_pref_socks_password">本地代理密碼 (選擇)</string>
<string name="summary_pref_socks_password">密碼</string>
<string name="title_pref_local_dns_port">本機 DNS 埠</string> <string name="title_pref_local_dns_port">本機 DNS 埠</string>
<string name="summary_pref_local_dns_port">本機 DNS 埠</string> <string name="summary_pref_local_dns_port">本機 DNS 埠</string>
@@ -241,6 +251,7 @@
<string name="summary_pref_tg_group">加入 Telegram 群組</string> <string name="summary_pref_tg_group">加入 Telegram 群組</string>
<string name="toast_tg_app_not_found">未找到 Telegram 應用程式</string> <string name="toast_tg_app_not_found">未找到 Telegram 應用程式</string>
<string name="title_privacy_policy">隱私權政策</string> <string name="title_privacy_policy">隱私權政策</string>
<string name="title_qr_code">QR 碼</string>
<string name="title_about">關於</string> <string name="title_about">關於</string>
<string name="title_source_code">原始碼</string> <string name="title_source_code">原始碼</string>
<string name="title_oss_license">Open Source licenses</string> <string name="title_oss_license">Open Source licenses</string>
@@ -297,13 +308,17 @@
<string name="title_update_config_count">更新 %d 個配置</string> <string name="title_update_config_count">更新 %d 個配置</string>
<string name="title_update_subscription_result">更新了 %1$d 個配置(%2$d 個成功,%3$d 個失敗,%4$d 個跳過)</string> <string name="title_update_subscription_result">更新了 %1$d 個配置(%2$d 個成功,%3$d 個失敗,%4$d 個跳過)</string>
<string name="title_update_subscription_no_subscription">無訂閱</string> <string name="title_update_subscription_no_subscription">無訂閱</string>
<string name="toast_server_not_found_in_group">當前分組中未找到選中的伺服器</string>
<string name="toast_fragment_not_available">定位所選配置</string>
<string name="title_locate_selected_config">Locate the selected config</string>
<string name="tasker_start_service">啟動服務</string> <string name="tasker_start_service">啟動服務</string>
<string name="tasker_setting_confirm">確定</string> <string name="tasker_setting_confirm">確定</string>
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">網域策略</string> <string name="routing_settings_domain_strategy">網域策略</string>
<string name="routing_settings_title">轉送設定</string> <string name="routing_settings_title">轉送設定</string>
<string name="routing_settings_tips">以半形逗號「,」分隔,domain和ip二選一填寫</string> <string name="routing_settings_tips">用逗號(,)分隔,選擇填寫 domain、ip 或 process 中的一項</string>
<string name="routing_settings_save">儲存</string> <string name="routing_settings_save">儲存</string>
<string name="routing_settings_delete">清除</string> <string name="routing_settings_delete">清除</string>
<string name="routing_settings_rule_title">路由規則設定</string> <string name="routing_settings_rule_title">路由規則設定</string>
@@ -314,6 +329,15 @@
<string name="routing_settings_import_rulesets_from_qrcode">從二維碼匯入規則集</string> <string name="routing_settings_import_rulesets_from_qrcode">從二維碼匯入規則集</string>
<string name="routing_settings_export_rulesets_to_clipboard">匯出規則集至剪貼簿</string> <string name="routing_settings_export_rulesets_to_clipboard">匯出規則集至剪貼簿</string>
<string name="routing_settings_locked">鎖定中,匯入預設時不刪除此規則</string> <string name="routing_settings_locked">鎖定中,匯入預設時不刪除此規則</string>
<string name="routing_settings_domain">domain</string>
<string name="routing_settings_ip">ip</string>
<string name="routing_settings_process">process (套件名稱 — 僅在使用 Xray 的 TUN、開啟 routeOnly 且 Android 10+ 時可用)</string>
<string name="routing_settings_port">port</string>
<string name="routing_settings_protocol">protocol</string>
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
<string name="routing_settings_network">network</string>
<string name="routing_settings_network_tip">[udp|tcp]</string>
<string name="routing_settings_outbound_tag">outboundTag</string>
<string name="connection_test_pending">"測試連線能力"</string> <string name="connection_test_pending">"測試連線能力"</string>
<string name="connection_test_testing">"測試中……"</string> <string name="connection_test_testing">"測試中……"</string>
@@ -7,5 +7,6 @@
<dimen name="view_height_dp36">36dp</dimen> <dimen name="view_height_dp36">36dp</dimen>
<dimen name="view_height_dp48">48dp</dimen> <dimen name="view_height_dp48">48dp</dimen>
<dimen name="view_height_dp64">64dp</dimen> <dimen name="view_height_dp64">64dp</dimen>
<dimen name="view_height_dp120">120dp</dimen>
<dimen name="view_height_dp160">160dp</dimen> <dimen name="view_height_dp160">160dp</dimen>
</resources> </resources>
+26 -11
View File
@@ -68,7 +68,7 @@
<string name="server_lab_path_kcp">kcp seed</string> <string name="server_lab_path_kcp">kcp seed</string>
<string name="server_lab_path_grpc">gRPC serviceName</string> <string name="server_lab_path_grpc">gRPC serviceName</string>
<string name="server_lab_stream_security">TLS</string> <string name="server_lab_stream_security">TLS</string>
<string name="server_lab_stream_fingerprint" translatable="false">Fingerprint</string> <string name="server_lab_stream_fingerprint">Fingerprint</string>
<string name="server_lab_stream_alpn">Alpn</string> <string name="server_lab_stream_alpn">Alpn</string>
<string name="server_lab_allow_insecure">allowInsecure</string> <string name="server_lab_allow_insecure">allowInsecure</string>
<string name="server_lab_sni">SNI</string> <string name="server_lab_sni">SNI</string>
@@ -113,6 +113,7 @@
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string> <string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
@@ -186,8 +187,11 @@
<string name="title_pref_fake_dns_enabled">Enable fake DNS</string> <string name="title_pref_fake_dns_enabled">Enable fake DNS</string>
<string name="summary_pref_fake_dns_enabled">Local DNS returns fake IP addresses (faster, but it may not work for some apps)</string> <string name="summary_pref_fake_dns_enabled">Local DNS returns fake IP addresses (faster, but it may not work for some apps)</string>
<string name="title_pref_ipv6_enabled">Enable IPv6</string>
<string name="summary_pref_ipv6_enabled">Add IPv6 address and routes to the VPN interface</string>
<string name="title_pref_prefer_ipv6">Prefer IPv6</string> <string name="title_pref_prefer_ipv6">Prefer IPv6</string>
<string name="summary_pref_prefer_ipv6">Enable IPv6 routes and Prefer IPv6 addresses</string> <string name="summary_pref_prefer_ipv6">Prefer IPv6 addresses when resolving domain names</string>
<string name="title_pref_remote_dns">Remote DNS (udp/tcp/https/quic)(Optional)</string> <string name="title_pref_remote_dns">Remote DNS (udp/tcp/https/quic)(Optional)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
@@ -221,6 +225,12 @@
<string name="title_pref_socks_port">Local proxy port</string> <string name="title_pref_socks_port">Local proxy port</string>
<string name="summary_pref_socks_port">Local proxy port</string> <string name="summary_pref_socks_port">Local proxy port</string>
<string name="title_pref_dynamic_socks_port">Enable dynamic local proxy port</string>
<string name="summary_pref_dynamic_socks_port">Generate a random local proxy port each time the inbound is created</string>
<string name="title_pref_socks_username">Local proxy username (optional)</string>
<string name="summary_pref_socks_username">Username</string>
<string name="title_pref_socks_password">Local proxy password (optional)</string>
<string name="summary_pref_socks_password">Password</string>
<string name="title_pref_local_dns_port">Local DNS port</string> <string name="title_pref_local_dns_port">Local DNS port</string>
<string name="summary_pref_local_dns_port">Local DNS port</string> <string name="summary_pref_local_dns_port">Local DNS port</string>
@@ -303,6 +313,9 @@
<string name="title_update_config_count">Update %d configs</string> <string name="title_update_config_count">Update %d configs</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string> <string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="title_update_subscription_no_subscription">No subscriptions</string> <string name="title_update_subscription_no_subscription">No subscriptions</string>
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
<string name="toast_fragment_not_available">Unable to locate current view</string>
<string name="title_locate_selected_config">Locate the selected config</string>
<string name="tasker_start_service">Start Service</string> <string name="tasker_start_service">Start Service</string>
<string name="tasker_setting_confirm">Confirm</string> <string name="tasker_setting_confirm">Confirm</string>
@@ -310,7 +323,7 @@
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">Domain strategy</string> <string name="routing_settings_domain_strategy">Domain strategy</string>
<string name="routing_settings_title">Routing Settings</string> <string name="routing_settings_title">Routing Settings</string>
<string name="routing_settings_tips">Separated by commas(,), choose domain or ip</string> <string name="routing_settings_tips">Separated by commas(,), \nchoose domain or ip or process</string>
<string name="routing_settings_save">Save</string> <string name="routing_settings_save">Save</string>
<string name="routing_settings_delete">Clear</string> <string name="routing_settings_delete">Clear</string>
<string name="routing_settings_rule_title">Routing Rule Settings</string> <string name="routing_settings_rule_title">Routing Rule Settings</string>
@@ -321,14 +334,16 @@
<string name="routing_settings_import_rulesets_from_qrcode">Import ruleset from QRcode</string> <string name="routing_settings_import_rulesets_from_qrcode">Import ruleset from QRcode</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string> <string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string> <string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_domain" translatable="false">domain</string> <string name="routing_settings_domain">domain</string>
<string name="routing_settings_ip" translatable="false">ip</string> <string name="routing_settings_ip">ip</string>
<string name="routing_settings_port" translatable="false">port</string> <string name="routing_settings_process">process (Package Name — only supported when using Xray TUN, routeOnly enabled, and Android 10+)</string>
<string name="routing_settings_protocol" translatable="false">protocol</string> <string name="routing_settings_port">port</string>
<string name="routing_settings_protocol_tip" translatable="false">[http,tls,bittorrent]</string> <string name="routing_settings_protocol">protocol</string>
<string name="routing_settings_network" translatable="false">network</string> <string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
<string name="routing_settings_network_tip" translatable="false">[udp|tcp]</string> <string name="routing_settings_network">network</string>
<string name="routing_settings_outbound_tag" translatable="false">outboundTag</string> <string name="routing_settings_network_tip">[udp|tcp]</string>
<string name="routing_settings_outbound_tag_hint" translatable="false">proxy / direct / block / &lt;remarks&gt;</string>
<string name="routing_settings_outbound_tag">outboundTag</string>
<string name="connection_test_pending">Check Connectivity</string> <string name="connection_test_pending">Check Connectivity</string>
<string name="connection_test_testing">Testing…</string> <string name="connection_test_testing">Testing…</string>
@@ -47,6 +47,11 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/title_vpn_settings"> <PreferenceCategory android:title="@string/title_vpn_settings">
<CheckBoxPreference
android:key="pref_ipv6_enabled"
android:summary="@string/summary_pref_ipv6_enabled"
android:title="@string/title_pref_ipv6_enabled" />
<CheckBoxPreference <CheckBoxPreference
android:key="pref_prefer_ipv6" android:key="pref_prefer_ipv6"
android:summary="@string/summary_pref_prefer_ipv6" android:summary="@string/summary_pref_prefer_ipv6"
@@ -157,6 +162,23 @@
android:summary="10808" android:summary="10808"
android:title="@string/title_pref_socks_port" /> android:title="@string/title_pref_socks_port" />
<CheckBoxPreference
android:defaultValue="false"
android:key="pref_dynamic_socks_port"
android:summary="@string/summary_pref_dynamic_socks_port"
android:title="@string/title_pref_dynamic_socks_port" />
<EditTextPreference
android:key="pref_socks_username"
android:summary="@string/summary_pref_socks_username"
android:title="@string/title_pref_socks_username" />
<EditTextPreference
android:inputType="textPassword"
android:key="pref_socks_password"
android:summary="@string/summary_pref_socks_password"
android:title="@string/title_pref_socks_password" />
<EditTextPreference <EditTextPreference
android:key="pref_remote_dns" android:key="pref_remote_dns"
android:summary="@string/summary_pref_remote_dns" android:summary="@string/summary_pref_remote_dns"
@@ -28,4 +28,32 @@
android:targetPackage="com.v2ray.ang" /> android:targetPackage="com.v2ray.ang" />
<categories android:name="android.shortcut.conversation" /> <categories android:name="android.shortcut.conversation" />
</shortcut> </shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/ic_qu_start_24dp"
android:shortcutDisabledMessage="@string/toast_services_start"
android:shortcutId="shortcuts_start"
android:shortcutLongLabel="@string/toast_services_start"
android:shortcutShortLabel="@string/toast_services_start">
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.v2ray.ang.ui.ScStartActivity"
android:targetPackage="com.v2ray.ang" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/ic_qu_stop_24dp"
android:shortcutDisabledMessage="@string/toast_services_stop"
android:shortcutId="shortcuts_stop"
android:shortcutLongLabel="@string/toast_services_stop"
android:shortcutShortLabel="@string/toast_services_stop">
<intent
android:action="android.intent.action.VIEW"
android:targetClass="com.v2ray.ang.ui.ScStopActivity"
android:targetPackage="com.v2ray.ang" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts> </shortcuts>
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import android.util.Base64 import android.util.Base64
import android.util.Log import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import org.junit.After import org.junit.After

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