Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39d2513df1 | |||
| 3cb4df474b | |||
| b64bb92c8c | |||
| 7f0e2f801d | |||
| 0f7cee0613 | |||
| b2e10fc60b | |||
| aa3f17fb51 | |||
| d64fc627bb | |||
| 6934ab3e5d | |||
| 028d13aef8 | |||
| 4a78c165bc | |||
| c1033a006f | |||
| e8a42013c0 | |||
| 94e5263250 | |||
| 7c68aa9d83 |
@@ -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 = 728
|
versionCode = 731
|
||||||
versionName = "2.1.8"
|
versionName = "2.2.1"
|
||||||
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"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.v2ray.ang.dto.CoreConfigContext
|
|||||||
import com.v2ray.ang.dto.entities.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.isComplexType
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.SettingsManager
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
@@ -13,58 +14,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()
|
||||||
@@ -93,17 +164,14 @@ object CoreConfigContextBuilder {
|
|||||||
}
|
}
|
||||||
.filter { it.server.isNotNullEmpty() }
|
.filter { it.server.isNotNullEmpty() }
|
||||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||||
.filter { it.configType != EConfigType.CUSTOM }
|
.filter { !it.configType.isComplexType() }
|
||||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
|
||||||
.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)
|
||||||
@@ -115,25 +183,21 @@ object CoreConfigContextBuilder {
|
|||||||
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
|
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
|
||||||
.filter { it.server.isNotNullEmpty() }
|
.filter { it.server.isNotNullEmpty() }
|
||||||
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||||
.filter { it.configType != EConfigType.CUSTOM }
|
.filter { !it.configType.isComplexType() }
|
||||||
.filter { it.configType != EConfigType.POLICYGROUP }
|
|
||||||
.filter { it.configType != EConfigType.PROXYCHAIN }
|
|
||||||
.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 +205,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
@@ -3,8 +3,8 @@ package com.v2ray.ang.core
|
|||||||
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.entities.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
|
||||||
@@ -410,7 +410,7 @@ object CoreOutboundBuilder {
|
|||||||
|
|
||||||
NetworkType.WS.type -> {
|
NetworkType.WS.type -> {
|
||||||
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
|
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
|
||||||
wssetting.headers.Host = host.orEmpty()
|
wssetting.host = host.orEmpty()
|
||||||
sni = host
|
sni = host
|
||||||
wssetting.path = path ?: "/"
|
wssetting.path = path ?: "/"
|
||||||
streamSettings.wsSettings = wssetting
|
streamSettings.wsSettings = wssetting
|
||||||
@@ -685,4 +685,4 @@ object CoreOutboundBuilder {
|
|||||||
}
|
}
|
||||||
return resolvedIps.first()
|
return resolvedIps.first()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.v2ray.ang.contracts.ServiceControl
|
|||||||
import com.v2ray.ang.dto.OutboundTrafficStat
|
import com.v2ray.ang.dto.OutboundTrafficStat
|
||||||
import com.v2ray.ang.dto.entities.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.isComplexType
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.NotificationManager
|
import com.v2ray.ang.handler.NotificationManager
|
||||||
@@ -145,9 +146,7 @@ object CoreServiceManager {
|
|||||||
error(context.getString(R.string.toast_config_file_invalid))
|
error(context.getString(R.string.toast_config_file_invalid))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.configType != EConfigType.CUSTOM
|
if (!config.configType.isComplexType()
|
||||||
&& config.configType != EConfigType.POLICYGROUP
|
|
||||||
&& config.configType != EConfigType.PROXYCHAIN
|
|
||||||
&& !Utils.isValidUrl(config.server)
|
&& !Utils.isValidUrl(config.server)
|
||||||
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
||||||
) {
|
) {
|
||||||
@@ -176,7 +175,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import android.content.Context
|
|||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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*/
|
||||||
@@ -199,13 +197,12 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class WsSettingsBean(
|
data class WsSettingsBean(
|
||||||
var path: String? = null,
|
var path: String? = null,
|
||||||
var headers: HeadersBean = HeadersBean(),
|
var host: String? = null,
|
||||||
|
var headers: Map<String, String>? = null,
|
||||||
val maxEarlyData: Int? = null,
|
val maxEarlyData: Int? = null,
|
||||||
val useBrowserForwarding: Boolean? = null,
|
val useBrowserForwarding: Boolean? = null,
|
||||||
val acceptProxyProtocol: Boolean? = null
|
val acceptProxyProtocol: Boolean? = null
|
||||||
) {
|
)
|
||||||
data class HeadersBean(var Host: String = "")
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HttpupgradeSettingsBean(
|
data class HttpupgradeSettingsBean(
|
||||||
var path: String? = null,
|
var path: String? = null,
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.v2ray.ang.AngApplication
|
import com.v2ray.ang.AngApplication
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import es.dmoral.toasty.Toasty
|
import es.dmoral.toasty.Toasty
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@@ -200,4 +201,22 @@ fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean =
|
|||||||
}
|
}
|
||||||
return regex?.containsMatchIn(this)
|
return regex?.containsMatchIn(this)
|
||||||
?: this.contains(keyword, ignoreCase = ignoreCase)
|
?: this.contains(keyword, ignoreCase = ignoreCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the config type is a group type (PolicyGroup or ProxyChain).
|
||||||
|
*
|
||||||
|
* @return True if the config type is PolicyGroup or ProxyChain, false otherwise.
|
||||||
|
*/
|
||||||
|
fun EConfigType.isGroupType(): Boolean {
|
||||||
|
return this == EConfigType.POLICYGROUP || this == EConfigType.PROXYCHAIN
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the config type is a complex type (Custom, PolicyGroup, or ProxyChain).
|
||||||
|
*
|
||||||
|
* @return True if the config type is Custom, PolicyGroup, or ProxyChain, false otherwise.
|
||||||
|
*/
|
||||||
|
fun EConfigType.isComplexType(): Boolean {
|
||||||
|
return this == EConfigType.CUSTOM || this == EConfigType.POLICYGROUP || this == EConfigType.PROXYCHAIN
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.v2ray.ang.fmt
|
package com.v2ray.ang.fmt
|
||||||
|
|
||||||
import com.v2ray.ang.dto.entities.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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.entities.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
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import android.text.TextUtils
|
|||||||
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.CoreConfigManager
|
import com.v2ray.ang.core.CoreConfigManager
|
||||||
|
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.ProfileItem
|
||||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||||
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
|
||||||
import com.v2ray.ang.dto.UrlContentRequest
|
|
||||||
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
|
||||||
@@ -234,15 +234,8 @@ object AngConfigManager {
|
|||||||
if (servers == null) {
|
if (servers == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
// Find the currently selected server that matches the subscription ID
|
// Find the currently selected server that belongs to the same subscription before replacement.
|
||||||
val removedSelected = if (subid.isNotBlank() && !append) {
|
val removedSelected = getRemovedSelectedProfile(subid, append)
|
||||||
MmkvManager.getSelectServer()
|
|
||||||
.takeIf { it?.isNotBlank() == true }
|
|
||||||
?.let { MmkvManager.decodeServerConfig(it) }
|
|
||||||
?.takeIf { it.subscriptionId == subid }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
|
|
||||||
@@ -288,7 +281,6 @@ object AngConfigManager {
|
|||||||
|
|
||||||
// Read serverList once
|
// Read serverList once
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
val serverList = MmkvManager.decodeServerList(subid)
|
||||||
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
|
|
||||||
|
|
||||||
configs.forEach { config ->
|
configs.forEach { config ->
|
||||||
val key = Utils.getUuid()
|
val key = Utils.getUuid()
|
||||||
@@ -297,10 +289,6 @@ object AngConfigManager {
|
|||||||
|
|
||||||
if (!serverList.contains(key)) {
|
if (!serverList.contains(key)) {
|
||||||
serverList.add(0, key)
|
serverList.add(0, key)
|
||||||
if (needSetSelected) {
|
|
||||||
MmkvManager.setSelectServer(key)
|
|
||||||
needSetSelected = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
keyToProfile[key] = config
|
keyToProfile[key] = config
|
||||||
}
|
}
|
||||||
@@ -323,7 +311,8 @@ object AngConfigManager {
|
|||||||
* @return Matched key or null
|
* @return Matched key or null
|
||||||
*/
|
*/
|
||||||
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()) return null
|
||||||
|
if (target == null) return null
|
||||||
|
|
||||||
// Level 0: Full match (remarks + server + port + password)
|
// Level 0: Full match (remarks + server + port + password)
|
||||||
if (target.remarks.isNotBlank()) {
|
if (target.remarks.isNotBlank()) {
|
||||||
@@ -360,7 +349,20 @@ object AngConfigManager {
|
|||||||
isSameText(saved.server, target.server)
|
isSameText(saved.server, target.server)
|
||||||
}?.key?.let { return it }
|
}?.key?.let { return it }
|
||||||
|
|
||||||
return null
|
// If old selected node cannot be matched, fall back to the first imported config.
|
||||||
|
return keyToProfile.keys.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently selected profile if it belongs to the target subscription and will be replaced.
|
||||||
|
*/
|
||||||
|
private fun getRemovedSelectedProfile(subid: String, append: Boolean): ProfileItem? {
|
||||||
|
if (subid.isBlank() || append) return null
|
||||||
|
|
||||||
|
return MmkvManager.getSelectServer()
|
||||||
|
.takeIf { it?.isNotBlank() == true }
|
||||||
|
?.let { MmkvManager.decodeServerConfig(it) }
|
||||||
|
?.takeIf { it.subscriptionId == subid }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -396,18 +398,25 @@ object AngConfigManager {
|
|||||||
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
if (serverList.isNotEmpty()) {
|
||||||
|
val removedSelected = getRemovedSelectedProfile(subid, append)
|
||||||
if (!append) {
|
if (!append) {
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
var count = 0
|
var count = 0
|
||||||
|
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
||||||
for (srv in serverList.reversed()) {
|
for (srv in serverList.reversed()) {
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||||
config.subscriptionId = subid
|
config.subscriptionId = subid
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
|
||||||
|
keyToProfile[key] = config
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||||
|
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||||
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.V2rayConfig
|
||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
import com.v2ray.ang.dto.entities.RulesetItem
|
import com.v2ray.ang.dto.entities.RulesetItem
|
||||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ object SpeedtestManager {
|
|||||||
proxyPassword = proxyPassword
|
proxyPassword = proxyPassword
|
||||||
)
|
)
|
||||||
) ?: return null
|
) ?: return null
|
||||||
val ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
|
val ipInfo = JsonUtil.fromJsonSafe(content, IPAPIInfo::class.java) ?: return null
|
||||||
|
|
||||||
val ip = listOf(
|
val ip = listOf(
|
||||||
ipInfo.ip,
|
ipInfo.ip,
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ object UpdateCheckerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
|||||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
import com.v2ray.ang.dto.entities.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.isComplexType
|
||||||
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
|
||||||
@@ -269,9 +270,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
|
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
|
||||||
val isCustom = profile.configType == EConfigType.CUSTOM
|
val isCustom = profile.configType.isComplexType()
|
||||||
|| profile.configType == EConfigType.POLICYGROUP
|
|
||||||
|| profile.configType == EConfigType.PROXYCHAIN
|
|
||||||
|
|
||||||
val (shareOptions, skip) = if (more) {
|
val (shareOptions, skip) = if (more) {
|
||||||
val options = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
val options = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -18,6 +20,11 @@ import com.v2ray.ang.viewmodel.LogcatViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
||||||
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
|
||||||
@@ -45,6 +52,55 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shareLogcat() {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val logText = viewModel.getAll().joinToString("\n")
|
||||||
|
|
||||||
|
val result = try {
|
||||||
|
val shareDir = File(cacheDir, "shared_logs").apply {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
shareDir.listFiles()?.forEach { it.delete() }
|
||||||
|
|
||||||
|
val timestamp = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US).format(Date())
|
||||||
|
val logFile = File(shareDir, "v2rayNG_logcat_$timestamp.txt")
|
||||||
|
logFile.writeText(logText, Charsets.UTF_8)
|
||||||
|
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
this@LogcatActivity,
|
||||||
|
"${packageName}.cache",
|
||||||
|
logFile
|
||||||
|
)
|
||||||
|
|
||||||
|
uri to logFile.name
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
toast(e.localizedMessage ?: e.toString())
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_STREAM, result.first)
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, result.second)
|
||||||
|
putExtra(Intent.EXTRA_TITLE, result.second)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
clipData = ClipData.newUri(contentResolver, result.second, result.first)
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
shareIntent,
|
||||||
|
getString(R.string.logcat_share)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_logcat, menu)
|
menuInflater.inflate(R.menu.menu_logcat, menu)
|
||||||
|
|
||||||
@@ -78,6 +134,11 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.share_all -> {
|
||||||
|
shareLogcat()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.clear_all -> {
|
R.id.clear_all -> {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
viewModel.clearLogcat()
|
viewModel.clearLogcat()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -14,6 +15,7 @@ import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
|||||||
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
||||||
import com.v2ray.ang.dto.entities.ProfileItem
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
import com.v2ray.ang.dto.entities.ServersCache
|
import com.v2ray.ang.dto.entities.ServersCache
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
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
|
||||||
@@ -58,7 +60,7 @@ class MainRecyclerAdapter(
|
|||||||
//Name address
|
//Name address
|
||||||
holder.itemMainBinding.tvName.text = profile.remarks
|
holder.itemMainBinding.tvName.text = profile.remarks
|
||||||
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
||||||
holder.itemMainBinding.tvType.text = profile.configType.name
|
holder.itemMainBinding.tvType.text = getProtocolDescription(profile)
|
||||||
|
|
||||||
//TestResult
|
//TestResult
|
||||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
@@ -140,6 +142,31 @@ class MainRecyclerAdapter(
|
|||||||
return subRemarks?.toString() ?: ""
|
return subRemarks?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getProtocolDescription(profile: ProfileItem): String {
|
||||||
|
if (profile.configType.isComplexType()) {
|
||||||
|
return profile.configType.name
|
||||||
|
}
|
||||||
|
|
||||||
|
val parts = mutableListOf<String>()
|
||||||
|
parts.add(profile.configType.name)
|
||||||
|
|
||||||
|
// Transport: hide tcp or blank
|
||||||
|
profile.network?.let { net ->
|
||||||
|
if (net.isNotBlank() && !net.equals("tcp", ignoreCase = true)) {
|
||||||
|
parts.add(net)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: hide blank or tls
|
||||||
|
profile.security?.let { sec ->
|
||||||
|
if (sec.isNotBlank() && !sec.equals("tls", ignoreCase = true)) {
|
||||||
|
parts.add(sec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.joinToString(" / ")
|
||||||
|
}
|
||||||
|
|
||||||
fun removeServerSub(guid: String, position: Int) {
|
fun removeServerSub(guid: String, position: Int) {
|
||||||
val idx = data.indexOfFirst { it.guid == guid }
|
val idx = data.indexOfFirst { it.guid == guid }
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
@@ -203,4 +230,4 @@ class MainRecyclerAdapter(
|
|||||||
|
|
||||||
override fun onItemDismiss(position: Int) {
|
override fun onItemDismiss(position: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ 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)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.v2ray.ang.contracts.BaseAdapterListener
|
|||||||
import com.v2ray.ang.databinding.ActivityServerProxyChainBinding
|
import com.v2ray.ang.databinding.ActivityServerProxyChainBinding
|
||||||
import com.v2ray.ang.dto.entities.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.isComplexType
|
||||||
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
|
||||||
@@ -55,18 +56,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() {
|
||||||
@@ -114,10 +110,7 @@ class ServerProxyChainActivity : BaseActivity() {
|
|||||||
|
|
||||||
val invalidMembers = chainMembers.filter { member ->
|
val invalidMembers = chainMembers.filter { member ->
|
||||||
val profile = SettingsManager.getServerViaRemarks(member)
|
val profile = SettingsManager.getServerViaRemarks(member)
|
||||||
profile == null
|
profile == null || profile.configType.isComplexType()
|
||||||
|| profile.configType == EConfigType.CUSTOM
|
|
||||||
|| profile.configType == EConfigType.POLICYGROUP
|
|
||||||
|| profile.configType == EConfigType.PROXYCHAIN
|
|
||||||
}
|
}
|
||||||
if (invalidMembers.isNotEmpty()) {
|
if (invalidMembers.isNotEmpty()) {
|
||||||
toast(getString(R.string.server_proxy_chain_members_invalid, invalidMembers.joinToString(", ")))
|
toast(getString(R.string.server_proxy_chain_members_invalid, invalidMembers.joinToString(", ")))
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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.entities.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
|
||||||
@@ -78,10 +79,13 @@ class SubEditActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupProfileRemarkInputs() {
|
private fun setupProfileRemarkInputs() {
|
||||||
val suggestions = MmkvManager.decodeAllServerList()
|
val suggestions = SettingsManager.getProfileRemarks(
|
||||||
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
|
excludeConfigTypes = setOf(
|
||||||
.filter { it.isNotBlank() }
|
EConfigType.CUSTOM,
|
||||||
.distinct()
|
EConfigType.POLICYGROUP,
|
||||||
|
EConfigType.PROXYCHAIN,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
setupProfileRemarkInput(binding.etPreProfile, binding.btnPreProfileDropdown, suggestions)
|
setupProfileRemarkInput(binding.etPreProfile, binding.btnPreProfileDropdown, suggestions)
|
||||||
setupProfileRemarkInput(binding.etNextProfile, binding.btnNextProfileDropdown, suggestions)
|
setupProfileRemarkInput(binding.etNextProfile, binding.btnNextProfileDropdown, suggestions)
|
||||||
|
|||||||
@@ -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,11 @@ 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.entities.ServersCache
|
|
||||||
import com.v2ray.ang.dto.entities.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.isComplexType
|
||||||
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
|
||||||
@@ -296,16 +297,28 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes duplicate servers.
|
* Removes duplicate servers.
|
||||||
|
* Excludes servers with complex types (Custom, PolicyGroup, or ProxyChain) from duplicate comparison.
|
||||||
* @return The number of removed servers.
|
* @return The number of removed servers.
|
||||||
*/
|
*/
|
||||||
fun removeDuplicateServer(): Int {
|
fun removeDuplicateServer(): Int {
|
||||||
val serversCacheCopy = serversCache.toList().toMutableList()
|
val serversCacheCopy = serversCache.toList().toMutableList()
|
||||||
val deleteServer = mutableListOf<String>()
|
val deleteServer = mutableListOf<String>()
|
||||||
|
|
||||||
serversCacheCopy.forEachIndexed { index, sc ->
|
serversCacheCopy.forEachIndexed { index, sc ->
|
||||||
val profile = sc.profile
|
val profile = sc.profile
|
||||||
|
// Skip if this profile has a complex config type
|
||||||
|
if (profile.configType.isComplexType()) {
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
|
||||||
serversCacheCopy.forEachIndexed { index2, sc2 ->
|
serversCacheCopy.forEachIndexed { index2, sc2 ->
|
||||||
if (index2 > index) {
|
if (index2 > index) {
|
||||||
val profile2 = sc2.profile
|
val profile2 = sc2.profile
|
||||||
|
// Skip if the second profile has a complex config type
|
||||||
|
if (profile2.configType.isComplexType()) {
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
|
||||||
if (profile == profile2 && !deleteServer.contains(sc2.guid)) {
|
if (profile == profile2 && !deleteServer.contains(sc2.guid)) {
|
||||||
deleteServer.add(sc2.guid)
|
deleteServer.add(sc2.guid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +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.UrlContentRequest
|
||||||
import com.v2ray.ang.dto.entities.AssetUrlCache
|
import com.v2ray.ang.dto.entities.AssetUrlCache
|
||||||
import com.v2ray.ang.dto.entities.AssetUrlItem
|
import com.v2ray.ang.dto.entities.AssetUrlItem
|
||||||
import com.v2ray.ang.dto.UrlContentRequest
|
|
||||||
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
|
||||||
|
|||||||
@@ -17,4 +17,9 @@
|
|||||||
android:icon="@drawable/ic_copy"
|
android:icon="@drawable/ic_copy"
|
||||||
android:title="@string/logcat_copy"
|
android:title="@string/logcat_copy"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/share_all"
|
||||||
|
android:icon="@drawable/ic_share_24dp"
|
||||||
|
android:title="@string/logcat_share"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
</menu>
|
</menu>
|
||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">نسخ</string>
|
<string name="logcat_copy">نسخ</string>
|
||||||
<string name="logcat_clear">مسح</string>
|
<string name="logcat_clear">مسح</string>
|
||||||
|
<string name="logcat_share">مشاركة السجل</string>
|
||||||
<string name="title_service_restart">إعادة تشغيل الخدمة</string>
|
<string name="title_service_restart">إعادة تشغيل الخدمة</string>
|
||||||
<string name="title_del_all_config">حذف جميع الإعدادات (قبل أول خطوة)</string>
|
<string name="title_del_all_config">حذف جميع الإعدادات (قبل أول خطوة)</string>
|
||||||
<string name="title_del_duplicate_config">حذف الإعدادات المكررة (2)</string>
|
<string name="title_del_duplicate_config">حذف الإعدادات المكررة (2)</string>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">লগক্যাট</string>
|
<string name="title_logcat">লগক্যাট</string>
|
||||||
<string name="logcat_copy">কপি করুন</string>
|
<string name="logcat_copy">কপি করুন</string>
|
||||||
<string name="logcat_clear">স্পষ্ট করুন</string>
|
<string name="logcat_clear">স্পষ্ট করুন</string>
|
||||||
|
<string name="logcat_share">লগ শেয়ার করুন</string>
|
||||||
<string name="title_service_restart">সার্ভিস পুনরায় চালু করুন</string>
|
<string name="title_service_restart">সার্ভিস পুনরায় চালু করুন</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">گوزارشا</string>
|
<string name="title_logcat">گوزارشا</string>
|
||||||
<string name="logcat_copy">لف گیری</string>
|
<string name="logcat_copy">لف گیری</string>
|
||||||
<string name="logcat_clear">روفتن</string>
|
<string name="logcat_clear">روفتن</string>
|
||||||
|
<string name="logcat_share">Share log</string>
|
||||||
<string name="title_service_restart">ره وندن دووارته خدمات</string>
|
<string name="title_service_restart">ره وندن دووارته خدمات</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
<string name="title_logcat">گزارشات</string>
|
<string name="title_logcat">گزارشات</string>
|
||||||
<string name="logcat_copy">کپی</string>
|
<string name="logcat_copy">کپی</string>
|
||||||
<string name="logcat_clear">پاک کردن</string>
|
<string name="logcat_clear">پاک کردن</string>
|
||||||
|
<string name="logcat_share">اشتراکگذاری گزارش</string>
|
||||||
<string name="title_service_restart">راهاندازی مجدد خدمات</string>
|
<string name="title_service_restart">راهاندازی مجدد خدمات</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -291,6 +291,7 @@
|
|||||||
<string name="title_logcat">Журнал</string>
|
<string name="title_logcat">Журнал</string>
|
||||||
<string name="logcat_copy">Копировать</string>
|
<string name="logcat_copy">Копировать</string>
|
||||||
<string name="logcat_clear">Очистить</string>
|
<string name="logcat_clear">Очистить</string>
|
||||||
|
<string name="logcat_share">Поделиться логом</string>
|
||||||
<string name="title_service_restart">Перезапуск службы</string>
|
<string name="title_service_restart">Перезапуск службы</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">Sao chép</string>
|
<string name="logcat_copy">Sao chép</string>
|
||||||
<string name="logcat_clear">Xoá</string>
|
<string name="logcat_clear">Xoá</string>
|
||||||
|
<string name="logcat_share">Chia sẻ nhật ký</string>
|
||||||
<string name="title_service_restart">Khởi động lại dịch vụ lõi</string>
|
<string name="title_service_restart">Khởi động lại dịch vụ lõi</string>
|
||||||
<string name="title_del_all_config">Xoá tất cả cấu hình</string>
|
<string name="title_del_all_config">Xoá tất cả cấu hình</string>
|
||||||
<string name="title_del_duplicate_config">Xoá cấu hình trùng lặp</string>
|
<string name="title_del_duplicate_config">Xoá cấu hình trùng lặp</string>
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">复制</string>
|
<string name="logcat_copy">复制</string>
|
||||||
<string name="logcat_clear">清除</string>
|
<string name="logcat_clear">清除</string>
|
||||||
|
<string name="logcat_share">分享日志</string>
|
||||||
<string name="title_service_restart">服务重启</string>
|
<string name="title_service_restart">服务重启</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">複製</string>
|
<string name="logcat_copy">複製</string>
|
||||||
<string name="logcat_clear">清除</string>
|
<string name="logcat_clear">清除</string>
|
||||||
|
<string name="logcat_share">分享日誌</string>
|
||||||
<string name="title_service_restart">重啟服務</string>
|
<string name="title_service_restart">重啟服務</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -293,6 +293,7 @@
|
|||||||
<string name="title_logcat">Logcat</string>
|
<string name="title_logcat">Logcat</string>
|
||||||
<string name="logcat_copy">Copy</string>
|
<string name="logcat_copy">Copy</string>
|
||||||
<string name="logcat_clear">Clear</string>
|
<string name="logcat_clear">Clear</string>
|
||||||
|
<string name="logcat_share">Share log</string>
|
||||||
<string name="title_service_restart">Service restart</string>
|
<string name="title_service_restart">Service restart</string>
|
||||||
<string name="title_del_all_config">Delete config</string>
|
<string name="title_del_all_config">Delete config</string>
|
||||||
<string name="title_del_duplicate_config">Delete duplicate config</string>
|
<string name="title_del_duplicate_config">Delete duplicate config</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user