Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d64fc627bb | |||
| 6934ab3e5d | |||
| 028d13aef8 | |||
| 4a78c165bc | |||
| c1033a006f | |||
| e8a42013c0 | |||
| 94e5263250 | |||
| 7c68aa9d83 |
@@ -6,14 +6,14 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "com.v2ray.ang"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 728
|
||||
versionName = "2.1.8"
|
||||
targetSdk = 37
|
||||
versionCode = 730
|
||||
versionName = "2.2.0"
|
||||
multiDexEnabled = true
|
||||
|
||||
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.CHANGE_NETWORK_STATE" />
|
||||
<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.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"
|
||||
tools:ignore="ForegroundServicesPolicy" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
android:minSdkVersion="34" />
|
||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||
<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" />
|
||||
|
||||
<application
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -73,8 +74,10 @@
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
"streamSettings": {
|
||||
"sockopt": {
|
||||
"domainStrategy": "UseIP"
|
||||
}
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -46,7 +47,8 @@
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -89,8 +91,10 @@
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"domainStrategy": "UseIP"
|
||||
"streamSettings": {
|
||||
"sockopt": {
|
||||
"domainStrategy": "UseIP"
|
||||
}
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
|
||||
@@ -105,7 +105,8 @@ object AppConfig {
|
||||
const val TAG_FRAGMENT = "fragment"
|
||||
const val TAG_DNS = "dns-module"
|
||||
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. */
|
||||
const val UPLINK = "uplink"
|
||||
|
||||
@@ -13,58 +13,128 @@ import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
/**
|
||||
* Builds [com.v2ray.ang.dto.CoreConfigContext] from the selected profile.
|
||||
* Keeps parsing and resolution logic out of [CoreConfigManager].
|
||||
* Build runtime context from the selected profile.
|
||||
*
|
||||
* 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 {
|
||||
|
||||
/** 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? {
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return null
|
||||
|
||||
// CUSTOM: return immediately — CoreConfigManager handles this path on its own.
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
return CoreConfigContext(
|
||||
context = context,
|
||||
guid = guid,
|
||||
selectedProfile = config,
|
||||
resolvedProfiles = listOf(config),
|
||||
resolvedType = CoreResolvedType.CUSTOM,
|
||||
)
|
||||
return CoreConfigContext(context = context, guid = guid, isCustom = true)
|
||||
}
|
||||
|
||||
// Pre-resolve custom outbound profiles from routing rulesets
|
||||
val customOutbounds = resolveCustomOutbounds()
|
||||
// Step 1: Resolve the main outbound (always tag = TAG_PROXY).
|
||||
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
|
||||
val (resolvedProfiles, resolvedType) = when (config.configType) {
|
||||
EConfigType.POLICYGROUP -> {
|
||||
val profiles = resolvePolicyGroupProfiles(config)
|
||||
Pair(profiles, CoreResolvedType.POLICYGROUP)
|
||||
}
|
||||
// Step 2: Resolve all non-builtin routing outbound tags.
|
||||
val routingResolvedOutbounds = resolveRoutingOutbounds()
|
||||
|
||||
return CoreConfigContext(
|
||||
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 -> {
|
||||
val profiles = resolveProxyChainProfiles(config)
|
||||
Pair(profiles, CoreResolvedType.PROXYCHAIN)
|
||||
val chainProfiles = resolveProxyChainProfiles(profile)
|
||||
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
|
||||
Pair(chainProfiles, type)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val chainProfiles = resolveProxyChainProfilesFromGroup(config)
|
||||
val chainProfiles = resolveProxyChainProfilesFromGroup(profile)
|
||||
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
|
||||
Pair(chainProfiles, type)
|
||||
}
|
||||
}
|
||||
|
||||
// Create context with common fields
|
||||
return CoreConfigContext(
|
||||
context = context,
|
||||
guid = guid,
|
||||
selectedProfile = config,
|
||||
return CoreConfigContext.ResolvedOutbound(
|
||||
tag = tag,
|
||||
profile = profile,
|
||||
resolvedProfiles = resolvedProfiles,
|
||||
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> {
|
||||
try {
|
||||
val serverList = MmkvManager.decodeAllServerList()
|
||||
@@ -98,12 +168,11 @@ object CoreConfigContextBuilder {
|
||||
.filter { it.configType != EConfigType.PROXYCHAIN }
|
||||
.toList()
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolves proxy-chain members with the same filters as runtime build. */
|
||||
private fun resolveProxyChainProfiles(config: ProfileItem): List<ProfileItem> {
|
||||
if (config.proxyChainProfiles.isNullOrBlank()) {
|
||||
return listOf(config)
|
||||
@@ -121,19 +190,17 @@ object CoreConfigContextBuilder {
|
||||
.toList()
|
||||
.reversed()
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves chain nodes in fixed order: next -> current -> prev.
|
||||
* If chain cannot be built, caller treats result as normal mode.
|
||||
* Resolve chain nodes from subscription neighbors in order: next, current, prev.
|
||||
*
|
||||
* When no chain is available, return a single-node result.
|
||||
*/
|
||||
private fun resolveProxyChainProfilesFromGroup(config: ProfileItem): List<ProfileItem> {
|
||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
|
||||
return listOf(config)
|
||||
}
|
||||
if (config.subscriptionId.isEmpty()) {
|
||||
return listOf(config)
|
||||
}
|
||||
@@ -141,56 +208,13 @@ object CoreConfigContextBuilder {
|
||||
try {
|
||||
val subItem = MmkvManager.decodeSubscription(config.subscriptionId) ?: return listOf(config)
|
||||
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) }
|
||||
resolved.add(config)
|
||||
SettingsManager.getServerViaRemarks(subItem.prevProfile)?.let { resolved.add(it) }
|
||||
|
||||
return resolved
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.JsonObject
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
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.NetworkType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
|
||||
@@ -176,7 +176,20 @@ object CoreServiceManager {
|
||||
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.enums.CoreResolvedType
|
||||
|
||||
/**
|
||||
* Runtime context produced by the builder and consumed by CoreConfigManager.
|
||||
*/
|
||||
data class CoreConfigContext(
|
||||
val context: Context,
|
||||
val guid: String,
|
||||
val selectedProfile: ProfileItem,
|
||||
val resolvedProfiles: List<ProfileItem>,
|
||||
val resolvedType: CoreResolvedType,
|
||||
val customOutboundProfiles: Map<String, ProfileItem> = emptyMap(),
|
||||
)
|
||||
val isCustom: Boolean = false,
|
||||
val resolvedOutbounds: List<ResolvedOutbound> = emptyList(),
|
||||
) {
|
||||
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 settings: OutSettingsBean? = null,
|
||||
var streamSettings: StreamSettingsBean? = null,
|
||||
val proxySettings: Any? = null,
|
||||
val sendThrough: String? = null,
|
||||
var mux: MuxBean? = MuxBean(false)
|
||||
) {
|
||||
@@ -82,7 +81,6 @@ data class V2rayConfig(
|
||||
var address: Any? = null,
|
||||
var port: Int? = null,
|
||||
/*Freedom*/
|
||||
var domainStrategy: String? = null,
|
||||
val redirect: String? = null,
|
||||
val userLevel: Int? = null,
|
||||
/*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,
|
||||
POLICYGROUP,
|
||||
PROXYCHAIN,
|
||||
CUSTOM,
|
||||
}
|
||||
@@ -13,17 +13,6 @@ enum class PermissionType {
|
||||
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+) */
|
||||
POST_NOTIFICATIONS {
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
@@ -37,7 +26,6 @@ enum class PermissionType {
|
||||
fun getLabel(): String {
|
||||
return when (this) {
|
||||
CAMERA -> "Camera"
|
||||
READ_STORAGE -> "Storage"
|
||||
POST_NOTIFICATIONS -> "Notification"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ object SocksFmt : FmtBase() {
|
||||
config.serverPort = uri.port.toString()
|
||||
|
||||
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) {
|
||||
config.username = result.first()
|
||||
config.password = result.last()
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.v2ray.ang.fmt
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.VmessQRCode
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
|
||||
@@ -6,11 +6,11 @@ import android.text.TextUtils
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
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.SubscriptionCache
|
||||
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.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.fmt.CustomFmt
|
||||
|
||||
@@ -102,7 +102,7 @@ object MmkvManager {
|
||||
return if (json.isNullOrBlank()) {
|
||||
mutableListOf()
|
||||
} 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()) {
|
||||
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()) {
|
||||
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 ->
|
||||
val json = subStorage.decodeString(key)
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -420,7 +420,7 @@ object MmkvManager {
|
||||
*/
|
||||
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
|
||||
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()) {
|
||||
mutableListOf()
|
||||
} 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 ->
|
||||
val json = assetStorage.decodeString(key)
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -495,7 +495,7 @@ object MmkvManager {
|
||||
*/
|
||||
fun decodeAsset(assetid: String): AssetUrlItem? {
|
||||
val json = assetStorage.decodeString(assetid) ?: return null
|
||||
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
|
||||
return JsonUtil.fromJsonSafe(json, AssetUrlItem::class.java)
|
||||
}
|
||||
|
||||
//endregion
|
||||
@@ -510,7 +510,7 @@ object MmkvManager {
|
||||
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
|
||||
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
|
||||
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? {
|
||||
val json = mainStorage.decodeString(KEY_WEBDAV_CONFIG) ?: return null
|
||||
return JsonUtil.fromJson(json, WebDavConfig::class.java)
|
||||
return JsonUtil.fromJsonSafe(json, WebDavConfig::class.java)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
@@ -12,10 +12,10 @@ import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
|
||||
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
|
||||
import com.v2ray.ang.AppConfig.TAG_DIRECT
|
||||
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.RulesetItem
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.Language
|
||||
import com.v2ray.ang.enums.RoutingType
|
||||
@@ -73,7 +73,7 @@ object SettingsManager {
|
||||
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 {
|
||||
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java)?.toMutableList()
|
||||
val rulesetList = JsonUtil.fromJsonSafe(content, Array<RulesetItem>::class.java)?.toMutableList()
|
||||
if (rulesetList.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
@@ -191,7 +191,7 @@ object SettingsManager {
|
||||
val config = decodeServerConfig(guid) ?: return false
|
||||
if (config.configType == EConfigType.CUSTOM) {
|
||||
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 {
|
||||
it.domain?.contains(GEOSITE_PRIVATE) == true || it.ip?.contains(GEOIP_PRIVATE) == true
|
||||
}
|
||||
@@ -246,6 +246,20 @@ object SettingsManager {
|
||||
.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.
|
||||
* If there are no remaining subscriptions,
|
||||
@@ -571,7 +585,7 @@ object SettingsManager {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ object SpeedtestManager {
|
||||
proxyPassword = proxyPassword
|
||||
)
|
||||
) ?: 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(
|
||||
ipInfo.ip,
|
||||
|
||||
@@ -45,11 +45,11 @@ object UpdateCheckerManager {
|
||||
}
|
||||
|
||||
val latestRelease = if (includePreRelease) {
|
||||
JsonUtil.fromJson(response, Array<GitHubRelease>::class.java)
|
||||
JsonUtil.fromJsonSafe(response, Array<GitHubRelease>::class.java)
|
||||
?.firstOrNull()
|
||||
?: throw IllegalStateException("No pre-release found")
|
||||
} else {
|
||||
JsonUtil.fromJson(response, GitHubRelease::class.java)
|
||||
JsonUtil.fromJsonSafe(response, GitHubRelease::class.java)
|
||||
}
|
||||
if (latestRelease == null) {
|
||||
return@withContext CheckUpdateResult(hasUpdate = false)
|
||||
|
||||
@@ -56,9 +56,7 @@ abstract class HelperBaseActivity : BaseActivity() {
|
||||
mimeType: String = "*/*",
|
||||
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.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -78,9 +77,7 @@ class RoutingEditActivity : BaseActivity() {
|
||||
* The dropdown button triggers showing the full list without typing.
|
||||
*/
|
||||
private fun setupOutboundTagInput() {
|
||||
val profileRemarks = MmkvManager.decodeAllServerList()
|
||||
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
|
||||
.filter { it.isNotBlank() }
|
||||
val profileRemarks = SettingsManager.getProfileRemarks()
|
||||
|
||||
val suggestions = (BUILTIN_OUTBOUND_TAGS.toList() + profileRemarks).distinct()
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
|
||||
|
||||
@@ -55,18 +55,13 @@ class ServerProxyChainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun loadAvailableRemarks() {
|
||||
allRemarks = MmkvManager.decodeAllServerList()
|
||||
.asSequence()
|
||||
.mapNotNull { guid -> MmkvManager.decodeServerConfig(guid) }
|
||||
.filter { profile ->
|
||||
profile.configType != EConfigType.CUSTOM
|
||||
&& profile.configType != EConfigType.POLICYGROUP
|
||||
&& profile.configType != EConfigType.PROXYCHAIN
|
||||
}
|
||||
.map { it.remarks.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinct()
|
||||
.toList()
|
||||
allRemarks = SettingsManager.getProfileRemarks(
|
||||
excludeConfigTypes = setOf(
|
||||
EConfigType.CUSTOM,
|
||||
EConfigType.POLICYGROUP,
|
||||
EConfigType.PROXYCHAIN,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupRecycler() {
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivitySubEditBinding
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
@@ -78,10 +79,13 @@ class SubEditActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun setupProfileRemarkInputs() {
|
||||
val suggestions = MmkvManager.decodeAllServerList()
|
||||
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
val suggestions = SettingsManager.getProfileRemarks(
|
||||
excludeConfigTypes = setOf(
|
||||
EConfigType.CUSTOM,
|
||||
EConfigType.POLICYGROUP,
|
||||
EConfigType.PROXYCHAIN,
|
||||
)
|
||||
)
|
||||
|
||||
setupProfileRemarkInput(binding.etPreProfile, binding.btnPreProfileDropdown, suggestions)
|
||||
setupProfileRemarkInput(binding.etNextProfile, binding.btnNextProfileDropdown, suggestions)
|
||||
|
||||
@@ -35,6 +35,23 @@ object JsonUtil {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -14,10 +14,10 @@ import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
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.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.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
|
||||
@@ -2,9 +2,9 @@ package com.v2ray.ang.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.UrlContentRequest
|
||||
import com.v2ray.ang.dto.entities.AssetUrlCache
|
||||
import com.v2ray.ang.dto.entities.AssetUrlItem
|
||||
import com.v2ray.ang.dto.UrlContentRequest
|
||||
import com.v2ray.ang.extension.concatUrl
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
|
||||
Reference in New Issue
Block a user