Compare commits

..

23 Commits

Author SHA1 Message Date
2dust d64fc627bb up 2.2.0 2026-05-23 15:36:30 +08:00
2dust 6934ab3e5d Bump SDK to 37
Update compileSdk and targetSdk to 37. Adjust AndroidManifest: remove legacy storage read permissions, add tools:ignore for FOREGROUND_SERVICE policy and keep FOREGROUND_SERVICE_SPECIAL_USE with minSdk 34. Wrap ContextCompat.startForegroundService with SecurityException handling and special-case ForegroundServiceStartNotAllowedException (logged and rethrown as IllegalStateException) to surface permission/OS restrictions. Remove the READ_STORAGE PermissionType and related runtime permission request in HelperBaseActivity (file chooser now launches directly). These changes align permissions and service startup behavior with newer Android SDK requirements.
2026-05-23 14:56:45 +08:00
2dust 028d13aef8 Add SettingsManager.getProfileRemarks and use it 2026-05-23 11:09:46 +08:00
2dust 4a78c165bc Refactor core config, routing and balancer logic
Rework CoreConfigContextBuilder and CoreConfigManager to centralize and simplify outbound resolution and runtime config assembly. Key changes: handle CUSTOM profiles early, introduce ResolvedOutbound and resolvedOutbounds list, resolve routing-target outbounds separately, and unify normal/group/chain build flows into buildUnifiedConfig. Routing, DNS, inbounds, outbounds and balancer logic were modularized (configureInbounds/configureRouting/configureDns/configureFakeDns/configureLocalDns/etc.), balancer strategies and observability are consolidated, and domain pre-resolution/hosts handling preserved. Also added new balancer tag naming (TAG_BALANCER_PRE) and small API/utility adjustments across CoreOutboundBuilder, DTOs and enums to support the new flow. These changes improve clarity of the build pipeline, make custom/routing outbounds handling explicit, and prepare for richer policy-group balancing behavior.
2026-05-23 10:49:20 +08:00
2dust c1033a006f Use safe JSON parsing and add fromJsonSafe 2026-05-21 14:35:34 +08:00
DHR60 e8a42013c0 Remove freedom.domainStrategy (#5656) 2026-05-20 14:10:20 +08:00
vemneyy 94e5263250 Added QUIC sniffig option in destOverride to default config (#5639) 2026-05-20 14:06:56 +08:00
2dust 7c68aa9d83 Parse SOCKS userInfo without unnecessary decoding
https://github.com/2dust/v2rayNG/issues/5657
2026-05-20 13:59:54 +08:00
2dust ef13e336da up 2.1.8 2026-05-19 16:56:12 +08:00
2dust d16cb21059 Update AndroidLibXrayLite 2026-05-19 11:37:28 +08:00
2dust 237837e7be Refactor speed notification and add outbound stats
Add OutboundTrafficStat DTO and a CoreServiceManager.queryAllOutboundTrafficStats() method to fetch/reset all outbound counters in one core call and parse Go-side payload. Remove ProfileItem.getAllOutboundTags() and simplify NotificationManager API (startSpeedNotification()/stopSpeedNotification()) to use aggregated stats via the new core query. Replace per-tag polling with a single aggregated update loop (updateSpeedNotificationOnce) to reduce IPC and CPU usage, and update CoreServiceManager call sites accordingly.
2026-05-19 11:33:03 +08:00
2dust 2a6edd98be Add SOCKS4/5 and map-based parser lookup
https://github.com/2dust/v2rayNG/issues/5649
2026-05-19 09:48:49 +08:00
DHR60 9238b80675 Fix (#5651) 2026-05-18 09:18:53 +08:00
skl256 bb5e6f1959 Fix https://github.com/2dust/v2rayNG/issues/5619 (#5632)
Added "Level 0" matching to perform a full check of remarks, server, port, and password before falling back to partial matches. This ensures that identical profiles with the same remarks are correctly identified
2026-05-13 18:48:22 +08:00
2dust 1cc0865411 Add profile remark dropdowns to editor
Replace plain EditText fields for pre/next profile with AutoCompleteTextView + dropdown ImageButton in activity_sub_edit.xml and wire them up in SubEditActivity.kt. New setupProfileRemarkInputs() loads distinct non-blank server remarks from MmkvManager, sets an ArrayAdapter (threshold=0) and shows the dropdown when the field or button is clicked to make selecting existing profile remarks easier. Also adds necessary imports.
2026-05-12 11:37:47 +08:00
2dust cb8d979f23 Add dto.entities package 2026-05-12 11:25:37 +08:00
2dust 5932b0db29 Add UrlContentRequest and refactor HttpUtil
Introduce UrlContentRequest DTO to encapsulate url, timeout, httpPort, proxy credentials and userAgent. Refactor HttpUtil APIs (getUrlContent, getUrlContentWithUserAgent, downloadToFile) to accept UrlContentRequest and update callers accordingly in AngConfigManager, SpeedtestManager, UpdateCheckerManager, PerAppProxyActivity and UserAssetViewModel. This centralizes HTTP request parameters, reduces duplicated parameter lists, and preserves existing behaviors (redirect handling, userAgent fallback, proxy auth).
2026-05-12 11:05:11 +08:00
Hossein Abaspanah dbe4fe78b5 Update Luri Bakhtiari translation (#5628)
* Update Luri Bakhtiari translation

* Update strings new.xml

* Update strings.xml
2026-05-12 08:57:54 +08:00
2dust b5bcd81ae4 up 2.1.7 2026-05-10 14:17:02 +08:00
2dust d45ccd7953 Update AndroidLibXrayLite 2026-05-10 14:16:54 +08:00
solokot 6815563902 Update Russian translation (#5621) 2026-05-10 14:07:00 +08:00
2dust 7c38ad7c57 Improve WireGuard addresses 2026-05-09 16:48:09 +08:00
2dust 885aeb384e Refactor TCP HTTP request builder, relax Pragma
https://github.com/2dust/v2rayNG/issues/5623
2026-05-09 16:35:11 +08:00
68 changed files with 1234 additions and 874 deletions
+4 -4
View File
@@ -6,14 +6,14 @@ plugins {
android { android {
namespace = "com.v2ray.ang" namespace = "com.v2ray.ang"
compileSdk = 36 compileSdk = 37
defaultConfig { defaultConfig {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 37
versionCode = 726 versionCode = 730
versionName = "2.1.6" versionName = "2.2.0"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
+2 -3
View File
@@ -30,15 +30,14 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"
tools:ignore="ForegroundServicesPolicy" />
<uses-permission <uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
android:minSdkVersion="34" /> android:minSdkVersion="34" />
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> --> <!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
@@ -30,7 +30,8 @@
"enabled": true, "enabled": true,
"destOverride": [ "destOverride": [
"http", "http",
"tls" "tls",
"quic"
] ]
} }
} }
@@ -73,8 +74,10 @@
}, },
{ {
"protocol": "freedom", "protocol": "freedom",
"settings": { "streamSettings": {
"domainStrategy": "UseIP" "sockopt": {
"domainStrategy": "UseIP"
}
}, },
"tag": "direct" "tag": "direct"
}, },
@@ -30,7 +30,8 @@
"enabled": true, "enabled": true,
"destOverride": [ "destOverride": [
"http", "http",
"tls" "tls",
"quic"
] ]
} }
}, },
@@ -46,7 +47,8 @@
"enabled": true, "enabled": true,
"destOverride": [ "destOverride": [
"http", "http",
"tls" "tls",
"quic"
] ]
} }
} }
@@ -89,8 +91,10 @@
}, },
{ {
"protocol": "freedom", "protocol": "freedom",
"settings": { "streamSettings": {
"domainStrategy": "UseIP" "sockopt": {
"domainStrategy": "UseIP"
}
}, },
"tag": "direct" "tag": "direct"
}, },
@@ -105,7 +105,8 @@ object AppConfig {
const val TAG_FRAGMENT = "fragment" const val TAG_FRAGMENT = "fragment"
const val TAG_DNS = "dns-module" const val TAG_DNS = "dns-module"
const val TAG_DOMESTIC_DNS = "domestic-dns" const val TAG_DOMESTIC_DNS = "domestic-dns"
const val TAG_BALANCER = "proxy-round" const val TAG_BALANCER = "balancer-main"
const val TAG_BALANCER_PRE = "balancer"
/** Network-related constants. */ /** Network-related constants. */
const val UPLINK = "uplink" const val UPLINK = "uplink"
@@ -180,6 +181,8 @@ object AppConfig {
const val CUSTOM = "" const val CUSTOM = ""
const val SHADOWSOCKS = "ss://" const val SHADOWSOCKS = "ss://"
const val SOCKS = "socks://" const val SOCKS = "socks://"
const val SOCKS4 = "socks4://"
const val SOCKS5 = "socks5://"
const val HTTP = "http://" const val HTTP = "http://"
const val VLESS = "vless://" const val VLESS = "vless://"
const val TROJAN = "trojan://" const val TROJAN = "trojan://"
@@ -1,6 +1,6 @@
package com.v2ray.ang.contracts package com.v2ray.ang.contracts
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
interface MainAdapterListener : BaseAdapterListener { interface MainAdapterListener : BaseAdapterListener {
@@ -3,7 +3,7 @@ package com.v2ray.ang.core
import android.content.Context import android.content.Context
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.CoreConfigContext import com.v2ray.ang.dto.CoreConfigContext
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.CoreResolvedType import com.v2ray.ang.enums.CoreResolvedType
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
@@ -13,58 +13,128 @@ import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
/** /**
* Builds [com.v2ray.ang.dto.CoreConfigContext] from the selected profile. * Build runtime context from the selected profile.
* Keeps parsing and resolution logic out of [CoreConfigManager]. *
* All outbound type analysis is completed here for both the selected profile
* and routing targets. Custom profiles are returned immediately without
* entering the normal analysis flow.
*/ */
object CoreConfigContextBuilder { object CoreConfigContextBuilder {
/** Loads profile by guid and returns a resolved runtime context. */ /**
* Load one profile and produce a fully analyzed context.
*
* Null is returned only when the selected profile cannot be loaded.
*/
fun build(context: Context, guid: String): CoreConfigContext? { fun build(context: Context, guid: String): CoreConfigContext? {
val config = MmkvManager.decodeServerConfig(guid) ?: return null val config = MmkvManager.decodeServerConfig(guid) ?: return null
// CUSTOM: return immediately — CoreConfigManager handles this path on its own.
if (config.configType == EConfigType.CUSTOM) { if (config.configType == EConfigType.CUSTOM) {
return CoreConfigContext( return CoreConfigContext(context = context, guid = guid, isCustom = true)
context = context,
guid = guid,
selectedProfile = config,
resolvedProfiles = listOf(config),
resolvedType = CoreResolvedType.CUSTOM,
)
} }
// Pre-resolve custom outbound profiles from routing rulesets // Step 1: Resolve the main outbound (always tag = TAG_PROXY).
val customOutbounds = resolveCustomOutbounds() val primaryResolvedOutbound = resolveOutbound(AppConfig.TAG_PROXY, config) ?: run {
LogUtil.e(AppConfig.TAG, "Failed to resolve main outbound for '${config.remarks}'")
return null
}
// Determine resolved profiles and type based on config type // Step 2: Resolve all non-builtin routing outbound tags.
val (resolvedProfiles, resolvedType) = when (config.configType) { val routingResolvedOutbounds = resolveRoutingOutbounds()
EConfigType.POLICYGROUP -> {
val profiles = resolvePolicyGroupProfiles(config) return CoreConfigContext(
Pair(profiles, CoreResolvedType.POLICYGROUP) context = context,
} guid = guid,
resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds,
)
}
/**
* Resolve one outbound target into a normalized outbound entry.
*
* Custom profiles are ignored at this stage and produce no entry.
*/
private fun resolveOutbound(tag: String, profile: ProfileItem): CoreConfigContext.ResolvedOutbound? {
if (profile.configType == EConfigType.CUSTOM) {
return null
}
val (resolvedProfiles, resolvedType) = when (profile.configType) {
EConfigType.POLICYGROUP -> Pair(
resolvePolicyGroupProfiles(profile),
CoreResolvedType.POLICYGROUP,
)
EConfigType.PROXYCHAIN -> { EConfigType.PROXYCHAIN -> {
val profiles = resolveProxyChainProfiles(config) val chainProfiles = resolveProxyChainProfiles(profile)
Pair(profiles, CoreResolvedType.PROXYCHAIN) val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
Pair(chainProfiles, type)
} }
else -> { else -> {
val chainProfiles = resolveProxyChainProfilesFromGroup(config) val chainProfiles = resolveProxyChainProfilesFromGroup(profile)
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
Pair(chainProfiles, type) Pair(chainProfiles, type)
} }
} }
// Create context with common fields return CoreConfigContext.ResolvedOutbound(
return CoreConfigContext( tag = tag,
context = context, profile = profile,
guid = guid,
selectedProfile = config,
resolvedProfiles = resolvedProfiles, resolvedProfiles = resolvedProfiles,
resolvedType = resolvedType, resolvedType = resolvedType,
customOutboundProfiles = customOutbounds,
) )
} }
/** Resolves policy-group members with the same filters as runtime build. */ /**
* Collect and resolve non-builtin routing targets from enabled rules.
*
* Invalid or empty targets are skipped and handled by fallback logic later.
*/
private fun resolveRoutingOutbounds(): List<CoreConfigContext.ResolvedOutbound> {
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return emptyList()
val resolvedOutbounds = mutableListOf<CoreConfigContext.ResolvedOutbound>()
val processedTags = mutableSetOf<String>()
try {
rulesetItems
.filter { it.enabled }
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
.filter { tag -> tag !in AppConfig.BUILTIN_OUTBOUND_TAGS }
.distinct()
.forEach { tag ->
if (tag in processedTags) {
return@forEach
}
processedTags.add(tag)
try {
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
LogUtil.w(AppConfig.TAG, "Routing tag '$tag' has no matching profile — will fall back to proxy at routing time")
return@forEach
}
val resolvedOutbound = resolveOutbound(tag, profile) ?: run {
LogUtil.w(AppConfig.TAG, "Cannot use CUSTOM profile as routing outbound for tag '$tag', skipping")
return@forEach
}
if (resolvedOutbound.resolvedProfiles.isEmpty()) {
LogUtil.w(AppConfig.TAG, "Routing outbound '$tag' resolved to empty list, skipping")
return@forEach
}
resolvedOutbounds.add(resolvedOutbound)
LogUtil.d(AppConfig.TAG, "Resolved routing outbound: tag='$tag', type='${resolvedOutbound.resolvedType}', profiles=${resolvedOutbound.resolvedProfiles.size}")
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve routing outbound for tag '$tag', skipping", e)
}
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve routing outbounds from rulesets", e)
}
return resolvedOutbounds
}
private fun resolvePolicyGroupProfiles(config: ProfileItem): List<ProfileItem> { private fun resolvePolicyGroupProfiles(config: ProfileItem): List<ProfileItem> {
try { try {
val serverList = MmkvManager.decodeAllServerList() val serverList = MmkvManager.decodeAllServerList()
@@ -98,12 +168,11 @@ object CoreConfigContextBuilder {
.filter { it.configType != EConfigType.PROXYCHAIN } .filter { it.configType != EConfigType.PROXYCHAIN }
.toList() .toList()
} catch (e: Exception) { } catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for config '${config.remarks}'", e) LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for '${config.remarks}'", e)
return listOf(config) return listOf(config)
} }
} }
/** Resolves proxy-chain members with the same filters as runtime build. */
private fun resolveProxyChainProfiles(config: ProfileItem): List<ProfileItem> { private fun resolveProxyChainProfiles(config: ProfileItem): List<ProfileItem> {
if (config.proxyChainProfiles.isNullOrBlank()) { if (config.proxyChainProfiles.isNullOrBlank()) {
return listOf(config) return listOf(config)
@@ -121,19 +190,17 @@ object CoreConfigContextBuilder {
.toList() .toList()
.reversed() .reversed()
} catch (e: Exception) { } catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles for config '${config.remarks}'", e) LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles for '${config.remarks}'", e)
return listOf(config) return listOf(config)
} }
} }
/** /**
* Resolves chain nodes in fixed order: next -> current -> prev. * Resolve chain nodes from subscription neighbors in order: next, current, prev.
* If chain cannot be built, caller treats result as normal mode. *
* When no chain is available, return a single-node result.
*/ */
private fun resolveProxyChainProfilesFromGroup(config: ProfileItem): List<ProfileItem> { private fun resolveProxyChainProfilesFromGroup(config: ProfileItem): List<ProfileItem> {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
return listOf(config)
}
if (config.subscriptionId.isEmpty()) { if (config.subscriptionId.isEmpty()) {
return listOf(config) return listOf(config)
} }
@@ -141,56 +208,13 @@ object CoreConfigContextBuilder {
try { try {
val subItem = MmkvManager.decodeSubscription(config.subscriptionId) ?: return listOf(config) val subItem = MmkvManager.decodeSubscription(config.subscriptionId) ?: return listOf(config)
val resolved = mutableListOf<ProfileItem>() val resolved = mutableListOf<ProfileItem>()
// Keep the same practical chain order as current runtime assembly:
// next -> current -> prev
SettingsManager.getServerViaRemarks(subItem.nextProfile)?.let { resolved.add(it) } SettingsManager.getServerViaRemarks(subItem.nextProfile)?.let { resolved.add(it) }
resolved.add(config) resolved.add(config)
SettingsManager.getServerViaRemarks(subItem.prevProfile)?.let { resolved.add(it) } SettingsManager.getServerViaRemarks(subItem.prevProfile)?.let { resolved.add(it) }
return resolved return resolved
} catch (e: Exception) { } catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles from group for config '${config.remarks}'", e) LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain from group for '${config.remarks}'", e)
return listOf(config) return listOf(config)
} }
} }
/**
* Resolves custom outbound profiles from routing rulesets.
* Scans rulesets for non-builtin outbound tags and looks up matching profiles by remarks.
* Returns a map of tag -> ProfileItem for independent processing.
*/
private fun resolveCustomOutbounds(): Map<String, ProfileItem> {
val customMap = mutableMapOf<String, ProfileItem>()
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return customMap
try {
val processedTags = mutableSetOf<String>()
rulesetItems
.filter { it.enabled }
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
.filter { tag -> tag !in AppConfig.BUILTIN_OUTBOUND_TAGS }
.distinct()
.forEach { tag ->
if (tag in processedTags) return@forEach
processedTags.add(tag)
try {
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
return@forEach
}
customMap[tag] = profile
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve custom outbound for tag '$tag', skipping", e)
}
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve custom outbound profiles", e)
}
return customMap
}
} }
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,10 @@
package com.v2ray.ang.core package com.v2ray.ang.core
import android.text.TextUtils
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
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.entities.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
@@ -73,41 +72,6 @@ object CoreOutboundBuilder {
outbound.mux?.concurrency = -1 outbound.mux?.concurrency = -1
} }
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
var localTunAddr = if (outbound.settings?.address == null) {
listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
} else {
outbound.settings?.address as List<*>
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) != true) {
localTunAddr = listOf(localTunAddr.first())
}
outbound.settings?.address = localTunAddr
}
if (outbound.streamSettings?.network == AppConfig.DEFAULT_NETWORK
&& outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.HEADER_TYPE_HTTP
) {
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
val requestString: String by lazy {
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":["no-cache"]}}"""
}
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString,
OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
)
outbound.streamSettings?.tcpSettings?.header?.request?.path =
if (path.isNullOrEmpty()) {
listOf("/")
} else {
path
}
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
}
} catch (e: Exception) { } catch (e: Exception) {
LogUtil.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
@@ -291,9 +255,23 @@ object CoreOutboundBuilder {
private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? { private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.WIREGUARD) val outboundBean = createInitOutbound(EConfigType.WIREGUARD)
val rawAddresses = profileItem.localAddress
?.split(",")
?.map { it.trim() }
?.filter { it.isNotEmpty() }
?.ifEmpty { null }
?: listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
val addresses = if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) == true) {
rawAddresses
} else {
val ipv4Addresses = rawAddresses.filter { !it.contains(":") }
ipv4Addresses.ifEmpty { listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4) }
}
outboundBean?.settings?.let { wireguard -> outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).split(",") wireguard.address = addresses
wireguard.peers?.firstOrNull()?.let { peer -> wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty() peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank() peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
@@ -328,6 +306,23 @@ object CoreOutboundBuilder {
return outboundBean return outboundBean
} }
private fun createTcpHttpRequest(
host: String?,
path: String?
): OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean {
val requestString =
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
val request = JsonUtil.fromJson(
requestString,
OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
) ?: OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
val parsedHost = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
request.headers.Host = parsedHost.ifEmpty { null }
request.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }.ifEmpty { listOf("/") }
return request
}
/** /**
* Configures transport settings for an outbound connection. * Configures transport settings for an outbound connection.
* *
@@ -358,13 +353,9 @@ object CoreOutboundBuilder {
val tcpSetting = OutboundBean.StreamSettingsBean.TcpSettingsBean() val tcpSetting = OutboundBean.StreamSettingsBean.TcpSettingsBean()
if (headerType == AppConfig.HEADER_TYPE_HTTP) { if (headerType == AppConfig.HEADER_TYPE_HTTP) {
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) { val requestObj = createTcpHttpRequest(host, path)
val requestObj = OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean() tcpSetting.header.request = requestObj
requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() } sni = requestObj.headers.Host?.getOrNull(0)
requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
tcpSetting.header.request = requestObj
sni = requestObj.headers.Host?.getOrNull(0)
}
} else { } else {
tcpSetting.header.type = "none" tcpSetting.header.type = "none"
sni = host sni = host
@@ -13,7 +13,8 @@ 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.contracts.ServiceControl import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.OutboundTrafficStat
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
@@ -175,7 +176,20 @@ object CoreServiceManager {
Intent(context.applicationContext, CoreProxyOnlyService::class.java) Intent(context.applicationContext, CoreProxyOnlyService::class.java)
} }
ContextCompat.startForegroundService(context, intent) try {
ContextCompat.startForegroundService(context, intent)
} catch (e: SecurityException) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Missing permission to start foreground service", e)
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
} catch (e: RuntimeException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
e.javaClass.name == "android.app.ForegroundServiceStartNotAllowedException"
) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Foreground service start not allowed", e)
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
}
throw e
}
} }
/** /**
@@ -257,7 +271,7 @@ object CoreServiceManager {
} }
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "") MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
NotificationManager.startSpeedNotification(currentConfig) NotificationManager.startSpeedNotification()
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
} }
@@ -299,13 +313,32 @@ object CoreServiceManager {
} }
/** /**
* Queries the statistics for a given tag and link. * Queries and resets all outbound traffic counters in one core call.
* @param tag The tag to query. * Go side format: tag,direction,value;tag,direction,value;
* @param link The link to query.
* @return The statistics value.
*/ */
fun queryStats(tag: String, link: String): Long { fun queryAllOutboundTrafficStats(): List<OutboundTrafficStat> {
return coreController.queryStats(tag, link) val payload = coreController.queryAllOutboundTrafficStats()
val result = ArrayList<OutboundTrafficStat>()
payload.split(';').forEach { entry ->
if (entry.isBlank()) return@forEach
val parts = entry.split(',', limit = 3)
if (parts.size != 3) return@forEach
val value = parts[2].toLongOrNull() ?: return@forEach
result.add(
OutboundTrafficStat(
tag = parts[0],
direction = parts[1],
value = value,
)
)
}
// LogUtil.d(AppConfig.TAG, "Queried outbound traffic stats: $result")
return result
} }
/** /**
@@ -488,12 +521,12 @@ object CoreServiceManager {
when (intent?.action) { when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> { Intent.ACTION_SCREEN_OFF -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off")
NotificationManager.stopSpeedNotification(currentConfig) NotificationManager.stopSpeedNotification()
} }
Intent.ACTION_SCREEN_ON -> { Intent.ACTION_SCREEN_ON -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on") LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on")
NotificationManager.startSpeedNotification(currentConfig) NotificationManager.startSpeedNotification()
} }
} }
} }
@@ -1,16 +1,19 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto
import android.content.Context import android.content.Context
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.CoreResolvedType import com.v2ray.ang.enums.CoreResolvedType
/**
* Runtime context produced by the builder and consumed by CoreConfigManager.
*/
data class CoreConfigContext( data class CoreConfigContext(
val context: Context, val context: Context,
val guid: String, val guid: String,
val selectedProfile: ProfileItem, val isCustom: Boolean = false,
val resolvedProfiles: List<ProfileItem>, val resolvedOutbounds: List<ResolvedOutbound> = emptyList(),
val resolvedType: CoreResolvedType, ) {
val customOutboundProfiles: Map<String, ProfileItem> = emptyMap(), data class ResolvedOutbound(
) val tag: String,
val profile: ProfileItem,
val resolvedProfiles: List<ProfileItem>,
val resolvedType: CoreResolvedType,
)
}
@@ -0,0 +1,7 @@
package com.v2ray.ang.dto
data class OutboundTrafficStat(
val tag: String,
val direction: String,
val value: Long,
)
@@ -0,0 +1,10 @@
package com.v2ray.ang.dto
data class UrlContentRequest(
val url: String?,
val timeout: Int = 15000,
val httpPort: Int = 0,
val proxyUsername: String? = null,
val proxyPassword: String? = null,
val userAgent: String? = null
)
@@ -68,7 +68,6 @@ data class V2rayConfig(
var protocol: String, var protocol: String,
var settings: OutSettingsBean? = null, var settings: OutSettingsBean? = null,
var streamSettings: StreamSettingsBean? = null, var streamSettings: StreamSettingsBean? = null,
val proxySettings: Any? = null,
val sendThrough: String? = null, val sendThrough: String? = null,
var mux: MuxBean? = MuxBean(false) var mux: MuxBean? = MuxBean(false)
) { ) {
@@ -82,7 +81,6 @@ data class V2rayConfig(
var address: Any? = null, var address: Any? = null,
var port: Int? = null, var port: Int? = null,
/*Freedom*/ /*Freedom*/
var domainStrategy: String? = null,
val redirect: String? = null, val redirect: String? = null,
val userLevel: Int? = null, val userLevel: Int? = null,
/*Loopback*/ /*Loopback*/
@@ -181,7 +179,7 @@ data class V2rayConfig(
@SerializedName("Accept-Encoding") @SerializedName("Accept-Encoding")
val acceptEncoding: List<String>? = null, val acceptEncoding: List<String>? = null,
val Connection: List<String>? = null, val Connection: List<String>? = null,
val Pragma: String? = null val Pragma: Any? = null
) )
} }
} }
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
data class AssetUrlCache( data class AssetUrlCache(
val guid: String, val guid: String,
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
data class AssetUrlItem( data class AssetUrlItem(
var remarks: String = "", var remarks: String = "",
@@ -1,10 +1,6 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
import com.v2ray.ang.AppConfig.LOOPBACK import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.PORT_SOCKS
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -80,13 +76,9 @@ data class ProfileItem(
} }
} }
fun getAllOutboundTags(): MutableList<String> {
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
}
fun getServerAddressAndPort(): String { fun getServerAddressAndPort(): String {
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) { if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
return "$LOOPBACK:$PORT_SOCKS" return "${AppConfig.LOOPBACK}:${AppConfig.PORT_SOCKS}"
} }
return Utils.getIpv6Address(server) + ":" + serverPort return Utils.getIpv6Address(server) + ":" + serverPort
} }
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
data class RulesetItem( data class RulesetItem(
var remarks: String? = "", var remarks: String? = "",
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) { data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
fun getTestDelayString(): String { fun getTestDelayString(): String {
@@ -7,4 +7,4 @@ data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
} }
return testDelayMillis.toString() + "ms" return testDelayMillis.toString() + "ms"
} }
} }
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
data class ServersCache( data class ServersCache(
val guid: String, val guid: String,
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
data class SubscriptionCache( data class SubscriptionCache(
val guid: String, val guid: String,
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
data class SubscriptionItem( data class SubscriptionItem(
var remarks: String = "", var remarks: String = "",
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto package com.v2ray.ang.dto.entities
data class WebDavConfig( data class WebDavConfig(
val baseUrl: String, val baseUrl: String,
@@ -6,4 +6,4 @@ data class WebDavConfig(
val password: String? = null, val password: String? = null,
val remoteBasePath: String = "/", val remoteBasePath: String = "/",
val timeoutSeconds: Long = 30 val timeoutSeconds: Long = 30
) )
@@ -0,0 +1,26 @@
package com.v2ray.ang.enums
/**
* Load-balancing strategy types supported by xray balancer.
*
* @param policyGroupType The string value written into the xray config JSON.
* @param policyGroupTypeValue The numeric string stored in [com.v2ray.ang.dto.entities.ProfileItem.policyGroupType].
* @param requiresBurstObservatory Whether this strategy needs a burstObservatory (leastLoad).
* @param requiresObservatory Whether this strategy needs an observatory (leastPing).
*/
enum class BalancerStrategyType(
val policyGroupType: String,
val policyGroupTypeValue: String,
val requiresBurstObservatory: Boolean = false,
val requiresObservatory: Boolean = false,
) {
LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true),
RANDOM("random", "2"),
ROUND_ROBIN("roundRobin", "3"),
LEAST_PING("leastPing", "", requiresObservatory = true); // default / else
companion object {
fun from(policyGroupType: String?): BalancerStrategyType =
entries.firstOrNull { it.policyGroupTypeValue == policyGroupType } ?: LEAST_PING
}
}
@@ -5,5 +5,4 @@ enum class CoreResolvedType {
NORMAL, NORMAL,
POLICYGROUP, POLICYGROUP,
PROXYCHAIN, PROXYCHAIN,
CUSTOM,
} }
@@ -13,17 +13,6 @@ enum class PermissionType {
override fun getPermission(): String = Manifest.permission.CAMERA override fun getPermission(): String = Manifest.permission.CAMERA
}, },
/** Read storage / media permission (adapts to Android version) */
READ_STORAGE {
override fun getPermission(): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
}
},
/** Notification permission (Android 13+) */ /** Notification permission (Android 13+) */
POST_NOTIFICATIONS { POST_NOTIFICATIONS {
@RequiresApi(Build.VERSION_CODES.TIRAMISU) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -37,7 +26,6 @@ enum class PermissionType {
fun getLabel(): String { fun getLabel(): String {
return when (this) { return when (this) {
CAMERA -> "Camera" CAMERA -> "Camera"
READ_STORAGE -> "Storage"
POST_NOTIFICATIONS -> "Notification" POST_NOTIFICATIONS -> "Notification"
} }
} }
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.NetworkType import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.nullIfBlank import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.util.HttpUtil import com.v2ray.ang.util.HttpUtil
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.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.idnHost import com.v2ray.ang.extension.idnHost
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.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.idnHost import com.v2ray.ang.extension.idnHost
@@ -1,6 +1,6 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
@@ -26,7 +26,11 @@ object SocksFmt : FmtBase() {
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
if (uri.userInfo?.isEmpty() == false) { if (uri.userInfo?.isEmpty() == false) {
val result = Utils.decode(uri.userInfo).split(":", limit = 2) val result = if (uri.userInfo.contains(":")) {
uri.userInfo.split(":", limit = 2)
} else {
Utils.decode(uri.userInfo).split(":", limit = 2)
}
if (result.count() == 2) { if (result.count() == 2) {
config.username = result.first() config.username = result.first()
config.password = result.last() config.password = result.last()
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.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.idnHost import com.v2ray.ang.extension.idnHost
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
@@ -2,8 +2,8 @@ package com.v2ray.ang.fmt
import android.text.TextUtils import android.text.TextUtils
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.VmessQRCode import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.dto.entities.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.idnHost import com.v2ray.ang.extension.idnHost
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.nullIfBlank import com.v2ray.ang.extension.nullIfBlank
@@ -4,13 +4,13 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.text.TextUtils import android.text.TextUtils
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.core.CoreConfigManager import com.v2ray.ang.core.CoreConfigManager
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.SubscriptionUpdateResult import com.v2ray.ang.dto.SubscriptionUpdateResult
import com.v2ray.ang.dto.UrlContentRequest
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.CustomFmt
@@ -30,6 +30,21 @@ import java.net.URI
object AngConfigManager { object AngConfigManager {
// Parser mapping for different config types (lazy initialized)
private val configFmtParsers: Map<String, (String) -> ProfileItem?> by lazy {
mapOf(
EConfigType.VMESS.protocolScheme to VmessFmt::parse,
EConfigType.SHADOWSOCKS.protocolScheme to ShadowsocksFmt::parse,
EConfigType.SOCKS.protocolScheme to SocksFmt::parse,
AppConfig.SOCKS4 to SocksFmt::parse,
AppConfig.SOCKS5 to SocksFmt::parse,
EConfigType.TROJAN.protocolScheme to TrojanFmt::parse,
EConfigType.VLESS.protocolScheme to VlessFmt::parse,
EConfigType.WIREGUARD.protocolScheme to WireguardFmt::parse,
EConfigType.HYSTERIA2.protocolScheme to Hysteria2Fmt::parse,
AppConfig.HY2 to Hysteria2Fmt::parse
)
}
/** /**
* Shares the configuration to the clipboard. * Shares the configuration to the clipboard.
@@ -310,6 +325,16 @@ object AngConfigManager {
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? { private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
if (keyToProfile.isEmpty() || target == null) return null if (keyToProfile.isEmpty() || target == null) return null
// Level 0: Full match (remarks + server + port + password)
if (target.remarks.isNotBlank()) {
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.remarks, target.remarks) &&
isSameText(saved.server, target.server) &&
isSameText(saved.serverPort, target.serverPort) &&
isSameText(saved.password, target.password)
}?.key?.let { return it }
}
// Level 1: Match by remarks // Level 1: Match by remarks
if (target.remarks.isNotBlank()) { if (target.remarks.isNotBlank()) {
keyToProfile.entries.firstOrNull { (_, saved) -> keyToProfile.entries.firstOrNull { (_, saved) ->
@@ -442,22 +467,8 @@ object AngConfigManager {
return null return null
} }
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) { val config = configFmtParsers.firstNotNullOfOrNull { (scheme, parser) ->
VmessFmt.parse(str) if (str.startsWith(scheme)) parser(str) else null
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
ShadowsocksFmt.parse(str)
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
SocksFmt.parse(str)
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
TrojanFmt.parse(str)
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
VlessFmt.parse(str)
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
WireguardFmt.parse(str)
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
Hysteria2Fmt.parse(str)
} else {
null
} }
if (config == null) { if (config == null) {
@@ -535,14 +546,28 @@ object AngConfigManager {
var configText = try { var configText = try {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort, proxyUsername, proxyPassword) HttpUtil.getUrlContentWithUserAgent(
UrlContentRequest(
url = url,
userAgent = userAgent,
timeout = 15000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
)
)
} catch (e: Exception) { } catch (e: Exception) {
LogUtil.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(
UrlContentRequest(
url = url,
userAgent = userAgent
)
)
} catch (e: Exception) { } catch (e: Exception) {
LogUtil.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)
"" ""
@@ -4,14 +4,14 @@ import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.DEFAULT_SUBSCRIPTION_ID import com.v2ray.ang.AppConfig.DEFAULT_SUBSCRIPTION_ID
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
import com.v2ray.ang.dto.AssetUrlCache import com.v2ray.ang.dto.entities.AssetUrlCache
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.entities.AssetUrlItem
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.dto.ServerAffiliationInfo import com.v2ray.ang.dto.entities.ServerAffiliationInfo
import com.v2ray.ang.dto.SubscriptionCache import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.dto.WebDavConfig import com.v2ray.ang.dto.entities.WebDavConfig
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -102,7 +102,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.fromJsonSafe(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
} }
} }
@@ -144,7 +144,7 @@ object MmkvManager {
if (json.isNullOrBlank()) { if (json.isNullOrBlank()) {
return null return null
} }
return JsonUtil.fromJson(json, ProfileItem::class.java) return JsonUtil.fromJsonSafe(json, ProfileItem::class.java)
} }
@@ -247,7 +247,7 @@ object MmkvManager {
if (json.isNullOrBlank()) { if (json.isNullOrBlank()) {
return null return null
} }
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java) return JsonUtil.fromJsonSafe(json, ServerAffiliationInfo::class.java)
} }
/** /**
@@ -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.fromJsonSafe(json, SubscriptionItem::class.java) ?: SubscriptionItem()
subscriptions.add(SubscriptionCache(key, item)) subscriptions.add(SubscriptionCache(key, item))
} }
} }
@@ -420,7 +420,7 @@ object MmkvManager {
*/ */
fun decodeSubscription(subscriptionId: String): SubscriptionItem? { fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
val json = subStorage.decodeString(subscriptionId) ?: return null val json = subStorage.decodeString(subscriptionId) ?: return null
return JsonUtil.fromJson(json, SubscriptionItem::class.java) return JsonUtil.fromJsonSafe(json, SubscriptionItem::class.java)
} }
/** /**
@@ -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.fromJsonSafe(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.fromJsonSafe(json, AssetUrlItem::class.java) ?: AssetUrlItem()
assetUrlItems.add(AssetUrlCache(key, item)) assetUrlItems.add(AssetUrlCache(key, item))
} }
} }
@@ -495,7 +495,7 @@ object MmkvManager {
*/ */
fun decodeAsset(assetid: String): AssetUrlItem? { fun decodeAsset(assetid: String): AssetUrlItem? {
val json = assetStorage.decodeString(assetid) ?: return null val json = assetStorage.decodeString(assetid) ?: return null
return JsonUtil.fromJson(json, AssetUrlItem::class.java) return JsonUtil.fromJsonSafe(json, AssetUrlItem::class.java)
} }
//endregion //endregion
@@ -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.fromJsonSafe(ruleset, Array<RulesetItem>::class.java)?.toMutableList() ?: mutableListOf()
} }
/** /**
@@ -714,7 +714,7 @@ object MmkvManager {
*/ */
fun decodeWebDavConfig(): WebDavConfig? { fun decodeWebDavConfig(): WebDavConfig? {
val json = mainStorage.decodeString(KEY_WEBDAV_CONFIG) ?: return null val json = mainStorage.decodeString(KEY_WEBDAV_CONFIG) ?: return null
return JsonUtil.fromJson(json, WebDavConfig::class.java) return JsonUtil.fromJsonSafe(json, WebDavConfig::class.java)
} }
//endregion //endregion
@@ -14,7 +14,7 @@ import androidx.core.app.NotificationCompat
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.core.CoreServiceManager import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.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 com.v2ray.ang.util.LogUtil
@@ -43,53 +43,15 @@ object NotificationManager {
* Starts the speed notification. * Starts the speed notification.
* @param currentConfig The current profile configuration. * @param currentConfig The current profile configuration.
*/ */
fun startSpeedNotification(currentConfig: ProfileItem?) { fun startSpeedNotification() {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
if (speedNotificationJob != null || CoreServiceManager.isRunning() == false) return if (speedNotificationJob != null || CoreServiceManager.isRunning() == false) return
var lastZeroSpeed = false var lastZeroSpeed = false
val outboundTags = currentConfig?.getAllOutboundTags()
outboundTags?.remove(AppConfig.TAG_DIRECT)
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch { speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) { while (isActive) {
val queryTime = System.currentTimeMillis() lastZeroSpeed = updateSpeedNotificationOnce(lastZeroSpeed)
val sinceLastQueryIn = (queryTime - lastQueryTime)
// If the query interval is too short, skip this round to avoid excessive CPU usage
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
lastQueryTime = queryTime
delay(QUERY_INTERVAL_MS)
continue
}
val sinceLastQueryInSeconds = sinceLastQueryIn / 1000.0
var proxyTotal = 0L
val text = StringBuilder()
outboundTags?.forEach {
val up = CoreServiceManager.queryStats(it, AppConfig.UPLINK)
val down = CoreServiceManager.queryStats(it, AppConfig.DOWNLINK)
if (up + down > 0) {
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
proxyTotal += up + down
}
}
val directUplink = CoreServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.UPLINK)
val directDownlink = CoreServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.DOWNLINK)
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
if (!zeroSpeed || !lastZeroSpeed) {
if (proxyTotal == 0L) {
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
}
appendSpeedString(
text, AppConfig.TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
directDownlink / sinceLastQueryInSeconds
)
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
}
lastZeroSpeed = zeroSpeed
lastQueryTime = queryTime
delay(QUERY_INTERVAL_MS) delay(QUERY_INTERVAL_MS)
} }
} }
@@ -168,13 +130,12 @@ object NotificationManager {
/** /**
* Stops the speed notification. * Stops the speed notification.
* @param currentConfig The current profile configuration.
*/ */
fun stopSpeedNotification(currentConfig: ProfileItem?) { fun stopSpeedNotification() {
speedNotificationJob?.let { speedNotificationJob?.let {
it.cancel() it.cancel()
speedNotificationJob = null speedNotificationJob = null
updateNotification(currentConfig?.remarks, 0, 0) updateNotification("", 0, 0)
} }
} }
@@ -247,6 +208,69 @@ object NotificationManager {
text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n") text.append("• ${up.toLong().toSpeedString()}↑ ${down.toLong().toSpeedString()}↓\n")
} }
/**
* Updates the speed notification once.
* Queries traffic stats, separates proxy and direct, and updates the notification.
* @param lastZeroSpeed The previous zero speed state.
* @return The current zero speed state.
*/
private fun updateSpeedNotificationOnce(lastZeroSpeed: Boolean): Boolean {
val queryTime = System.currentTimeMillis()
val sinceLastQueryIn = (queryTime - lastQueryTime)
// If the query interval is too short, skip this round to avoid excessive CPU usage
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
lastQueryTime = queryTime
return lastZeroSpeed
}
val sinceLastQueryInSeconds = sinceLastQueryIn / 1000.0
var proxyUplink = 0L
var proxyDownlink = 0L
var directUplink = 0L
var directDownlink = 0L
CoreServiceManager.queryAllOutboundTrafficStats().forEach { stat ->
when {
stat.tag == AppConfig.TAG_DIRECT -> {
when (stat.direction) {
AppConfig.UPLINK -> directUplink += stat.value
AppConfig.DOWNLINK -> directDownlink += stat.value
}
}
stat.tag.startsWith(AppConfig.TAG_PROXY) -> {
when (stat.direction) {
AppConfig.UPLINK -> proxyUplink += stat.value
AppConfig.DOWNLINK -> proxyDownlink += stat.value
}
}
}
}
val proxyTotal = proxyUplink + proxyDownlink
val directTotal = directUplink + directDownlink
val zeroSpeed = proxyTotal + directTotal == 0L
if (!zeroSpeed || !lastZeroSpeed) {
val text = StringBuilder()
appendSpeedString(
text, AppConfig.TAG_PROXY,
proxyUplink / sinceLastQueryInSeconds,
proxyDownlink / sinceLastQueryInSeconds
)
appendSpeedString(
text, AppConfig.TAG_DIRECT,
directUplink / sinceLastQueryInSeconds,
directDownlink / sinceLastQueryInSeconds
)
updateNotification(text.toString(), proxyTotal, directTotal)
}
lastQueryTime = queryTime
return zeroSpeed
}
/** /**
* Gets the service instance. * Gets the service instance.
* @return The service instance. * @return The service instance.
@@ -12,10 +12,10 @@ import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.TAG_DIRECT import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.VPN import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.Language import com.v2ray.ang.enums.Language
import com.v2ray.ang.enums.RoutingType import com.v2ray.ang.enums.RoutingType
@@ -73,7 +73,7 @@ object SettingsManager {
return null return null
} }
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java)?.toMutableList() return JsonUtil.fromJsonSafe(assets, Array<RulesetItem>::class.java)?.toMutableList()
} }
/** /**
@@ -97,7 +97,7 @@ object SettingsManager {
} }
try { try {
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java)?.toMutableList() val rulesetList = JsonUtil.fromJsonSafe(content, Array<RulesetItem>::class.java)?.toMutableList()
if (rulesetList.isNullOrEmpty()) { if (rulesetList.isNullOrEmpty()) {
return false return false
} }
@@ -191,7 +191,7 @@ object SettingsManager {
val config = decodeServerConfig(guid) ?: return false val config = decodeServerConfig(guid) ?: return false
if (config.configType == EConfigType.CUSTOM) { if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid) ?: return false val raw = MmkvManager.decodeServerRaw(guid) ?: return false
val v2rayConfig = JsonUtil.fromJson(raw, V2rayConfig::class.java) val v2rayConfig = JsonUtil.fromJsonSafe(raw, V2rayConfig::class.java)
val exist = v2rayConfig?.routing?.rules?.filter { it.outboundTag == TAG_DIRECT }?.any { val exist = v2rayConfig?.routing?.rules?.filter { it.outboundTag == TAG_DIRECT }?.any {
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
} }
@@ -246,6 +246,20 @@ object SettingsManager {
.firstOrNull { it.remarks == remarks } .firstOrNull { it.remarks == remarks }
} }
/**
* Collects non-empty profile remarks while excluding specific config types.
*/
fun getProfileRemarks(excludeConfigTypes: Set<EConfigType> = setOf(EConfigType.CUSTOM)): List<String> {
return decodeAllServerList()
.asSequence()
.mapNotNull { guid -> decodeServerConfig(guid) }
.filter { profile -> profile.configType !in excludeConfigTypes }
.map { it.remarks.trim() }
.filter { it.isNotEmpty() }
.distinct()
.toList()
}
/** /**
* Removes the subscription. * Removes the subscription.
* If there are no remaining subscriptions, * If there are no remaining subscriptions,
@@ -571,7 +585,7 @@ object SettingsManager {
return return
} }
val guids = JsonUtil.fromJson(oldJson, Array<String>::class.java) ?: run { val guids = JsonUtil.fromJsonSafe(oldJson, Array<String>::class.java) ?: run {
MmkvManager.encodeSettings(migrationKey, true) MmkvManager.encodeSettings(migrationKey, true)
return return
} }
@@ -2,6 +2,7 @@ package com.v2ray.ang.handler
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.IPAPIInfo import com.v2ray.ang.dto.IPAPIInfo
import com.v2ray.ang.dto.UrlContentRequest
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.LogUtil
@@ -88,8 +89,16 @@ object SpeedtestManager {
val proxyPassword = SettingsManager.getSocksPassword() val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
if (httpPort == 0) return null if (httpPort == 0) return null
val content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: return null val content = HttpUtil.getUrlContent(
val ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null UrlContentRequest(
url = url,
timeout = 5000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
)
) ?: return null
val ipInfo = JsonUtil.fromJsonSafe(content, IPAPIInfo::class.java) ?: return null
val ip = listOf( val ip = listOf(
ipInfo.ip, ipInfo.ip,
@@ -13,7 +13,7 @@ import androidx.work.workDataOf
import com.v2ray.ang.AngApplication import com.v2ray.ang.AngApplication
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.SubscriptionCache import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.enums.NotificationChannelType import com.v2ray.ang.enums.NotificationChannelType
import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.NotificationHelper import com.v2ray.ang.util.NotificationHelper
@@ -5,6 +5,7 @@ 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
import com.v2ray.ang.dto.GitHubRelease import com.v2ray.ang.dto.GitHubRelease
import com.v2ray.ang.dto.UrlContentRequest
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
@@ -23,19 +24,32 @@ object UpdateCheckerManager {
val proxyUsername = SettingsManager.getSocksUsername() val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword() val proxyPassword = SettingsManager.getSocksPassword()
var response = HttpUtil.getUrlContent(url, 5000) var response = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000
)
)
if (response.isNullOrEmpty()) { if (response.isNullOrEmpty()) {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
response = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) response = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
)
)
?: throw IllegalStateException("Failed to get response") ?: throw IllegalStateException("Failed to get response")
} }
val latestRelease = if (includePreRelease) { val latestRelease = if (includePreRelease) {
JsonUtil.fromJson(response, Array<GitHubRelease>::class.java) JsonUtil.fromJsonSafe(response, Array<GitHubRelease>::class.java)
?.firstOrNull() ?.firstOrNull()
?: throw IllegalStateException("No pre-release found") ?: throw IllegalStateException("No pre-release found")
} else { } else {
JsonUtil.fromJson(response, GitHubRelease::class.java) JsonUtil.fromJsonSafe(response, GitHubRelease::class.java)
} }
if (latestRelease == null) { if (latestRelease == null) {
return@withContext CheckUpdateResult(hasUpdate = false) return@withContext CheckUpdateResult(hasUpdate = false)
@@ -1,7 +1,7 @@
package com.v2ray.ang.handler package com.v2ray.ang.handler
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.WebDavConfig import com.v2ray.ang.dto.entities.WebDavConfig
import com.v2ray.ang.util.LogUtil import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -61,6 +61,45 @@ class DialerNativeService : IDialerService {
private const val UNARY_BODY_WAIT_TIMEOUT_MS = 15_000L private const val UNARY_BODY_WAIT_TIMEOUT_MS = 15_000L
private val TOKEN_REGEX = Regex("""/websocket\?token=([^"'\s]+)""") private val TOKEN_REGEX = Regex("""/websocket\?token=([^"'\s]+)""")
private val METHODS_WITHOUT_BODY = setOf("GET", "HEAD") private val METHODS_WITHOUT_BODY = setOf("GET", "HEAD")
private val HEADERS_BLACKLIST = hashSetOf(
// AI suggest:
"host",
"content-length",
"transfer-encoding",
"content-encoding",
"connection",
"upgrade",
"sec-websocket-key",
"sec-websocket-version",
"sec-websocket-protocol",
"Timing-Allow-Origin",
// xray
"Set-Cookie",
"Cookie",
"Origin",
"Sec-CH-UA",
"Sec-CH-UA-Mobile",
"Sec-CH-UA-Platform",
"DNT",
"User-Agent",
"Accept-Language",
"Cache-Control",
"Upgrade-Insecure-Requests",
"Sec-Fetch-Site",
"Sec-Fetch-Mode",
"Sec-Fetch-User",
"Sec-Fetch-Dest",
"Referer",
"Accept",
"Priority",
"Pragma",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Expose-Headers",
"Access-Control-Max-Age",
)
} }
@Volatile @Volatile
@@ -630,6 +669,11 @@ class DialerNativeService : IDialerService {
requestBuilder.header("Cache-Control", "no-cache, no-store, must-revalidate") requestBuilder.header("Cache-Control", "no-cache, no-store, must-revalidate")
task.extra.referrer?.takeIf { it.isNotBlank() } task.extra.referrer?.takeIf { it.isNotBlank() }
?.let { requestBuilder.header("Referer", it) } ?.let { requestBuilder.header("Referer", it) }
val taskHeaders = task.extra.headers.filterKeys { key ->
val lowerKey = key.lowercase()
!HEADERS_BLACKLIST.any { blackKey -> blackKey.equals(lowerKey, ignoreCase = true) }
}
taskHeaders.forEach { (key, value) -> requestBuilder.header(key, value) }
val method = task.method.uppercase() val method = task.method.uppercase()
val methodAllowsBody = method !in METHODS_WITHOUT_BODY val methodAllowsBody = method !in METHODS_WITHOUT_BODY
@@ -663,7 +707,7 @@ class DialerNativeService : IDialerService {
val extra: Extra val extra: Extra
) { ) {
data class Extra( data class Extra(
// val headers: Map<String, String> = emptyMap(), val headers: Map<String, String> = emptyMap(),
// val cookies: Map<String, String> = emptyMap(), // val cookies: Map<String, String> = emptyMap(),
val protocols: List<String> = emptyList(), val protocols: List<String> = emptyList(),
val referrer: String? = null val referrer: String? = null
@@ -679,7 +723,7 @@ class DialerNativeService : IDialerService {
val streamResponse = root.optBoolean("streamResponse", false) val streamResponse = root.optBoolean("streamResponse", false)
val extraObject = root.optJSONObject("extra") val extraObject = root.optJSONObject("extra")
// val headers = extraObject.optStringMap("headers") val headers = extraObject.optStringMap("headers")
// val cookies = extraObject.optStringMap("cookies") // val cookies = extraObject.optStringMap("cookies")
val referrer = extraObject?.optString("referrer")?.takeIf { it.isNotBlank() } val referrer = extraObject?.optString("referrer")?.takeIf { it.isNotBlank() }
val protocols = extraObject.optProtocols() val protocols = extraObject.optProtocols()
@@ -689,7 +733,7 @@ class DialerNativeService : IDialerService {
url = url, url = url,
streamResponse = streamResponse, streamResponse = streamResponse,
extra = Extra( extra = Extra(
// headers = headers, headers = headers,
// cookies = cookies, // cookies = cookies,
protocols = protocols, protocols = protocols,
referrer = referrer referrer = referrer
@@ -698,16 +742,16 @@ class DialerNativeService : IDialerService {
}.getOrNull() }.getOrNull()
} }
// private fun JSONObject?.optStringMap(name: String): Map<String, String> { private fun JSONObject?.optStringMap(name: String): Map<String, String> {
// val child = this?.optJSONObject(name) ?: return emptyMap() val child = this?.optJSONObject(name) ?: return emptyMap()
// val map = LinkedHashMap<String, String>() val map = LinkedHashMap<String, String>()
// val iter = child.keys() val iter = child.keys()
// while (iter.hasNext()) { while (iter.hasNext()) {
// val key = iter.next() val key = iter.next()
// map[key] = child.optString(key) map[key] = child.optString(key)
// } }
// return map return map
// } }
private fun JSONObject?.optProtocols(): List<String> { private fun JSONObject?.optProtocols(): List<String> {
val raw = this?.opt("protocol") ?: return emptyList() val raw = this?.opt("protocol") ?: return emptyList()
@@ -12,7 +12,7 @@ import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBackupBinding import com.v2ray.ang.databinding.ActivityBackupBinding
import com.v2ray.ang.databinding.DialogWebdavBinding import com.v2ray.ang.databinding.DialogWebdavBinding
import com.v2ray.ang.dto.WebDavConfig import com.v2ray.ang.dto.entities.WebDavConfig
import com.v2ray.ang.extension.toastError 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
@@ -17,7 +17,7 @@ import com.v2ray.ang.R
import com.v2ray.ang.contracts.MainAdapterListener import com.v2ray.ang.contracts.MainAdapterListener
import com.v2ray.ang.databinding.FragmentGroupServerBinding import com.v2ray.ang.databinding.FragmentGroupServerBinding
import com.v2ray.ang.databinding.ItemQrcodeBinding import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastError
@@ -56,9 +56,7 @@ abstract class HelperBaseActivity : BaseActivity() {
mimeType: String = "*/*", mimeType: String = "*/*",
onResult: (Uri?) -> Unit onResult: (Uri?) -> Unit
) { ) {
checkAndRequestPermission(PermissionType.READ_STORAGE) { fileChooser.launch(mimeType, onResult)
fileChooser.launch(mimeType, onResult)
}
} }
/** /**
@@ -12,8 +12,8 @@ import com.v2ray.ang.R
import com.v2ray.ang.contracts.MainAdapterListener import com.v2ray.ang.contracts.MainAdapterListener
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
import com.v2ray.ang.databinding.ItemRecyclerMainBinding import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.ServersCache import com.v2ray.ang.dto.entities.ServersCache
import com.v2ray.ang.extension.nullIfBlank import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
@@ -14,6 +14,7 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBypassListBinding import com.v2ray.ang.databinding.ActivityBypassListBinding
import com.v2ray.ang.dto.AppInfo import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.dto.UrlContentRequest
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.extension.v2RayApplication import com.v2ray.ang.extension.v2RayApplication
@@ -189,12 +190,25 @@ class PerAppProxyActivity : BaseActivity() {
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000) var content = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000
)
)
if (content.isNullOrEmpty()) { if (content.isNullOrEmpty()) {
val proxyUsername = SettingsManager.getSocksUsername() val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword() val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: "" content = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
)
) ?: ""
} }
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
//LogUtil.i(AppConfig.TAG, content) //LogUtil.i(AppConfig.TAG, content)
@@ -11,11 +11,10 @@ import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
import com.v2ray.ang.AppConfig.TAG_PROXY 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.entities.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
@@ -78,9 +77,7 @@ class RoutingEditActivity : BaseActivity() {
* The dropdown button triggers showing the full list without typing. * The dropdown button triggers showing the full list without typing.
*/ */
private fun setupOutboundTagInput() { private fun setupOutboundTagInput() {
val profileRemarks = MmkvManager.decodeAllServerList() val profileRemarks = SettingsManager.getProfileRemarks()
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
.filter { it.isNotBlank() }
val suggestions = (BUILTIN_OUTBOUND_TAGS.toList() + profileRemarks).distinct() val suggestions = (BUILTIN_OUTBOUND_TAGS.toList() + profileRemarks).distinct()
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions) val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
@@ -20,7 +20,7 @@ import com.v2ray.ang.AppConfig.TLS
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4 import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.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
@@ -10,7 +10,7 @@ import com.blacksquircle.ui.language.json.JsonLanguage
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
@@ -8,7 +8,7 @@ import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerGroupBinding import com.v2ray.ang.databinding.ActivityServerGroupBinding
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
@@ -11,7 +11,7 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.contracts.BaseAdapterListener import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ActivityServerProxyChainBinding import com.v2ray.ang.databinding.ActivityServerProxyChainBinding
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
@@ -55,18 +55,13 @@ class ServerProxyChainActivity : BaseActivity() {
} }
private fun loadAvailableRemarks() { private fun loadAvailableRemarks() {
allRemarks = MmkvManager.decodeAllServerList() allRemarks = SettingsManager.getProfileRemarks(
.asSequence() excludeConfigTypes = setOf(
.mapNotNull { guid -> MmkvManager.decodeServerConfig(guid) } EConfigType.CUSTOM,
.filter { profile -> EConfigType.POLICYGROUP,
profile.configType != EConfigType.CUSTOM EConfigType.PROXYCHAIN,
&& profile.configType != EConfigType.POLICYGROUP )
&& profile.configType != EConfigType.PROXYCHAIN )
}
.map { it.remarks.trim() }
.filter { it.isNotEmpty() }
.distinct()
.toList()
} }
private fun setupRecycler() { private fun setupRecycler() {
@@ -4,12 +4,16 @@ import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.ImageButton
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
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.enums.EConfigType
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
@@ -33,6 +37,7 @@ class SubEditActivity : BaseActivity() {
//setContentView(binding.root) //setContentView(binding.root)
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting)) setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting))
setupProfileRemarkInputs()
SettingsChangeManager.makeSetupGroupTab() SettingsChangeManager.makeSetupGroupTab()
val subItem = MmkvManager.decodeSubscription(editSubId) val subItem = MmkvManager.decodeSubscription(editSubId)
if (subItem != null) { if (subItem != null) {
@@ -73,6 +78,37 @@ class SubEditActivity : BaseActivity() {
return true return true
} }
private fun setupProfileRemarkInputs() {
val suggestions = SettingsManager.getProfileRemarks(
excludeConfigTypes = setOf(
EConfigType.CUSTOM,
EConfigType.POLICYGROUP,
EConfigType.PROXYCHAIN,
)
)
setupProfileRemarkInput(binding.etPreProfile, binding.btnPreProfileDropdown, suggestions)
setupProfileRemarkInput(binding.etNextProfile, binding.btnNextProfileDropdown, suggestions)
}
private fun setupProfileRemarkInput(
input: AutoCompleteTextView,
dropdownButton: ImageButton,
suggestions: List<String>
) {
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
input.setAdapter(adapter)
input.threshold = 0
dropdownButton.setOnClickListener {
input.requestFocus()
input.showDropDown()
}
input.setOnClickListener {
input.showDropDown()
}
}
/** /**
* save server config * save server config
*/ */
@@ -15,7 +15,7 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.contracts.BaseAdapterListener import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ActivityUserAssetBinding import com.v2ray.ang.databinding.ActivityUserAssetBinding
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.entities.AssetUrlItem
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.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
@@ -8,7 +8,7 @@ import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig
import com.v2ray.ang.R import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.entities.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
@@ -3,6 +3,7 @@ package com.v2ray.ang.util
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
import com.v2ray.ang.dto.UrlContentRequest
import okhttp3.Credentials import okhttp3.Credentials
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -108,20 +109,15 @@ object HttpUtil {
* @param httpPort The HTTP port to use. * @param httpPort The HTTP port to use.
* @return The content of the URL as a string. * @return The content of the URL as a string.
*/ */
fun getUrlContent( fun getUrlContent(request: UrlContentRequest): String? {
url: String, val url = request.url ?: return null
timeout: Int, val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = true)
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String? {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
.url(url) .url(url)
.get() .get()
.header("Connection", "close") .header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) { if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword)) requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
} }
try { try {
client.newCall(requestBuilder.build()).execute().use { response -> client.newCall(requestBuilder.build()).execute().use { response ->
@@ -147,25 +143,18 @@ 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( fun getUrlContentWithUserAgent(request: UrlContentRequest): String {
url: String?, var currentUrl = request.url
userAgent: String?,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String {
var currentUrl = url
var redirects = 0 var redirects = 0
val maxRedirects = 3 val maxRedirects = 3
while (redirects++ < maxRedirects) { while (redirects++ < maxRedirects) {
if (currentUrl == null) continue if (currentUrl == null) continue
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = false) val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = false)
val finalUserAgent = if (userAgent.isNullOrBlank()) { val finalUserAgent = if (request.userAgent.isNullOrBlank()) {
"v2rayNG/${BuildConfig.VERSION_NAME}" "v2rayNG/${BuildConfig.VERSION_NAME}"
} else { } else {
userAgent request.userAgent
} }
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
.url(currentUrl) .url(currentUrl)
@@ -175,8 +164,8 @@ object HttpUtil {
applyEmbeddedBasicAuthHeader(currentUrl, requestBuilder) applyEmbeddedBasicAuthHeader(currentUrl, requestBuilder)
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) { if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword)) requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
} }
client.newCall(requestBuilder.build()).execute().use { response -> client.newCall(requestBuilder.build()).execute().use { response ->
@@ -271,20 +260,17 @@ object HttpUtil {
} }
fun downloadToFile( fun downloadToFile(
url: String, request: UrlContentRequest,
targetFile: File, targetFile: File
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): Boolean { ): Boolean {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true) val url = request.url ?: return false
val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
.url(url) .url(url)
.get() .get()
.header("Connection", "close") .header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) { if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword)) requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
} }
return try { return try {
@@ -35,6 +35,23 @@ object JsonUtil {
return gson.fromJson(src, cls) return gson.fromJson(src, cls)
} }
/**
* Safely parses a JSON string into an object of the specified class.
* Returns null if parsing fails instead of throwing an exception.
*
* @param src The JSON string to parse.
* @param cls The class of the object to parse into.
* @return The parsed object, or null if parsing fails.
*/
fun <T> fromJsonSafe(src: String, cls: Class<T>): T? {
return try {
gson.fromJson(src, cls)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to parse JSON", e)
null
}
}
/** /**
* Converts an object to its pretty-printed JSON representation. * Converts an object to its pretty-printed JSON representation.
* *
@@ -14,10 +14,10 @@ import com.v2ray.ang.AngApplication
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.GroupMapItem import com.v2ray.ang.dto.GroupMapItem
import com.v2ray.ang.dto.ServersCache
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.dto.entities.ServersCache
import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.extension.matchesPattern import com.v2ray.ang.extension.matchesPattern
import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
@@ -1,7 +1,7 @@
package com.v2ray.ang.viewmodel package com.v2ray.ang.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.SettingsManager
@@ -1,8 +1,8 @@
package com.v2ray.ang.viewmodel package com.v2ray.ang.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.v2ray.ang.dto.SubscriptionCache import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.dto.entities.SubscriptionItem
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
@@ -2,8 +2,9 @@ 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.dto.AssetUrlCache import com.v2ray.ang.dto.UrlContentRequest
import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.dto.entities.AssetUrlCache
import com.v2ray.ang.dto.entities.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
@@ -92,7 +93,18 @@ class UserAssetViewModel : ViewModel() {
val targetTemp = File(extDir, item.remarks + "_temp") val targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks) val target = File(extDir, item.remarks)
try { try {
if (HttpUtil.downloadToFile(item.url, targetTemp, 15000, httpPort, proxyUsername, proxyPassword)) { if (
HttpUtil.downloadToFile(
UrlContentRequest(
url = item.url,
timeout = 15000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
),
targetTemp
)
) {
targetTemp.renameTo(target) targetTemp.renameTo(target)
return true return true
} }
@@ -209,12 +209,33 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/sub_setting_pre_profile" /> android:text="@string/sub_setting_pre_profile" />
<EditText <RelativeLayout
android:id="@+id/et_pre_profile"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/sub_setting_pre_profile_tip" android:layout_marginTop="@dimen/padding_spacing_dp8"
android:inputType="text" /> android:layout_marginBottom="@dimen/padding_spacing_dp8">
<AutoCompleteTextView
android:id="@+id/et_pre_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:completionThreshold="0"
android:hint="@string/sub_setting_pre_profile_tip"
android:inputType="text"
android:imeOptions="actionDone"
android:paddingEnd="40dp" />
<ImageButton
android:id="@+id/btn_pre_profile_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/sub_setting_pre_profile"
android:src="@drawable/ic_arrow_drop_down" />
</RelativeLayout>
</LinearLayout> </LinearLayout>
@@ -229,12 +250,33 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/sub_setting_next_profile" /> android:text="@string/sub_setting_next_profile" />
<EditText <RelativeLayout
android:id="@+id/et_next_profile"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/sub_setting_pre_profile_tip" android:layout_marginTop="@dimen/padding_spacing_dp8"
android:inputType="text" /> android:layout_marginBottom="@dimen/padding_spacing_dp8">
<AutoCompleteTextView
android:id="@+id/et_next_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:completionThreshold="0"
android:hint="@string/sub_setting_pre_profile_tip"
android:inputType="text"
android:imeOptions="actionDone"
android:paddingEnd="40dp" />
<ImageButton
android:id="@+id/btn_next_profile_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/sub_setting_next_profile"
android:src="@drawable/ic_arrow_drop_down" />
</RelativeLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -14,7 +14,7 @@
<string name="notification_action_stop_v2ray">واڌاشتن</string> <string name="notification_action_stop_v2ray">واڌاشتن</string>
<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>
@@ -30,7 +30,7 @@
<string name="menu_item_import_config_clipboard">و من ٱووردن کانفیگ ز کلیپ بورد</string> <string name="menu_item_import_config_clipboard">و من ٱووردن کانفیگ ز کلیپ بورد</string>
<string name="menu_item_import_config_local">و من ٱووردن کانفیگ ز مهلی</string> <string name="menu_item_import_config_local">و من ٱووردن کانفیگ ز مهلی</string>
<string name="menu_item_import_config_policy_group">ٱووردن [بونکۊ سیاست]</string> <string name="menu_item_import_config_policy_group">ٱووردن [بونکۊ سیاست]</string>
<string name="menu_item_import_config_proxy_chain">Add [Proxy chain]</string> <string name="menu_item_import_config_proxy_chain">ٱووردن [زنجیره پروکسی]</string>
<string name="menu_item_import_config_manually_vmess">هؽل دستی[VMess]</string> <string name="menu_item_import_config_manually_vmess">هؽل دستی[VMess]</string>
<string name="menu_item_import_config_manually_vless">هؽل دستی[VLESS]</string> <string name="menu_item_import_config_manually_vless">هؽل دستی[VLESS]</string>
<string name="menu_item_import_config_manually_ss">هؽل دستی[Shadowsocks]</string> <string name="menu_item_import_config_manually_ss">هؽل دستی[Shadowsocks]</string>
@@ -86,8 +86,8 @@
<string name="server_lab_spider_x">SpiderX</string> <string name="server_lab_spider_x">SpiderX</string>
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string> <string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
<string name="server_lab_secret_key">کیلیت سیخومی</string> <string name="server_lab_secret_key">کیلیت سیخومی</string>
<string name="server_lab_reserved">Reserved(اختیاری، وا کاما ز یک جوڌا ابۊن)</string> <string name="server_lab_reserved">Reserved(اختیاری، وا کاما (,) ز یک جوڌا ابۊن)</string>
<string name="server_lab_local_address">نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن)</string> <string name="server_lab_local_address">نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما (,) ز یک جوڌا ابۊن)</string>
<string name="server_lab_local_mtu">Mtu(اختیاری، پؽش فرز 1420)</string> <string name="server_lab_local_mtu">Mtu(اختیاری، پؽش فرز 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پؽش فرز هسته 1350)</string> <string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پؽش فرز هسته 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پؽش فرز هسته 50)</string> <string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پؽش فرز هسته 50)</string>
@@ -177,16 +177,16 @@
<string name="title_pref_speed_enabled">ره وندن نشووݩ داڌن سورعت</string> <string name="title_pref_speed_enabled">ره وندن نشووݩ داڌن سورعت</string>
<string name="summary_pref_speed_enabled">نشووݩ داڌن سورعت هیم سکویی من وارسۊویا. نماڌ وارسۊوی و ری و کار گرؽڌن کانفیگ آلشت ابۊ.</string> <string name="summary_pref_speed_enabled">نشووݩ داڌن سورعت هیم سکویی من وارسۊویا. نماڌ وارسۊوی و ری و کار گرؽڌن کانفیگ آلشت ابۊ.</string>
<string name="title_pref_sniffing_enabled">ره وندن Sniffing</string> <string name="title_pref_sniffing_enabled">ره وندن تجزیه وو تئلیل کتنا (Sniffing)</string>
<string name="summary_pref_sniffing_enabled">دامنه sniff ن ز کتن امتهووݩ کۊنین (پؽش فرز رۊشن)</string> <string name="summary_pref_sniffing_enabled">و کار گرؽڌن تشخیس نوم دامنه (Sniff) من کتنا (پؽش فرز رۊشن)</string>
<string name="title_pref_route_only_enabled">ره وندن routeOnly</string> <string name="title_pref_route_only_enabled">ره وندن routeOnly</string>
<string name="summary_pref_route_only_enabled">ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو نشۊوی موورد نزرن و عونوان نشۊوی IP ووردارین.</string> <string name="summary_pref_route_only_enabled">نوم دامنه sniffed ن تینا سی تور جوستن و کار بگیرین وو نشۊوی موورد نزرن و عونوان نشۊوی IP ووردارین.</string>
<string name="title_pref_local_dns_enabled">ره وندن DNS مهلی</string> <string name="title_pref_local_dns_enabled">ره وندن DNS مهلی</string>
<string name="summary_pref_local_dns_enabled">درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین فعال بۊ)</string> <string name="summary_pref_local_dns_enabled">درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین ره ونده بۊ)</string>
<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="title_pref_ipv6_enabled">ره وندن IPv6</string>
<string name="summary_pref_ipv6_enabled">نشۊویا IPv6 وو تورا IPv6 ن ز رابت VPN ره ونین</string> <string name="summary_pref_ipv6_enabled">نشۊویا IPv6 وو تورا IPv6 ن ز رابت VPN ره ونین</string>
@@ -211,21 +211,21 @@
<string name="title_pref_delay_test_url">نشۊوی اینترنتی آزمایش تئخیر واقعی</string> <string name="title_pref_delay_test_url">نشۊوی اینترنتی آزمایش تئخیر واقعی</string>
<string name="summary_pref_delay_test_url">نشۊوی اینترنتی</string> <string name="summary_pref_delay_test_url">نشۊوی اینترنتی</string>
<string name="title_pref_real_ping_concurrency">هموورگی آزمایش تئخیر واقعی</string> <string name="title_pref_real_ping_concurrency">تعداد هوم زمووی تست تئخیر واقعی</string>
<string name="title_pref_ip_api_url">نشۊوی اینترنتی آزمایش دووسمندیا منپیز هیم سکویی</string> <string name="title_pref_ip_api_url">نشۊوی اینترنتی آزمایش دووسمندیا منپیز هیم سکویی</string>
<string name="summary_pref_ip_api_url">نشۊوی اینترنتی</string> <string name="summary_pref_ip_api_url">نشۊوی اینترنتی</string>
<string name="title_pref_proxy_sharing_enabled">هشتن منپیزا ز شبکه مهلی</string> <string name="title_pref_proxy_sharing_enabled">هشتن منپیزا ز شبکه مهلی</string>
<string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا نشۊوی IP ایسا، ز ره socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ.</string> <string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا نشۊوی IP ایسا، ز ره socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل ائتماد ره ونده بۊ تا ز منپیز قیر موجاز جلو گری بۊ.</string>
<string name="toast_warning_pref_proxysharing_short">منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین.</string> <string name="toast_warning_pref_proxysharing_short">منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین.</string>
<string name="title_pref_allow_insecure">اجازه نا ٱمن</string> <string name="title_pref_allow_insecure">اجازه نا ٱمن</string>
<string name="summary_pref_allow_insecure">مجال و کار بوردن TLS ب تۉر پؽش فرز، موجوز نا ٱمن فعال هڌ.</string> <string name="summary_pref_allow_insecure">مجال و کار بوردن TLS ب تۉر پؽش فرز، موجوز نا ٱمن ره ونده هڌ.</string>
<string name="title_pref_socks_port">پورت پروکسی مهلی</string> <string name="title_pref_socks_port">پورت پروکسی مهلی</string>
<string name="title_pref_enable_local_proxy">ره وندن پروکسی مهلی</string> <string name="title_pref_enable_local_proxy">ره وندن پروکسی مهلی</string>
<string name="summary_pref_enable_local_proxy">هرسا قیرفعال بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو ورۊ رسۊوی یل اشتراک وو کارا جۊر یو نترن ز پروکسی استفاڌه کونن.</string> <string name="summary_pref_enable_local_proxy">هرسا ره نوسته بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو ورۊ رسۊوی یل اشتراک وو کارا جۊر یو نترن پروکسی ن و کار گرن.</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string> <string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">ائراز هۊویت socks5 مجال استفاڌه ز UDP کاملن ایمن نؽ</string> <string name="summary_pref_socks_enable_udp">ائراز هۊویت socks5 مجال استفاڌه ز UDP کاملن ایمن نؽ</string>
<string name="summary_pref_socks_port">پورت پروکسی مهلی</string> <string name="summary_pref_socks_port">پورت پروکسی مهلی</string>
@@ -249,9 +249,9 @@
<string name="summary_pref_append_http_proxy">پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.</string> <string name="summary_pref_append_http_proxy">پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.</string>
<string name="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</string> <string name="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</string>
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string> <string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینوو ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن فعال کۊنین</string> <string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن ره ونین</string>
<string name="summary_pref_group_all_display">ی بلگه «پوی بونکۊ یل کانفیگ» ازاف کۊنین</string> <string name="summary_pref_group_all_display">ی بلگه «پوی بونکۊ یل کانفیگ» ازاف کۊنین</string>
<!-- AboutActivity --> <!-- AboutActivity -->
<string name="title_pref_feedback">فشناڌن منشڌ</string> <string name="title_pref_feedback">فشناڌن منشڌ</string>
@@ -268,18 +268,18 @@
<string name="title_pref_promotion">تبلیقات</string> <string name="title_pref_promotion">تبلیقات</string>
<string name="title_pref_auto_update_subscription">ورۊ کردن خوتکار اشتراکا</string> <string name="title_pref_auto_update_subscription">ورۊ کردن خوتکار اشتراکا</string>
<string name="summary_pref_auto_update_subscription">اشتراکا خوتۉ ن و تۉر خوتکار وا فاسله زمۊوی من پس زمینه ورۊ کۊنین. ای ویژیی من پوی دسگایل گاشڌ همیشه کار نکونه</string> <string name="summary_pref_auto_update_subscription">اشتراکا خوتووݩ ن و تۉر خوتکار وا فاسله زمۊوی من پس زمینه ورۊ کۊنین. ای ویژیی من پوی دسگایل گاشڌ همیشه کار نکونه</string>
<string name="title_pref_auto_update_interval">فاسله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ)</string> <string name="title_pref_auto_update_interval">فاسله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ)</string>
<string name="title_core_loglevel">سئت گوزارشا</string> <string name="title_core_loglevel">سئت گوزارشا</string>
<string name="title_outbound_domain_resolve_method">بارت پؽش هل دامنه دری</string> <string name="title_outbound_domain_resolve_method">بارت پؽش هل دامنه دری</string>
<string name="title_mode">هالت</string> <string name="title_mode">هالت</string>
<string name="title_mode_help">سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ</string> <string name="title_mode_help">سی دووسمندیا وو هیاری قلوه، ری ای هؽل بزݩ</string>
<string name="title_language">زووݩ</string> <string name="title_language">زووݩ</string>
<string name="title_ui_settings">سامووا رابت منتوری</string> <string name="title_ui_settings">سامووا رابت منتوری</string>
<string name="title_pref_ui_mode_night">سامووا هالت رابت منتوری</string> <string name="title_pref_ui_mode_night">سامووا هالت رابت منتوری</string>
<string name="title_pref_use_hev_tunnel">فعال کردن ویژیی Hev TUN</string> <string name="title_pref_use_hev_tunnel">ره وندن ویژیی Hev TUN</string>
<string name="summary_pref_use_hev_tunnel">مجالی ک فعال بۊ، TUN ز hev-socks5-tunnel استفاڌه اکونه؛ ٱر فعال نبۊ، زهمو xray-core استفاڌه اکونه.</string> <string name="summary_pref_use_hev_tunnel">مجالی ک ره ونده بۊ، TUN ایا hev-socks5-tunnel ن و کار اگره؛ ٱر ره نوۊفته بۊ، همو xray-core ن و کار اگره.</string>
<string name="title_pref_hev_tunnel_loglevel">Hev Tun سئت گوزارشا</string> <string name="title_pref_hev_tunnel_loglevel">Hev Tun سئت گوزارشا</string>
<string name="title_pref_hev_tunnel_rw_timeout">زمووݩ مندیر بیڌن خوندن/هؽل کردن Hev Tun (سانیه) (پؽش فرز tcp، udp 300،60)</string> <string name="title_pref_hev_tunnel_rw_timeout">زمووݩ مندیر بیڌن خوندن/هؽل کردن Hev Tun (سانیه) (پؽش فرز tcp، udp 300،60)</string>
@@ -290,19 +290,19 @@
<string name="title_del_all_config">پاک کردن پوی کانفیگا بونکۊ سکویی</string> <string name="title_del_all_config">پاک کردن پوی کانفیگا بونکۊ سکویی</string>
<string name="title_del_duplicate_config">پاک کردن کانفیگا تکراری بونکۊ سکویی</string> <string name="title_del_duplicate_config">پاک کردن کانفیگا تکراری بونکۊ سکویی</string>
<string name="title_del_invalid_config">پاک کردن کانفیگا نا موئتبر بونکۊ سکویی</string> <string name="title_del_invalid_config">پاک کردن کانفیگا نا موئتبر بونکۊ سکویی</string>
<string name="title_export_all">و در کشیڌن کانفیگا غیر سفارشی بونکۊ سکویی من کلیپ بورد</string> <string name="title_export_all">و در کشیڌن کانفیگا قیر سفارشی بونکۊ سکویی من کلیپ بورد</string>
<string name="title_sub_setting">سامووا بونکۊ اشتراک</string> <string name="title_sub_setting">سامووا بونکۊ اشتراک</string>
<string name="sub_setting_remarks">نیشتنا</string> <string name="sub_setting_remarks">نیشتنا</string>
<string name="sub_setting_url">نشۊوی اینترنتی اختیاری</string> <string name="sub_setting_url">نشۊوی اینترنتی اختیاری</string>
<string name="sub_setting_user_agent">User Agent</string> <string name="sub_setting_user_agent">User Agent</string>
<string name="sub_setting_filter">نوم موستعار فیلتر</string> <string name="sub_setting_filter">نوم موستعار فیلتر</string>
<string name="sub_setting_enable">فعال بیڌن ورۊ کردن</string> <string name="sub_setting_enable">ره وندن ورۊ کردن</string>
<string name="sub_auto_update">فعال بیڌن ورۊ کردن خوتکار</string> <string name="sub_auto_update">ره وندن ورۊ کردن خوتکار</string>
<string name="sub_allow_insecure_url">موجاز کردن نشۊوی HTTP نا ٱمن</string> <string name="sub_allow_insecure_url">موجاز کردن نشۊوی HTTP نا ٱمن</string>
<string name="sub_setting_pre_profile">نوم موستعار پروکسی دیندایی</string> <string name="sub_setting_pre_profile">نوم موستعار پروکسی دیندایی</string>
<string name="sub_setting_next_profile">نوم موستعار پروکسی نیایی</string> <string name="sub_setting_next_profile">نوم موستعار پروکسی نیایی</string>
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string> <string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string>
<string name="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. اقل مقدار 15 دؽقه هڌ.</string> <string name="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. هدقل مقدار 15 دؽقه هڌ.</string>
<string name="title_sub_update">ورۊ کردن اشتراک بونکۊ سکویی</string> <string name="title_sub_update">ورۊ کردن اشتراک بونکۊ سکویی</string>
<string name="title_ping_all_server">Tcping کانفیگا بونکۊ سکویی</string> <string name="title_ping_all_server">Tcping کانفیگا بونکۊ سکویی</string>
<string name="title_real_ping_all_server">تئخیر واقعی کانفیگا بونکۊ سکویی</string> <string name="title_real_ping_all_server">تئخیر واقعی کانفیگا بونکۊ سکویی</string>
@@ -328,17 +328,17 @@
<!-- 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 یا process</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>
<string name="routing_settings_add_rule">ازاف کردن قانۉݩ</string> <string name="routing_settings_add_rule">ازاف کردن قانووݩ</string>
<string name="routing_settings_import_predefined_rulesets">و من ٱووردن قانووا</string> <string name="routing_settings_import_predefined_rulesets">و من ٱووردن قانووا</string>
<string name="routing_settings_import_rulesets_tip">قانووایی ک هیم سکو هڌسۉݩ پاک ابۊن، هنی هم اخۊی پاکسۉݩ کۊنی؟</string> <string name="routing_settings_import_rulesets_tip">قانووایی ک هیم سکو هڌسووݩ پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟</string>
<string name="routing_settings_import_rulesets_from_clipboard">و من ٱووردن قانووا ز کلیپ بورد</string> <string name="routing_settings_import_rulesets_from_clipboard">و من ٱووردن قانووا ز کلیپ بورد</string>
<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" translatable="false">domain</string> <string name="routing_settings_domain" translatable="false">domain</string>
<string name="routing_settings_ip" translatable="false">ip</string> <string name="routing_settings_ip" translatable="false">ip</string>
<string name="routing_settings_port" translatable="false">port</string> <string name="routing_settings_port" translatable="false">port</string>
@@ -377,12 +377,12 @@
<string name="title_policy_group_type">نوع بونکۊ سیاست</string> <string name="title_policy_group_type">نوع بونکۊ سیاست</string>
<string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string> <string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string>
<string name="title_policy_group_subscription_filter">توزیهات فیلتر معمۊلی</string> <string name="title_policy_group_subscription_filter">توزیهات فیلتر معمۊلی</string>
<string name="server_proxy_chain_members">اعضای زنجیره پروکسی</string> <string name="server_proxy_chain_members">ٱئزا زنجیره پروکسی</string>
<string name="server_proxy_chain_pick_members">اینجا بزنین تا عضو اِنتخاب اکه</string> <string name="server_proxy_chain_pick_members">سی پسند عوزو ایچو بزنین</string>
<string name="server_proxy_chain_member_unselected">یه عضو اِنتخاب کۊنین</string> <string name="server_proxy_chain_member_unselected">ی عوزو پسند کۊنین</string>
<string name="server_proxy_chain_members_unselected">لطفا سی هر ردیو زنجیره، remark اِنتخاب کۊنین</string> <string name="server_proxy_chain_members_unselected">سی هر ردیف زنجیره remark پسند کۊنین</string>
<string name="server_proxy_chain_members_insufficient">تعداد عضوها کافی نیس</string> <string name="server_proxy_chain_members_insufficient">تعداد ٱئزا بس نؽ</string>
<string name="server_proxy_chain_members_invalid">اعضای نامعتبر زنجیره: %1$s</string> <string name="server_proxy_chain_members_invalid">ٱئزا ناموئتبر زنجیره: %1$s</string>
<!-- BackupActivity --> <!-- BackupActivity -->
<string name="title_configuration_backup_restore">لادراری گرؽڌن &amp; وورگندن</string> <string name="title_configuration_backup_restore">لادراری گرؽڌن &amp; وورگندن</string>
@@ -70,7 +70,7 @@
<string name="server_lab_stream_security">TLS</string> <string name="server_lab_stream_security">TLS</string>
<string name="server_lab_stream_fingerprint">Отпечаток</string> <string name="server_lab_stream_fingerprint">Отпечаток</string>
<string name="server_lab_stream_alpn">ALPN</string> <string name="server_lab_stream_alpn">ALPN</string>
<string name="server_lab_allow_insecure">Разрешать небезопасные</string> <string name="server_lab_allow_insecure">Разрешать небезопасные соединения</string>
<string name="server_lab_sni">SNI</string> <string name="server_lab_sni">SNI</string>
<string name="server_lab_address3">Адрес</string> <string name="server_lab_address3">Адрес</string>
<string name="server_lab_port3">Порт</string> <string name="server_lab_port3">Порт</string>
@@ -118,6 +118,8 @@
<string name="server_lab_final_mask">Необработанный JSON FinalMask, формат: { FinalMaskObject }</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_pinned_ca256">Отпечаток сертификата (SHA-256)</string> <string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
<string name="server_lab_browser_dialer">Использовать переадресацию браузера</string>
<string name="server_lab_browser_dialer_tip">Поддерживаются только исходящие соединения XHTTP (packet-up) и WS; настройки, связанные с TLS, могут быть проигнорированы или конфликтовать</string>
<!-- UserAssetActivity --> <!-- UserAssetActivity -->
<string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string> <string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string>
@@ -343,7 +345,7 @@
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string> <string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string>
<string name="routing_settings_domain">Домен</string> <string name="routing_settings_domain">Домен</string>
<string name="routing_settings_ip">IP</string> <string name="routing_settings_ip">IP</string>
<string name="routing_settings_process">Процесс (название пакета; поддерживается только при использовании Xray TUN, включённой функции routeOnly и ОС Android 10+)</string> <string name="routing_settings_process">Процесс (название пакета; поддерживается только при использовании Xray TUN, включённой функции «Домен только для маршрутизации» и ОС Android 10+)</string>
<string name="routing_settings_port">Порт</string> <string name="routing_settings_port">Порт</string>
<string name="routing_settings_protocol">Протокол</string> <string name="routing_settings_protocol">Протокол</string>
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string> <string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
@@ -3,7 +3,7 @@ package com.v2ray.ang.fmt
import android.util.Base64 import android.util.Base64
import com.v2ray.ang.util.LogUtil 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.entities.ProfileItem
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse