Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d64fc627bb | |||
| 6934ab3e5d | |||
| 028d13aef8 | |||
| 4a78c165bc | |||
| c1033a006f | |||
| e8a42013c0 | |||
| 94e5263250 | |||
| 7c68aa9d83 | |||
| ef13e336da | |||
| d16cb21059 | |||
| 237837e7be | |||
| 2a6edd98be | |||
| 9238b80675 | |||
| bb5e6f1959 | |||
| 1cc0865411 | |||
| cb8d979f23 | |||
| 5932b0db29 | |||
| dbe4fe78b5 | |||
| b5bcd81ae4 | |||
| d45ccd7953 | |||
| 6815563902 | |||
| 7c38ad7c57 | |||
| 885aeb384e |
+1
-1
Submodule AndroidLibXrayLite updated: 1b0ec8e111...4c3a3cd051
@@ -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(';')
|
||||||
|
|||||||
@@ -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
-1
@@ -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
-1
@@ -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 = "",
|
||||||
+3
-11
@@ -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
-1
@@ -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? = "",
|
||||||
+2
-2
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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 = "",
|
||||||
+2
-2
@@ -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">لادراری گرؽڌن & وورگندن</string>
|
<string name="title_configuration_backup_restore">لادراری گرؽڌن & وورگندن</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
|
||||||
|
|||||||
Reference in New Issue
Block a user