Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 = 730
|
||||||
versionName = "2.1.8"
|
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"
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -176,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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*/
|
||||||
|
|||||||
@@ -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.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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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,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.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.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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user