Compare commits

..

24 Commits

Author SHA1 Message Date
2dust a155b24c1e up 2.2.2 2026-06-01 16:02:15 +08:00
2dust e805fcf088 Fix
https://github.com/2dust/v2rayNG/issues/5720
2026-06-01 15:55:06 +08:00
2dust 95e37db42e Update AndroidLibXrayLite 2026-06-01 15:05:39 +08:00
2dust fe812c4456 Remove allowInsecure preference and default to secure 2026-06-01 15:04:26 +08:00
fuilloi d5af0fe735 Update build.yml (#5718) 2026-06-01 13:46:07 +08:00
Hossein Abaspanah 9126aa9c9e Update Luri Bakhtiari translation (#5713)
* Update strings.xml

* Update strings new.xml

* Update strings.xml
2026-06-01 13:45:49 +08:00
Skh-web6982 4a763d2ec5 Update kotlin version to 2.3.21 (#5701)
* Update kotlin

* Update kotlin version to 2.3.21
2026-05-31 10:27:26 +08:00
Skh-web6982 08d6de4504 Upgrade Gradle wrapper (#5700) 2026-05-31 10:27:11 +08:00
Alexey 629d410189 fix: allow IPv6 policy group members (#5697) 2026-05-28 20:42:40 +08:00
2dust 39d2513df1 up 2.2.1 2026-05-27 19:59:55 +08:00
2dust 3cb4df474b Keep selected server when replacing subscription
https://github.com/2dust/v2rayNG/pull/5676
2026-05-27 19:53:18 +08:00
fuilloi b64bb92c8c Update MainRecyclerAdapter.kt (#5692)
* Update MainRecyclerAdapter.kt

Add text display for flow, network, and transport layer security.

* Update MainRecyclerAdapter.kt

remove flow
2026-05-27 17:30:22 +08:00
Tanbir Hossen 7f0e2f801d fix: migrate wsSettings host from deprecated headers.Host to independent host field (#5686)
* fix: migrate wsSettings host from deprecated headers.Host to independent host field

fix: migrate wsSettings host out of deprecated headers field

- Add independent `host` field to WsSettingsBean
- Replace HeadersBean with Map<String, String>? to match Xray docs
- Update CoreOutboundBuilder to use wssetting.host instead of wssetting.headers.Host

* fix: migrate wsSettings host from deprecated headers.Host to independent host field

fix: migrate wsSettings host out of deprecated headers field

- Add independent `host` field to WsSettingsBean
- Replace HeadersBean with Map<String, String>? to match Xray docs
- Update CoreOutboundBuilder to use wssetting.host instead of wssetting.headers.Host
2026-05-25 18:01:21 +08:00
2dust 0f7cee0613 Use isComplexType() to simplify config checks 2026-05-25 10:03:37 +08:00
2dust b2e10fc60b Skip complex config types when deduplicating servers
https://github.com/2dust/v2rayNG/issues/5675
2026-05-25 09:55:30 +08:00
name321467 aa3f17fb51 Add share log action (#5682)
Add a new "Share log" menu item in Logcat.The action creates a temporary .txt file from the current log output and opens the Android share sheet, allowing users to save or send logs to other apps.
2026-05-25 09:46:39 +08:00
2dust d64fc627bb up 2.2.0 2026-05-23 15:36:30 +08:00
2dust 6934ab3e5d Bump SDK to 37
Update compileSdk and targetSdk to 37. Adjust AndroidManifest: remove legacy storage read permissions, add tools:ignore for FOREGROUND_SERVICE policy and keep FOREGROUND_SERVICE_SPECIAL_USE with minSdk 34. Wrap ContextCompat.startForegroundService with SecurityException handling and special-case ForegroundServiceStartNotAllowedException (logged and rethrown as IllegalStateException) to surface permission/OS restrictions. Remove the READ_STORAGE PermissionType and related runtime permission request in HelperBaseActivity (file chooser now launches directly). These changes align permissions and service startup behavior with newer Android SDK requirements.
2026-05-23 14:56:45 +08:00
2dust 028d13aef8 Add SettingsManager.getProfileRemarks and use it 2026-05-23 11:09:46 +08:00
2dust 4a78c165bc Refactor core config, routing and balancer logic
Rework CoreConfigContextBuilder and CoreConfigManager to centralize and simplify outbound resolution and runtime config assembly. Key changes: handle CUSTOM profiles early, introduce ResolvedOutbound and resolvedOutbounds list, resolve routing-target outbounds separately, and unify normal/group/chain build flows into buildUnifiedConfig. Routing, DNS, inbounds, outbounds and balancer logic were modularized (configureInbounds/configureRouting/configureDns/configureFakeDns/configureLocalDns/etc.), balancer strategies and observability are consolidated, and domain pre-resolution/hosts handling preserved. Also added new balancer tag naming (TAG_BALANCER_PRE) and small API/utility adjustments across CoreOutboundBuilder, DTOs and enums to support the new flow. These changes improve clarity of the build pipeline, make custom/routing outbounds handling explicit, and prepare for richer policy-group balancing behavior.
2026-05-23 10:49:20 +08:00
2dust c1033a006f Use safe JSON parsing and add fromJsonSafe 2026-05-21 14:35:34 +08:00
DHR60 e8a42013c0 Remove freedom.domainStrategy (#5656) 2026-05-20 14:10:20 +08:00
vemneyy 94e5263250 Added QUIC sniffig option in destOverride to default config (#5639) 2026-05-20 14:06:56 +08:00
2dust 7c68aa9d83 Parse SOCKS userInfo without unnecessary decoding
https://github.com/2dust/v2rayNG/issues/5657
2026-05-20 13:59:54 +08:00
54 changed files with 942 additions and 714 deletions
+8 -8
View File
@@ -22,25 +22,25 @@ jobs:
fetch-depth: '0'
- name: Setup Android SDK
uses: android-actions/setup-android@v4.0.1
uses: android-actions/setup-android@v4
with:
log-accepted-android-sdk-licenses: false
cmdline-tools-version: '13114758'
packages: 'platforms;android-36.1 build-tools;36.1.0 platform-tools'
cmdline-tools-version: '14742923'
packages: 'platforms;android-37.0 build-tools;37.0.0 platform-tools'
- name: Install NDK
run: |
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
--channel=0 \
--install "ndk;28.2.13676358"
echo "NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> $GITHUB_ENV
--install "ndk;29.0.14206865"
echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> $GITHUB_ENV
sed -i '10i\
\
ndkVersion = "28.2.13676358"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
ndkVersion = "29.0.14206865"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
- name: Restore cached libhevtun
id: cache-libhevtun-restore
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: ${{ github.workspace }}/libs
key: libhevtun-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hev-socks5-tunnel/HEAD') }}-${{ hashFiles('compile-hevtun.sh') }}
@@ -52,7 +52,7 @@ jobs:
- name: Save libhevtun
if: steps.cache-libhevtun-restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
uses: actions/cache/save@v5
with:
path: ${{ github.workspace }}/libs
key: libhevtun-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hev-socks5-tunnel/HEAD') }}-${{ hashFiles('compile-hevtun.sh') }}
+1 -1
View File
@@ -3,7 +3,7 @@
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
[![API](https://img.shields.io/badge/API-24%2B-yellow.svg?style=flat)](https://developer.android.com/about/versions/lollipop)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.3.0-blue.svg)](https://kotlinlang.org)
[![Kotlin Version](https://img.shields.io/badge/Kotlin-2.3.21-blue.svg)](https://kotlinlang.org)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/2dust/v2rayNG)](https://github.com/2dust/v2rayNG/commits/master)
[![CodeFactor](https://www.codefactor.io/repository/github/2dust/v2rayng/badge)](https://www.codefactor.io/repository/github/2dust/v2rayng)
[![GitHub Releases](https://img.shields.io/github/downloads/2dust/v2rayNG/latest/total?logo=github)](https://github.com/2dust/v2rayNG/releases)
+4 -4
View File
@@ -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 = 732
versionName = "2.2.2"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
+2 -3
View File
@@ -30,15 +30,14 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.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"
},
@@ -55,7 +55,6 @@ object AppConfig {
const val PREF_IPV6_ENABLED = "pref_ipv6_enabled"
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
const val PREF_ENABLE_LOCAL_PROXY = "pref_enable_local_proxy"
const val PREF_SOCKS_PORT = "pref_socks_port"
const val PREF_DYNAMIC_SOCKS_PORT = "pref_dynamic_socks_port"
@@ -105,7 +104,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"
@@ -6,6 +6,7 @@ import com.v2ray.ang.dto.CoreConfigContext
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.CoreResolvedType
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isComplexType
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
@@ -13,58 +14,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()
@@ -92,18 +163,15 @@ object CoreConfigContextBuilder {
}
}
.filter { it.server.isNotNullEmpty() }
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
.filter { it.configType != EConfigType.CUSTOM }
.filter { it.configType != EConfigType.POLICYGROUP }
.filter { it.configType != EConfigType.PROXYCHAIN }
.filter { Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
.filter { !it.configType.isComplexType() }
.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)
@@ -114,26 +182,22 @@ object CoreConfigContextBuilder {
.asSequence()
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
.filter { it.server.isNotNullEmpty() }
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
.filter { it.configType != EConfigType.CUSTOM }
.filter { it.configType != EConfigType.POLICYGROUP }
.filter { it.configType != EConfigType.PROXYCHAIN }
.filter { Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
.filter { !it.configType.isComplexType() }
.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 +205,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
@@ -410,7 +410,7 @@ object CoreOutboundBuilder {
NetworkType.WS.type -> {
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
wssetting.headers.Host = host.orEmpty()
wssetting.host = host.orEmpty()
sni = host
wssetting.path = path ?: "/"
streamSettings.wsSettings = wssetting
@@ -545,7 +545,7 @@ object CoreOutboundBuilder {
*/
fun populateTlsSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
val streamSecurity = profileItem.security.orEmpty()
val allowInsecure = profileItem.insecure == true
val allowInsecure = profileItem.insecure == true && profileItem.pinnedCA256.isNullOrEmpty()
val sni = if (profileItem.sni.isNullOrEmpty()) {
when {
sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
@@ -685,4 +685,4 @@ object CoreOutboundBuilder {
}
return resolvedIps.first()
}
}
}
@@ -16,6 +16,7 @@ import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.dto.OutboundTrafficStat
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isComplexType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.NotificationManager
@@ -145,9 +146,7 @@ object CoreServiceManager {
error(context.getString(R.string.toast_config_file_invalid))
}
if (config.configType != EConfigType.CUSTOM
&& config.configType != EConfigType.POLICYGROUP
&& config.configType != EConfigType.PROXYCHAIN
if (!config.configType.isComplexType()
&& !Utils.isValidUrl(config.server)
&& !Utils.isPureIpAddress(config.server.orEmpty())
) {
@@ -176,7 +175,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*/
@@ -199,13 +197,12 @@ data class V2rayConfig(
data class WsSettingsBean(
var path: String? = null,
var headers: HeadersBean = HeadersBean(),
var host: String? = null,
var headers: Map<String, String>? = null,
val maxEarlyData: Int? = null,
val useBrowserForwarding: Boolean? = null,
val acceptProxyProtocol: Boolean? = null
) {
data class HeadersBean(var Host: String = "")
}
)
data class HttpupgradeSettingsBean(
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,
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"
}
}
@@ -8,6 +8,7 @@ import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.v2ray.ang.AngApplication
import com.v2ray.ang.enums.EConfigType
import es.dmoral.toasty.Toasty
import java.io.Serializable
import java.net.URI
@@ -200,4 +201,22 @@ fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean =
}
return regex?.containsMatchIn(this)
?: 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
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
@@ -50,9 +50,8 @@ open class FmtBase {
*
* @param config the ProfileItem object to populate
* @param queryParam the query parameters to use for populating the ProfileItem
* @param allowInsecure whether to allow insecure connections
*/
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>, allowInsecure: Boolean) {
fun getItemFormQuery(config: ProfileItem, queryParam: Map<String, String>) {
config.network = queryParam["type"] ?: NetworkType.TCP.type
config.headerType = queryParam["headerType"]
config.host = queryParam["host"]
@@ -79,7 +78,7 @@ open class FmtBase {
config.insecure = when {
allowInsecureKeys.any { queryParam[it] == "1" } -> true
allowInsecureKeys.any { queryParam[it] == "0" } -> false
else -> allowInsecure
else -> false
}
config.sni = queryParam["sni"]
config.fingerPrint = queryParam["fp"]
@@ -19,7 +19,6 @@ object Hysteria2Fmt : FmtBase() {
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str))
@@ -33,7 +32,7 @@ object Hysteria2Fmt : FmtBase() {
if (!uri.rawQuery.isNullOrEmpty()) {
val queryParam = getQueryParam(uri)
getItemFormQuery(config, queryParam, allowInsecure)
getItemFormQuery(config, queryParam)
config.security = queryParam["security"] ?: AppConfig.TLS
config.obfsPassword = queryParam["obfs-password"]
@@ -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()
@@ -17,7 +17,6 @@ object TrojanFmt : FmtBase() {
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
@@ -29,11 +28,11 @@ object TrojanFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) {
config.network = NetworkType.TCP.type
config.security = AppConfig.TLS
config.insecure = allowInsecure
config.insecure = false
} else {
val queryParam = getQueryParam(uri)
getItemFormQuery(config, queryParam, allowInsecure)
getItemFormQuery(config, queryParam)
config.security = queryParam["security"] ?: AppConfig.TLS
}
@@ -17,7 +17,6 @@ object VlessFmt : FmtBase() {
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parse(str: String): ProfileItem? {
var allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
@@ -30,7 +29,7 @@ object VlessFmt : FmtBase() {
config.password = uri.userInfo
config.method = queryParam["encryption"] ?: "none"
getItemFormQuery(config, queryParam, allowInsecure)
getItemFormQuery(config, queryParam)
return config
}
@@ -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
@@ -26,7 +26,6 @@ object VmessFmt : FmtBase() {
return parseVmessStd(str)
}
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
@@ -87,7 +86,7 @@ object VmessFmt : FmtBase() {
config.insecure = when (vmessQRCode.insecure) {
"1" -> true
"0" -> false
else -> allowInsecure
else -> false
}
return config
}
@@ -154,7 +153,6 @@ object VmessFmt : FmtBase() {
* @return the parsed ProfileItem object, or null if parsing fails
*/
fun parseVmessStd(str: String): ProfileItem? {
val allowInsecure = MmkvManager.decodeSettingsBool(AppConfig.PREF_ALLOW_INSECURE, false)
val config = ProfileItem.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
@@ -167,7 +165,7 @@ object VmessFmt : FmtBase() {
config.password = uri.userInfo
config.method = AppConfig.DEFAULT_SECURITY
getItemFormQuery(config, queryParam, allowInsecure)
getItemFormQuery(config, queryParam)
return config
}
@@ -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
@@ -234,15 +234,8 @@ object AngConfigManager {
if (servers == null) {
return 0
}
// Find the currently selected server that matches the subscription ID
val removedSelected = if (subid.isNotBlank() && !append) {
MmkvManager.getSelectServer()
.takeIf { it?.isNotBlank() == true }
?.let { MmkvManager.decodeServerConfig(it) }
?.takeIf { it.subscriptionId == subid }
} else {
null
}
// Find the currently selected server that belongs to the same subscription before replacement.
val removedSelected = getRemovedSelectedProfile(subid, append)
val subItem = MmkvManager.decodeSubscription(subid)
@@ -288,7 +281,6 @@ object AngConfigManager {
// Read serverList once
val serverList = MmkvManager.decodeServerList(subid)
var needSetSelected = MmkvManager.getSelectServer().isNullOrBlank()
configs.forEach { config ->
val key = Utils.getUuid()
@@ -297,10 +289,6 @@ object AngConfigManager {
if (!serverList.contains(key)) {
serverList.add(0, key)
if (needSetSelected) {
MmkvManager.setSelectServer(key)
needSetSelected = false
}
}
keyToProfile[key] = config
}
@@ -323,7 +311,8 @@ object AngConfigManager {
* @return Matched key or null
*/
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)
if (target.remarks.isNotBlank()) {
@@ -360,7 +349,20 @@ object AngConfigManager {
isSameText(saved.server, target.server)
}?.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()
if (serverList.isNotEmpty()) {
val removedSelected = getRemovedSelectedProfile(subid, append)
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
var count = 0
val keyToProfile = mutableMapOf<String, ProfileItem>()
for (srv in serverList.reversed()) {
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
config.subscriptionId = subid
config.description = generateDescription(config)
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
keyToProfile[key] = config
count += 1
}
if (count > 0) {
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
matchKey?.let { MmkvManager.setSelectServer(it) }
}
return count
}
} catch (e: Exception) {
@@ -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)
@@ -19,6 +19,7 @@ import com.v2ray.ang.databinding.FragmentGroupServerBinding
import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isComplexType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
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) {
val isCustom = profile.configType == EConfigType.CUSTOM
|| profile.configType == EConfigType.POLICYGROUP
|| profile.configType == EConfigType.PROXYCHAIN
val isCustom = profile.configType.isComplexType()
val (shareOptions, skip) = if (more) {
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 = "*/*",
onResult: (Uri?) -> Unit
) {
checkAndRequestPermission(PermissionType.READ_STORAGE) {
fileChooser.launch(mimeType, onResult)
}
fileChooser.launch(mimeType, onResult)
}
/**
@@ -1,6 +1,8 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@@ -18,6 +20,11 @@ import com.v2ray.ang.viewmodel.LogcatViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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 {
private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) }
@@ -45,6 +52,55 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
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 {
menuInflater.inflate(R.menu.menu_logcat, menu)
@@ -78,6 +134,11 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener {
true
}
R.id.share_all -> {
shareLogcat()
true
}
R.id.clear_all -> {
lifecycleScope.launch(Dispatchers.IO) {
viewModel.clearLogcat()
@@ -1,3 +1,4 @@
package com.v2ray.ang.ui
import android.annotation.SuppressLint
@@ -14,6 +15,7 @@ import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.ServersCache
import com.v2ray.ang.extension.isComplexType
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
@@ -58,7 +60,7 @@ class MainRecyclerAdapter(
//Name address
holder.itemMainBinding.tvName.text = profile.remarks
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
holder.itemMainBinding.tvType.text = profile.configType.name
holder.itemMainBinding.tvType.text = getProtocolDescription(profile)
//TestResult
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
@@ -140,6 +142,31 @@ class MainRecyclerAdapter(
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) {
val idx = data.indexOfFirst { it.guid == guid }
if (idx >= 0) {
@@ -203,4 +230,4 @@ class MainRecyclerAdapter(
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.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)
@@ -14,7 +14,6 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.DEFAULT_PORT
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.REALITY
import com.v2ray.ang.AppConfig.TLS
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
@@ -618,7 +617,7 @@ class ServerActivity : BaseActivity() {
val allowInsecure =
if (allowInsecureField == null || allowinsecures[allowInsecureField].isBlank()) {
MmkvManager.decodeSettingsBool(PREF_ALLOW_INSECURE)
false
} else {
allowinsecures[allowInsecureField].toBoolean()
}
@@ -13,6 +13,7 @@ import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ActivityServerProxyChainBinding
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isComplexType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
@@ -55,18 +56,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() {
@@ -114,10 +110,7 @@ class ServerProxyChainActivity : BaseActivity() {
val invalidMembers = chainMembers.filter { member ->
val profile = SettingsManager.getServerViaRemarks(member)
profile == null
|| profile.configType == EConfigType.CUSTOM
|| profile.configType == EConfigType.POLICYGROUP
|| profile.configType == EConfigType.PROXYCHAIN
profile == null || profile.configType.isComplexType()
}
if (invalidMembers.isNotEmpty()) {
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.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,11 @@ 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.isComplexType
import com.v2ray.ang.extension.matchesPattern
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
@@ -296,16 +297,28 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
/**
* Removes duplicate servers.
* Excludes servers with complex types (Custom, PolicyGroup, or ProxyChain) from duplicate comparison.
* @return The number of removed servers.
*/
fun removeDuplicateServer(): Int {
val serversCacheCopy = serversCache.toList().toMutableList()
val deleteServer = mutableListOf<String>()
serversCacheCopy.forEachIndexed { index, sc ->
val profile = sc.profile
// Skip if this profile has a complex config type
if (profile.configType.isComplexType()) {
return@forEachIndexed
}
serversCacheCopy.forEachIndexed { index2, sc2 ->
if (index2 > index) {
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)) {
deleteServer.add(sc2.guid)
}
@@ -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
@@ -17,4 +17,9 @@
android:icon="@drawable/ic_copy"
android:title="@string/logcat_copy"
app:showAsAction="ifRoom" />
<item
android:id="@+id/share_all"
android:icon="@drawable/ic_share_24dp"
android:title="@string/logcat_share"
app:showAsAction="ifRoom" />
</menu>
@@ -220,9 +220,6 @@
<string name="summary_pref_proxy_sharing_enabled">يمكن للأجهزة الأخرى الاتصال بالبروكسي بواسطة عنوان IP الخاص بك من خلال socks/http، يتم التمكين فقط في الشبكة الموثوقة لتجنب الاتصال غير المصرح به</string>
<string name="toast_warning_pref_proxysharing_short">السماح بالاتصالات من الشبكة المحلية، تأكد من أنك في شبكة موثوقة</string>
<string name="title_pref_allow_insecure">السماح غير الآمن</string>
<string name="summary_pref_allow_insecure">عند TLS، الافتراضي هو السماح غير الآمن</string>
<string name="title_pref_socks_port">منفذ بروكسي Local</string>
<string name="title_pref_enable_local_proxy">تفعيل الوكيل المحلي</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
@@ -286,6 +283,7 @@
<string name="title_logcat">Logcat</string>
<string name="logcat_copy">نسخ</string>
<string name="logcat_clear">مسح</string>
<string name="logcat_share">مشاركة السجل</string>
<string name="title_service_restart">إعادة تشغيل الخدمة</string>
<string name="title_del_all_config">حذف جميع الإعدادات (قبل أول خطوة)</string>
<string name="title_del_duplicate_config">حذف الإعدادات المكررة (2)</string>
@@ -220,9 +220,6 @@
<string name="summary_pref_proxy_sharing_enabled">অন্যান্য ডিভাইসগুলি আপনার আইপি ঠিকানা ব্যবহার করে প্রক্সিতে সংযুক্ত হতে পারে, শুধুমাত্র বিশ্বস্ত নেটওয়ার্কে সক্রিয় করুন যাতে অনুমোদিত সংযোগ এড়ানো যায়</string>
<string name="toast_warning_pref_proxysharing_short">LAN থেকে সংযোগ অনুমোদন করুন, নিশ্চিত করুন যে আপনি একটি বিশ্বস্ত নেটওয়ার্কে আছেন</string>
<string name="title_pref_allow_insecure">allowInsecure</string>
<string name="summary_pref_allow_insecure">যখন TLS, ডিফল্টভাবে allowInsecure</string>
<string name="title_pref_socks_port">Local প্রক্সি পোর্ট</string>
<string name="title_pref_enable_local_proxy">লোকাল প্রক্সি সক্ষম করুন</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
@@ -286,6 +283,7 @@
<string name="title_logcat">লগক্যাট</string>
<string name="logcat_copy">কপি করুন</string>
<string name="logcat_clear">স্পষ্ট করুন</string>
<string name="logcat_share">লগ শেয়ার করুন</string>
<string name="title_service_restart">সার্ভিস পুনরায় চালু করুন</string>
<string name="title_del_all_config">সব কনফিগারেশন মুছে ফেলুন</string>
<string name="title_del_duplicate_config">ডুপ্লিকেট কনফিগারেশন মুছে ফেলুন</string>
@@ -153,7 +153,7 @@
<string name="title_core_settings">سامووا هسته</string>
<string name="title_vpn_settings">سامووا VPN</string>
<string name="title_pref_per_app_proxy">پروکسی و ری برنومه</string>
<string name="summary_pref_per_app_proxy">پوی وولاتی: برنومه پسند وابیڌه وا ی پروکسی منپیز ابۊ، برنومه پسند نوابیڌه موستقیمن منپیز ابۊ. \nحالت دور زیڌن: برنومه پسند وابیڌه موستقیمن منپیز ابۊ، برنومه پسند نوابیڌه وا ی پروکسی منپیز ابۊ. \nپسند خوتکار برنومه یل پروکسی من نومگه امکووݩ پزیر هڌ.</string>
<string name="summary_pref_per_app_proxy">پوی وولاتی: برنومه پسند وابیڌه وا ی پروکسی منپیز ابۊ، برنومه پسند نوابیڌه موستقیمن منپیز ابۊ.\nهالت دور زیڌن: برنومه پسند وابیڌه موستقیمن منپیز ابۊ، برنومه پسند نوابیڌه وا ی پروکسی منپیز ابۊ.\nپسند خوتکار برنومه یل پروکسی من نومگه امکووݩ پزیر هڌ.</string>
<string name="title_pref_is_booted">منپیز خوتکار مجال ره ونی</string>
<string name="summary_pref_is_booted">مجال ره وندن، خوساخوس و سرور پسند بیڌه منپیز ابۊ که گاشڌ نا مووفق بۊ.</string>
@@ -175,7 +175,7 @@
</string-array>
<string name="title_pref_speed_enabled">ره وندن نشووݩ داڌن سورعت</string>
<string name="summary_pref_speed_enabled">نشووݩ داڌن سورعت هیم سکویی من وارسۊویا. نماڌ وارسۊوی و ری و کار گرؽڌن کانفیگ آلشت ابۊ.</string>
<string name="summary_pref_speed_enabled">نشووݩ داڌن سورعت هیم سکویی من وارسۊویا.\nنماڌ وارسۊوی و ری و کار گرؽڌن کانفیگ آلشت ابۊ.</string>
<string name="title_pref_sniffing_enabled">ره وندن تجزیه وو تئلیل کتنا (Sniffing)</string>
<string name="summary_pref_sniffing_enabled">و کار گرؽڌن تشخیس نوم دامنه (Sniff) من کتنا (پؽش فرز رۊشن)</string>
@@ -186,10 +186,10 @@
<string name="summary_pref_local_dns_enabled">درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین ره ونده بۊ)</string>
<string name="title_pref_fake_dns_enabled">ره وندن DNS جئلی</string>
<string name="summary_pref_fake_dns_enabled">DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من ی قرده ز برنومه یل کار نکونه)</string>
<string name="summary_pref_fake_dns_enabled">دی ان اس مهلی نشۊویا آی پی جئلی ن اوورگنه (زل تر، ٱما گاشڌ من ی قرده ز برنومه یل کار نکونه)</string>
<string name="title_pref_ipv6_enabled">ره وندن IPv6</string>
<string name="summary_pref_ipv6_enabled">نشۊویا IPv6 وو تورا IPv6 ن ز رابت VPN ره ونین</string>
<string name="summary_pref_ipv6_enabled">نشۊویا وو تورا IPv6 ن و رابت VPN ازاف کۊنین</string>
<string name="title_pref_prefer_ipv6">ترجی داڌن IPv6</string>
<string name="summary_pref_prefer_ipv6">مجال هل وو فسل نوم دامنه، نشۊویا IPv6 ن ترجی بڌین</string>
@@ -220,9 +220,6 @@
<string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا نشۊوی IP ایسا، ز ره socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل ائتماد ره ونده بۊ تا ز منپیز قیر موجاز جلو گری بۊ.</string>
<string name="toast_warning_pref_proxysharing_short">منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین.</string>
<string name="title_pref_allow_insecure">اجازه نا ٱمن</string>
<string name="summary_pref_allow_insecure">مجال و کار بوردن TLS ب تۉر پؽش فرز، موجوز نا ٱمن ره ونده هڌ.</string>
<string name="title_pref_socks_port">پورت پروکسی مهلی</string>
<string name="title_pref_enable_local_proxy">ره وندن پروکسی مهلی</string>
<string name="summary_pref_enable_local_proxy">هرسا ره نوسته بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو ورۊ رسۊوی یل اشتراک وو کارا جۊر یو نترن پروکسی ن و کار گرن.</string>
@@ -240,7 +237,7 @@
<string name="summary_pref_local_dns_port">پورت DNS مهلی</string>
<string name="title_pref_confirm_remove">قوۊل کردن پاک کردن کانفیگ</string>
<string name="summary_pref_confirm_remove">سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ.</string>
<string name="summary_pref_confirm_remove">سی پاک وابیڌن فایل کانفیگ، لنگ قوۊل کردن دووارته ز سمت منتور هڌ.</string>
<string name="title_pref_start_scan_immediate">زی اسکنن ره ون</string>
<string name="summary_pref_start_scan_immediate">شؽواتگرن سی اسکن، زی مجال ره وندن بۊگۊشین، ٱندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین.</string>
@@ -249,7 +246,7 @@
<string name="summary_pref_append_http_proxy">پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.</string>
<string name="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</string>
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینوو ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینوو ترین موئتوا بیشتری ن سیل کۊنین.\nسی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن ره ونین</string>
<string name="summary_pref_group_all_display">ی بلگه «پوی بونکۊ یل کانفیگ» ازاف کۊنین</string>
@@ -286,6 +283,7 @@
<string name="title_logcat">گوزارشا</string>
<string name="logcat_copy">لف گیری</string>
<string name="logcat_clear">روفتن</string>
<string name="logcat_share">یک رسۊوی گوزارش</string>
<string name="title_service_restart">ره وندن دووارته خدمات</string>
<string name="title_del_all_config">پاک کردن پوی کانفیگا بونکۊ سکویی</string>
<string name="title_del_duplicate_config">پاک کردن کانفیگا تکراری بونکۊ سکویی</string>
@@ -218,9 +218,6 @@
<string name="summary_pref_proxy_sharing_enabled">سایر دستگاه ها می توانند با استفاده از آدرس آیپی شما برای استفاده از یک پروکسی محلی متصل شوند. فقط در یک شبکه قابل اعتماد برای جلوگیری از اتصالات غیرمجاز استفاده کنید.</string>
<string name="toast_warning_pref_proxysharing_short">اتصالات از طریق شبکه محلی را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید.</string>
<string name="title_pref_allow_insecure">اعطای مجوز ناامن</string>
<string name="summary_pref_allow_insecure">هنگام استفاده از TLS، به طور پیش‌ فرض مجوز ناامن فعال است.</string>
<string name="title_pref_socks_port">پورت پروکسی محلی</string>
<string name="title_pref_enable_local_proxy">فعال‌سازی پروکسی محلی</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
@@ -283,6 +280,7 @@
<string name="title_logcat">گزارشات</string>
<string name="logcat_copy">کپی</string>
<string name="logcat_clear">پاک کردن</string>
<string name="logcat_share">اشتراک‌گذاری گزارش</string>
<string name="title_service_restart">راه‌اندازی مجدد خدمات</string>
<string name="title_del_all_config">حذف تمام کانفیگ های گروه فعلی</string>
<string name="title_del_duplicate_config">حذف کانفیگ های تکراری گروه فعلی</string>
@@ -224,9 +224,6 @@
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
<string name="title_pref_allow_insecure">Разрешать небезопасные соединения</string>
<string name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string>
<string name="title_pref_socks_port">Порт локального прокси</string>
<string name="title_pref_enable_local_proxy">Использовать локальный прокси</string>
<string name="summary_pref_enable_local_proxy">Если отключено, SOCKS-прокси недоступен, поэтому обновления подписки и подобные действия не смогут использовать прокси</string>
@@ -291,6 +288,7 @@
<string name="title_logcat">Журнал</string>
<string name="logcat_copy">Копировать</string>
<string name="logcat_clear">Очистить</string>
<string name="logcat_share">Поделиться логом</string>
<string name="title_service_restart">Перезапуск службы</string>
<string name="title_del_all_config">Удалить профили</string>
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string>
@@ -220,9 +220,6 @@
<string name="summary_pref_proxy_sharing_enabled">Các thiết bị khác trong cùng mạng LAN có thể kết nối đến SOCKS / HTTP proxy trên thiết bị của bạn. \nChỉ bật tính năng này trong các mạng đáng tin cậy để tránh kết nối trái phép.</string>
<string name="toast_warning_pref_proxysharing_short">Đang bật cho phép kết nối từ mạng LAN</string>
<string name="title_pref_allow_insecure">Bỏ qua xác minh chứng chỉ</string>
<string name="summary_pref_allow_insecure">Khi nhập những cấu hình có bảo mật TLS, mặc định sẽ không xác minh chứng chỉ.</string>
<string name="title_pref_socks_port">Cổng Proxy Local</string>
<string name="title_pref_enable_local_proxy">Bật proxy cục bộ</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
@@ -286,6 +283,7 @@
<string name="title_logcat">Logcat</string>
<string name="logcat_copy">Sao chép</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_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>
@@ -219,10 +219,7 @@
<string name="summary_pref_proxy_sharing_enabled">其他设备可以使用 socks/http 协议通过您的 IP 地址连接到代理, 仅在受信任的网络中启用以避免未经授权的连接</string>
<string name="toast_warning_pref_proxysharing_short">允许来自局域网的连接,请确保处于受信网络</string>
<string name="title_pref_allow_insecure">跳过证书验证 (allowInsecure)</string>
<string name="summary_pref_allow_insecure">传输层安全选 tls 时,默认跳过证书验证 (allowInsecure)</string>
<string name="title_pref_socks_port">本地代理端口</string>
<string name="title_pref_socks_port">本地代理端口</string>
<string name="title_pref_enable_local_proxy">启用本地代理</string>
<string name="summary_pref_enable_local_proxy">不开启时没有 socks 代理,无法通过代理更新订阅等</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
@@ -286,6 +283,7 @@
<string name="title_logcat">Logcat</string>
<string name="logcat_copy">复制</string>
<string name="logcat_clear">清除</string>
<string name="logcat_share">分享日志</string>
<string name="title_service_restart">服务重启</string>
<string name="title_del_all_config">删除配置</string>
<string name="title_del_duplicate_config">删除重复配置</string>
@@ -218,9 +218,6 @@
<string name="summary_pref_proxy_sharing_enabled">其他裝置可以使用 socks/http 協定透過您的 IP 位址連線到 Proxy,僅在受信任的網路中啟用以避免未經授權的連線</string>
<string name="toast_warning_pref_proxysharing_short">允許來自區域網路的連線,請確保處於受信網路</string>
<string name="title_pref_allow_insecure">跳過憑證驗證 (allowInsecure)</string>
<string name="summary_pref_allow_insecure">傳輸層安全選 tls 時,預設跳過憑證驗證 (allowInsecure)</string>
<string name="title_pref_socks_port">本地代理埠</string>
<string name="title_pref_enable_local_proxy">啟用本地代理</string>
<string name="summary_pref_enable_local_proxy">未開啟時沒有 socks 代理,無法透過代理更新訂閱等</string>
@@ -285,6 +282,7 @@
<string name="title_logcat">Logcat</string>
<string name="logcat_copy">複製</string>
<string name="logcat_clear">清除</string>
<string name="logcat_share">分享日誌</string>
<string name="title_service_restart">重啟服務</string>
<string name="title_del_all_config">刪除設定</string>
<string name="title_del_duplicate_config">刪除重複設定</string>
+1 -3
View File
@@ -226,9 +226,6 @@
<string name="summary_pref_proxy_sharing_enabled">Other devices can connect to proxy by your IP address through local proxy. Only enable in trusted networks to avoid unauthorized connections</string>
<string name="toast_warning_pref_proxysharing_short">Allow connections from the LAN. Make sure you are in a trusted network</string>
<string name="title_pref_allow_insecure">allowInsecure</string>
<string name="summary_pref_allow_insecure">When TLS is selected, allow insecure connections by default</string>
<string name="title_pref_socks_port">Local proxy port</string>
<string name="title_pref_enable_local_proxy">Enable local proxy</string>
<string name="summary_pref_enable_local_proxy">When disabled, no SOCKS proxy is available, so subscription updates and similar actions cannot use the proxy</string>
@@ -293,6 +290,7 @@
<string name="title_logcat">Logcat</string>
<string name="logcat_copy">Copy</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_del_all_config">Delete config</string>
<string name="title_del_duplicate_config">Delete duplicate config</string>
@@ -144,11 +144,6 @@
android:summary="@string/summary_pref_route_only_enabled"
android:title="@string/title_pref_route_only_enabled" />
<CheckBoxPreference
android:defaultValue="false"
android:key="pref_allow_insecure"
android:title="@string/title_pref_allow_insecure" />
<CheckBoxPreference
android:defaultValue="true"
android:key="pref_enable_local_proxy"
+1 -1
View File
@@ -2,7 +2,7 @@
agp = "9.2.1"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.3.10"
kotlin = "2.3.21"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
#Thu Nov 14 12:42:51 BDT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists