diff --git a/AndroidLibXrayLite b/AndroidLibXrayLite
index 2715a3b1..8ad3e1dd 160000
--- a/AndroidLibXrayLite
+++ b/AndroidLibXrayLite
@@ -1 +1 @@
-Subproject commit 2715a3b110f64e039a4b8d2c8ca0cb5a9a6b0958
+Subproject commit 8ad3e1ddf165d8d67e488346b2faa9153d3e33a4
diff --git a/README.md b/README.md
index 83214e02..4bd6f8ec 100644
--- a/README.md
+++ b/README.md
@@ -3,16 +3,12 @@
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
[](https://developer.android.com/about/versions/lollipop)
-[](https://kotlinlang.org)
+[](https://kotlinlang.org)
[](https://github.com/2dust/v2rayNG/commits/master)
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
[](https://github.com/2dust/v2rayNG/releases)
[](https://t.me/v2rayn)
-
-
-
-
### Telegram Channel
[github_2dust](https://t.me/github_2dust)
diff --git a/V2rayNG/app/build.gradle.kts b/V2rayNG/app/build.gradle.kts
index c64f0dcb..1624786c 100644
--- a/V2rayNG/app/build.gradle.kts
+++ b/V2rayNG/app/build.gradle.kts
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 35
- versionCode = 645
- versionName = "1.9.45"
+ versionCode = 658
+ versionName = "1.10.8"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml
index 4ff08af9..00e4b747 100644
--- a/V2rayNG/app/src/main/AndroidManifest.xml
+++ b/V2rayNG/app/src/main/AndroidManifest.xml
@@ -144,6 +144,9 @@
+
diff --git a/V2rayNG/app/src/main/assets/v2ray_config.json b/V2rayNG/app/src/main/assets/v2ray_config.json
index 90abcee0..4f8c3d7e 100644
--- a/V2rayNG/app/src/main/assets/v2ray_config.json
+++ b/V2rayNG/app/src/main/assets/v2ray_config.json
@@ -97,7 +97,7 @@
}
],
"routing": {
- "domainStrategy": "IPIfNonMatch",
+ "domainStrategy": "AsIs",
"rules": []
},
"dns": {
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt
index 16e02f16..44f680b3 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AngApplication.kt
@@ -39,5 +39,9 @@ class AngApplication : MultiDexApplication() {
WorkManager.initialize(this, workManagerConfiguration)
SettingsManager.initRoutingRulesets(this)
+
+ es.dmoral.toasty.Toasty.Config.getInstance()
+ .setGravity(android.view.Gravity.BOTTOM, 0, 200)
+ .apply()
}
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt
index 7613c318..09e3a9d5 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/AppConfig.kt
@@ -26,6 +26,7 @@ object AppConfig {
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
const val PREF_VPN_DNS = "pref_vpn_dns"
const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
+ const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index"
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
const val PREF_MUX_ENABLED = "pref_mux_enabled"
@@ -55,15 +56,18 @@ object AppConfig {
const val PREF_DNS_HOSTS = "pref_dns_hosts"
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
const val PREF_LOGLEVEL = "pref_core_loglevel"
+ const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method"
const val PREF_MODE = "pref_mode"
const val PREF_IS_BOOTED = "pref_is_booted"
+ const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release"
+ const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources"
/** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
/** Protocol identifiers. */
- const val PROTOCOL_FREEDOM: String = "freedom"
+ const val PROTOCOL_FREEDOM = "freedom"
/** Broadcast actions. */
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
@@ -88,19 +92,20 @@ object AppConfig {
const val DOWNLINK = "downlink"
/** URLs for various resources. */
- const val androidpackagenamelistUrl =
- "https://raw.githubusercontent.com/2dust/androidpackagenamelist/master/proxy.txt"
- const val v2rayCustomRoutingListUrl =
- "https://raw.githubusercontent.com/2dust/v2rayCustomRoutingList/master/"
- const val v2rayNGUrl = "https://github.com/2dust/v2rayNG"
- const val v2rayNGIssues = "$v2rayNGUrl/issues"
- const val v2rayNGWikiMode = "$v2rayNGUrl/wiki/Mode"
- const val v2rayNGPrivacyPolicy = "https://raw.githubusercontent.com/2dust/v2rayNG/master/CR.md"
- const val PromotionUrl = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
- const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
- const val TgChannelUrl = "https://t.me/github_2dust"
- const val DelayTestUrl = "https://www.gstatic.com/generate_204"
- const val DelayTestUrl2 = "https://www.google.com/generate_204"
+ const val GITHUB_URL = "https://github.com"
+ const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
+ const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
+ const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
+ const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
+ const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
+ const val APP_ISSUES_URL = "$APP_URL/issues"
+ const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
+ const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
+ const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
+ const val TG_CHANNEL_URL = "https://t.me/github_2dust"
+ const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
+ const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
+ const val IP_API_URL = "https://speed.cloudflare.com/meta"
/** DNS server addresses. */
const val DNS_PROXY = "1.1.1.1"
@@ -165,19 +170,13 @@ object AppConfig {
// Android Private DNS constants
const val DNS_DNSPOD_DOMAIN = "dot.pub"
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
- const val DNS_CLOUDFLARE_DOMAIN = "one.one.one.one"
+ const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
+ const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
+ const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
const val DNS_GOOGLE_DOMAIN = "dns.google"
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
-
- val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
- val DNS_CLOUDFLARE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
- val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
- val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
- val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
- val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
-
const val DEFAULT_PORT = 443
const val DEFAULT_SECURITY = "auto"
const val DEFAULT_LEVEL = 8
@@ -186,4 +185,64 @@ object AppConfig {
const val REALITY = "reality"
const val HEADER_TYPE_HTTP = "http"
+ val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
+ val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
+ val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
+ val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9")
+ val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
+ val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
+ val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
+ val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
+
+ //minimum list https://serverfault.com/a/304791
+ val ROUTED_IP_LIST = arrayListOf(
+ "0.0.0.0/5",
+ "8.0.0.0/7",
+ "11.0.0.0/8",
+ "12.0.0.0/6",
+ "16.0.0.0/4",
+ "32.0.0.0/3",
+ "64.0.0.0/2",
+ "128.0.0.0/3",
+ "160.0.0.0/5",
+ "168.0.0.0/6",
+ "172.0.0.0/12",
+ "172.32.0.0/11",
+ "172.64.0.0/10",
+ "172.128.0.0/9",
+ "173.0.0.0/8",
+ "174.0.0.0/7",
+ "176.0.0.0/4",
+ "192.0.0.0/9",
+ "192.128.0.0/11",
+ "192.160.0.0/13",
+ "192.169.0.0/16",
+ "192.170.0.0/15",
+ "192.172.0.0/14",
+ "192.176.0.0/12",
+ "192.192.0.0/10",
+ "193.0.0.0/8",
+ "194.0.0.0/7",
+ "196.0.0.0/6",
+ "200.0.0.0/5",
+ "208.0.0.0/4",
+ "240.0.0.0/4"
+ )
+
+ val PRIVATE_IP_LIST = arrayListOf(
+ "0.0.0.0/8",
+ "10.0.0.0/8",
+ "127.0.0.0/8",
+ "172.16.0.0/12",
+ "192.168.0.0/16",
+ "169.254.0.0/16",
+ "224.0.0.0/4"
+ )
+
+ val GEO_FILES_SOURCES = arrayListOf(
+ "Loyalsoldier/v2ray-rules-dat",
+ "runetfreedom/russia-v2ray-rules-dat",
+ "Chocolate4U/Iran-v2ray-rules"
+ )
+
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt
index d0703f09..5a8d1e60 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/AssetUrlItem.kt
@@ -4,5 +4,6 @@ data class AssetUrlItem(
var remarks: String = "",
var url: String = "",
val addedTime: Long = System.currentTimeMillis(),
- var lastUpdated: Long = -1
+ var lastUpdated: Long = -1,
+ var locked: Boolean? = false,
)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt
new file mode 100644
index 00000000..be4f62e5
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/CheckUpdateResult.kt
@@ -0,0 +1,10 @@
+package com.v2ray.ang.dto
+
+data class CheckUpdateResult(
+ val hasUpdate: Boolean,
+ val latestVersion: String? = null,
+ val releaseNotes: String? = null,
+ val downloadUrl: String? = null,
+ val error: String? = null,
+ val isPreRelease: Boolean = false
+)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt
index c0b70c6a..c8870248 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ConfigResult.kt
@@ -4,6 +4,6 @@ data class ConfigResult(
var status: Boolean,
var guid: String? = null,
var content: String = "",
- var domainPort: String? = null,
+ var socksPort: Int? = null,
)
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
new file mode 100644
index 00000000..0a7dce56
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/GitHubRelease.kt
@@ -0,0 +1,23 @@
+package com.v2ray.ang.dto
+
+import com.google.gson.annotations.SerializedName
+
+data class GitHubRelease(
+ @SerializedName("tag_name")
+ val tagName: String,
+ @SerializedName("body")
+ val body: String,
+ @SerializedName("assets")
+ val assets: List,
+ @SerializedName("prerelease")
+ val prerelease: Boolean = false,
+ @SerializedName("published_at")
+ val publishedAt: String = ""
+) {
+ data class Asset(
+ @SerializedName("name")
+ val name: String,
+ @SerializedName("browser_download_url")
+ val browserDownloadUrl: String
+ )
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
new file mode 100644
index 00000000..97814fbb
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/IPAPIInfo.kt
@@ -0,0 +1,12 @@
+package com.v2ray.ang.dto
+
+data class IPAPIInfo(
+ var ip: String? = null,
+ var clientIp: String? = null,
+ var ip_addr: String? = null,
+ var query: String? = null,
+ var country: String? = null,
+ var country_name: String? = null,
+ var country_code: String? = null,
+ var countryCode: String? = null
+)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt
deleted file mode 100644
index 12995abd..00000000
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/ProfileLiteItem.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.v2ray.ang.dto
-
-data class ProfileLiteItem(
- val configType: EConfigType,
- var subscriptionId: String = "",
- var remarks: String = "",
- var server: String?,
- var serverPort: Int?,
-)
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt
index 8e8c66a4..8957df78 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/SubscriptionItem.kt
@@ -11,5 +11,6 @@ data class SubscriptionItem(
var prevProfile: String? = null,
var nextProfile: String? = null,
var filter: String? = null,
+ var allowInsecureUrl: Boolean = false,
)
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt
index 692caf69..155be104 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/V2rayConfig.kt
@@ -1,25 +1,14 @@
package com.v2ray.ang.dto
-import android.text.TextUtils
-import com.google.gson.GsonBuilder
-import com.google.gson.JsonPrimitive
-import com.google.gson.JsonSerializationContext
-import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName
-import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.ServersBean
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.VnextBean.UsersBean
-import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean.WireGuardBean
import com.v2ray.ang.util.Utils
-import java.lang.reflect.Type
data class V2rayConfig(
var remarks: String? = null,
var stats: Any? = null,
val log: LogBean,
- var policy: PolicyBean?,
+ var policy: PolicyBean? = null,
val inbounds: ArrayList,
var outbounds: ArrayList,
var dns: DnsBean? = null,
@@ -34,9 +23,9 @@ data class V2rayConfig(
) {
data class LogBean(
- val access: String,
- val error: String,
- var loglevel: String?,
+ val access: String? = null,
+ val error: String? = null,
+ var loglevel: String? = null,
val dnsLog: Boolean? = null
)
@@ -46,7 +35,7 @@ data class V2rayConfig(
var protocol: String,
var listen: String? = null,
val settings: Any? = null,
- val sniffing: SniffingBean?,
+ val sniffing: SniffingBean? = null,
val streamSettings: Any? = null,
val allocate: Any? = null
) {
@@ -77,50 +66,6 @@ data class V2rayConfig(
val sendThrough: String? = null,
var mux: MuxBean? = MuxBean(false)
) {
- companion object {
- fun create(configType: EConfigType): OutboundBean? {
- return when (configType) {
- EConfigType.VMESS,
- EConfigType.VLESS ->
- return OutboundBean(
- protocol = configType.name.lowercase(),
- settings = OutSettingsBean(
- vnext = listOf(
- VnextBean(
- users = listOf(UsersBean())
- )
- )
- ),
- streamSettings = StreamSettingsBean()
- )
-
- EConfigType.SHADOWSOCKS,
- EConfigType.SOCKS,
- EConfigType.HTTP,
- EConfigType.TROJAN,
- EConfigType.HYSTERIA2 ->
- return OutboundBean(
- protocol = configType.name.lowercase(),
- settings = OutSettingsBean(
- servers = listOf(ServersBean())
- ),
- streamSettings = StreamSettingsBean()
- )
-
- EConfigType.WIREGUARD ->
- return OutboundBean(
- protocol = configType.name.lowercase(),
- settings = OutSettingsBean(
- secretKey = "",
- peers = listOf(WireGuardBean())
- )
- )
-
- EConfigType.CUSTOM -> null
- }
- }
- }
-
data class OutSettingsBean(
var vnext: List? = null,
var fragment: FragmentBean? = null,
@@ -197,7 +142,7 @@ data class V2rayConfig(
data class WireGuardBean(
var publicKey: String = "",
- var preSharedKey: String = "",
+ var preSharedKey: String? = null,
var endpoint: String = ""
)
}
@@ -299,7 +244,8 @@ data class V2rayConfig(
var tcpFastOpen: Boolean? = null,
var tproxy: String? = null,
var mark: Int? = null,
- var dialerProxy: String? = null
+ var dialerProxy: String? = null,
+ var domainStrategy: String? = null
)
data class TlsSettingsBean(
@@ -349,139 +295,6 @@ data class V2rayConfig(
)
}
- fun populateTransportSettings(
- transport: String,
- headerType: String?,
- host: String?,
- path: String?,
- seed: String?,
- quicSecurity: String?,
- key: String?,
- mode: String?,
- serviceName: String?,
- authority: String?
- ): String? {
- var sni: String? = null
- network = if (transport.isEmpty()) NetworkType.TCP.type else transport
- when (network) {
- NetworkType.TCP.type -> {
- val tcpSetting = TcpSettingsBean()
- if (headerType == AppConfig.HEADER_TYPE_HTTP) {
- tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
- if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
- val requestObj = TcpSettingsBean.HeaderBean.RequestBean()
- requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
- requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
- tcpSetting.header.request = requestObj
- sni = requestObj.headers.Host?.getOrNull(0)
- }
- } else {
- tcpSetting.header.type = "none"
- sni = host
- }
- tcpSettings = tcpSetting
- }
-
- NetworkType.KCP.type -> {
- val kcpsetting = KcpSettingsBean()
- kcpsetting.header.type = headerType ?: "none"
- if (seed.isNullOrEmpty()) {
- kcpsetting.seed = null
- } else {
- kcpsetting.seed = seed
- }
- if (host.isNullOrEmpty()) {
- kcpsetting.header.domain = null
- } else {
- kcpsetting.header.domain = host
- }
- kcpSettings = kcpsetting
- }
-
- NetworkType.WS.type -> {
- val wssetting = WsSettingsBean()
- wssetting.headers.Host = host.orEmpty()
- sni = host
- wssetting.path = path ?: "/"
- wsSettings = wssetting
- }
-
- NetworkType.HTTP_UPGRADE.type -> {
- val httpupgradeSetting = HttpupgradeSettingsBean()
- httpupgradeSetting.host = host.orEmpty()
- sni = host
- httpupgradeSetting.path = path ?: "/"
- httpupgradeSettings = httpupgradeSetting
- }
-
- NetworkType.XHTTP.type -> {
- val xhttpSetting = XhttpSettingsBean()
- xhttpSetting.host = host.orEmpty()
- sni = host
- xhttpSetting.path = path ?: "/"
- xhttpSettings = xhttpSetting
- }
-
- NetworkType.H2.type, NetworkType.HTTP.type -> {
- network = NetworkType.H2.type
- val h2Setting = HttpSettingsBean()
- h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
- sni = h2Setting.host.getOrNull(0)
- h2Setting.path = path ?: "/"
- httpSettings = h2Setting
- }
-
-// "quic" -> {
-// val quicsetting = QuicSettingBean()
-// quicsetting.security = quicSecurity ?: "none"
-// quicsetting.key = key.orEmpty()
-// quicsetting.header.type = headerType ?: "none"
-// quicSettings = quicsetting
-// }
-
- NetworkType.GRPC.type -> {
- val grpcSetting = GrpcSettingsBean()
- grpcSetting.multiMode = mode == "multi"
- grpcSetting.serviceName = serviceName.orEmpty()
- grpcSetting.authority = authority.orEmpty()
- grpcSetting.idle_timeout = 60
- grpcSetting.health_check_timeout = 20
- sni = authority
- grpcSettings = grpcSetting
- }
- }
- return sni
- }
-
- fun populateTlsSettings(
- streamSecurity: String,
- allowInsecure: Boolean,
- sni: String?,
- fingerprint: String?,
- alpns: String?,
- publicKey: String?,
- shortId: String?,
- spiderX: String?
- ) {
- security = if (streamSecurity.isEmpty()) null else streamSecurity
- if (security == null) return
- val tlsSetting = TlsSettingsBean(
- allowInsecure = allowInsecure,
- serverName = if (sni.isNullOrEmpty()) null else sni,
- fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
- alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
- publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
- shortId = if (shortId.isNullOrEmpty()) null else shortId,
- spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
- )
- if (security == AppConfig.TLS) {
- tlsSettings = tlsSetting
- realitySettings = null
- } else if (security == AppConfig.REALITY) {
- tlsSettings = null
- realitySettings = tlsSetting
- }
- }
}
data class MuxBean(
@@ -647,6 +460,18 @@ data class V2rayConfig(
}
return null
}
+
+ fun ensureSockopt(): V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean {
+ val stream = streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean().also {
+ streamSettings = it
+ }
+
+ val sockopt = stream.sockopt ?: V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean().also {
+ stream.sockopt = it
+ }
+
+ return sockopt
+ }
}
data class DnsBean(
@@ -723,15 +548,9 @@ data class V2rayConfig(
return null
}
- fun toPrettyPrinting(): String {
- return GsonBuilder()
- .setPrettyPrinting()
- .disableHtmlEscaping()
- .registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
- object : TypeToken() {}.type,
- JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? -> JsonPrimitive(src?.toInt()) }
- )
- .create()
- .toJson(this)
+ fun getAllProxyOutbound(): List {
+ return outbounds.filter { outbound ->
+ EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt
new file mode 100644
index 00000000..6b7bc379
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/VpnInterfaceAddressConfig.kt
@@ -0,0 +1,39 @@
+package com.v2ray.ang.dto
+
+/**
+ * VPN interface address configuration enum class
+ * Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration.
+ * Each option provides client and router addresses to establish point-to-point VPN tunnels.
+ */
+enum class VpnInterfaceAddressConfig(
+ val displayName: String,
+ val ipv4Client: String,
+ val ipv4Router: String,
+ val ipv6Client: String,
+ val ipv6Router: String
+) {
+ OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"),
+ OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"),
+ OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"),
+ OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"),
+ OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"),
+ OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"),
+ OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2");
+
+ companion object {
+ /**
+ * Retrieves the VPN interface address configuration based on the specified index.
+ *
+ * @param index The configuration index (0-based) corresponding to user selection
+ * @return The VpnInterfaceAddressConfig instance at the specified index,
+ * or OPTION_1 (default) if the index is out of bounds
+ */
+ fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig {
+ return if (index in values().indices) {
+ values()[index]
+ } else {
+ OPTION_1 // Default to the first configuration
+ }
+ }
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt
index 3064535f..6e5c6bb4 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt
@@ -196,4 +196,17 @@ inline fun Intent.serializable(key: String): T? = whe
*
* @return True if the CharSequence is not null and not empty, false otherwise.
*/
-fun CharSequence?.isNotNullEmpty(): Boolean = (this != null && this.isNotEmpty())
\ No newline at end of file
+fun CharSequence?.isNotNullEmpty(): Boolean = this != null && this.isNotEmpty()
+
+fun String.concatUrl(vararg paths: String): String {
+ val builder = StringBuilder(this.trimEnd('/'))
+
+ paths.forEach { path ->
+ val trimmedPath = path.trim('/')
+ if (trimmedPath.isNotEmpty()) {
+ builder.append('/').append(trimmedPath)
+ }
+ }
+
+ return builder.toString()
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt
index bd0e638d..73cdf958 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/FmtBase.kt
@@ -4,6 +4,8 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
+import com.v2ray.ang.handler.MmkvManager
+import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -18,15 +20,15 @@ open class FmtBase {
*/
fun toUri(config: ProfileItem, userInfo: String?, dicQuery: HashMap?): String {
val query = if (dicQuery != null)
- ("?" + dicQuery.toList().joinToString(
+ "?" + dicQuery.toList().joinToString(
separator = "&",
- transform = { it.first + "=" + Utils.urlEncode(it.second) }))
+ transform = { it.first + "=" + Utils.urlEncode(it.second) })
else ""
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(userInfo ?: ""),
- Utils.getIpv6Address(config.server),
+ Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
config.serverPort
)
@@ -148,4 +150,21 @@ open class FmtBase {
return dicQuery
}
-}
\ No newline at end of file
+
+ fun getServerAddress(profileItem: ProfileItem): String {
+ if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
+ return profileItem.server.orEmpty()
+ }
+
+ val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
+ if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
+ return domain
+ }
+ //Resolve and replace domain
+ val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
+ if (resolvedIps.isNullOrEmpty()) {
+ return domain
+ }
+ return resolvedIps.first()
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt
index faac0230..8c641f24 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/HttpFmt.kt
@@ -4,6 +4,7 @@ import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.isNotNullEmpty
+import com.v2ray.ang.handler.V2rayConfigManager
object HttpFmt : FmtBase() {
/**
@@ -13,10 +14,10 @@ object HttpFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = OutboundBean.create(EConfigType.HTTP)
+ val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HTTP)
outboundBean?.settings?.servers?.first()?.let { server ->
- server.address = profileItem.server.orEmpty()
+ server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt
index 7a6abe08..3b3dc88c 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/Hysteria2Fmt.kt
@@ -9,6 +9,7 @@ import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
+import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -24,7 +25,7 @@ object Hysteria2Fmt : FmtBase() {
val config = ProfileItem.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str))
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
+ config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
@@ -144,7 +145,7 @@ object Hysteria2Fmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = OutboundBean.create(EConfigType.HYSTERIA2)
+ val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.HYSTERIA2)
return outboundBean
}
}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
index fb383d25..87ba74f8 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/ShadowsocksFmt.kt
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
+import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -35,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
if (uri.port <= 0) return null
if (uri.userInfo.isNullOrEmpty()) return null
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
+ config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
@@ -131,38 +132,22 @@ object ShadowsocksFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = OutboundBean.create(EConfigType.SHADOWSOCKS)
+ val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SHADOWSOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
- server.address = profileItem.server.orEmpty()
+ server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.method = profileItem.method
}
- val sni = outboundBean?.streamSettings?.populateTransportSettings(
- profileItem.network.orEmpty(),
- profileItem.headerType,
- profileItem.host,
- profileItem.path,
- profileItem.seed,
- profileItem.quicSecurity,
- profileItem.quicKey,
- profileItem.mode,
- profileItem.serviceName,
- profileItem.authority,
- )
+ val sni = outboundBean?.streamSettings?.let {
+ V2rayConfigManager.populateTransportSettings(it, profileItem)
+ }
- outboundBean?.streamSettings?.populateTlsSettings(
- profileItem.security.orEmpty(),
- profileItem.insecure == true,
- if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
- profileItem.fingerPrint,
- profileItem.alpn,
- profileItem.publicKey,
- profileItem.shortId,
- profileItem.spiderX,
- )
+ outboundBean?.streamSettings?.let {
+ V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
+ }
return outboundBean
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt
index f37030c2..30bc08e4 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/SocksFmt.kt
@@ -5,6 +5,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
+import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -22,7 +23,7 @@ object SocksFmt : FmtBase() {
if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
+ config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
@@ -60,10 +61,10 @@ object SocksFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = OutboundBean.create(EConfigType.SOCKS)
+ val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.SOCKS)
outboundBean?.settings?.servers?.first()?.let { server ->
- server.address = profileItem.server.orEmpty()
+ server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
if (profileItem.username.isNotNullEmpty()) {
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt
index 56883cd8..446ef99c 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/TrojanFmt.kt
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
+import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -22,7 +23,7 @@ object TrojanFmt : FmtBase() {
val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
+ config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
@@ -60,38 +61,22 @@ object TrojanFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = OutboundBean.create(EConfigType.TROJAN)
+ val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.TROJAN)
outboundBean?.settings?.servers?.first()?.let { server ->
- server.address = profileItem.server.orEmpty()
+ server.address = getServerAddress(profileItem)
server.port = profileItem.serverPort.orEmpty().toInt()
server.password = profileItem.password
server.flow = profileItem.flow
}
- val sni = outboundBean?.streamSettings?.populateTransportSettings(
- profileItem.network.orEmpty(),
- profileItem.headerType,
- profileItem.host,
- profileItem.path,
- profileItem.seed,
- profileItem.quicSecurity,
- profileItem.quicKey,
- profileItem.mode,
- profileItem.serviceName,
- profileItem.authority,
- )
+ val sni = outboundBean?.streamSettings?.let {
+ V2rayConfigManager.populateTransportSettings(it, profileItem)
+ }
- outboundBean?.streamSettings?.populateTlsSettings(
- profileItem.security.orEmpty(),
- profileItem.insecure == true,
- if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
- profileItem.fingerPrint,
- profileItem.alpn,
- profileItem.publicKey,
- profileItem.shortId,
- profileItem.spiderX,
- )
+ outboundBean?.streamSettings?.let {
+ V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
+ }
return outboundBean
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt
index 77d2f2a3..9242f0ec 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VlessFmt.kt
@@ -6,7 +6,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
-import com.v2ray.ang.util.JsonUtil
+import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
+ config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
@@ -57,41 +57,23 @@ object VlessFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = OutboundBean.create(EConfigType.VLESS)
+ val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VLESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
- vnext.address = profileItem.server.orEmpty()
+ vnext.address = getServerAddress(profileItem)
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].encryption = profileItem.method
vnext.users[0].flow = profileItem.flow
}
- val sni = outboundBean?.streamSettings?.populateTransportSettings(
- profileItem.network.orEmpty(),
- profileItem.headerType,
- profileItem.host,
- profileItem.path,
- profileItem.seed,
- profileItem.quicSecurity,
- profileItem.quicKey,
- profileItem.mode,
- profileItem.serviceName,
- profileItem.authority,
- )
- outboundBean?.streamSettings?.xhttpSettings?.mode = profileItem.xhttpMode
- outboundBean?.streamSettings?.xhttpSettings?.extra = JsonUtil.parseString(profileItem.xhttpExtra)
+ val sni = outboundBean?.streamSettings?.let {
+ V2rayConfigManager.populateTransportSettings(it, profileItem)
+ }
- outboundBean?.streamSettings?.populateTlsSettings(
- profileItem.security.orEmpty(),
- profileItem.insecure == true,
- if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
- profileItem.fingerPrint,
- profileItem.alpn,
- profileItem.publicKey,
- profileItem.shortId,
- profileItem.spiderX,
- )
+ outboundBean?.streamSettings?.let {
+ V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
+ }
return outboundBean
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt
index bbc913d3..4201f4dc 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/VmessFmt.kt
@@ -11,6 +11,7 @@ import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.handler.MmkvManager
+import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -150,7 +151,7 @@ object VmessFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
+ config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.password = uri.userInfo
@@ -168,38 +169,22 @@ object VmessFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = OutboundBean.create(EConfigType.VMESS)
+ val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.VMESS)
outboundBean?.settings?.vnext?.first()?.let { vnext ->
- vnext.address = profileItem.server.orEmpty()
+ vnext.address = getServerAddress(profileItem)
vnext.port = profileItem.serverPort.orEmpty().toInt()
vnext.users[0].id = profileItem.password.orEmpty()
vnext.users[0].security = profileItem.method
}
- val sni = outboundBean?.streamSettings?.populateTransportSettings(
- profileItem.network.orEmpty(),
- profileItem.headerType,
- profileItem.host,
- profileItem.path,
- profileItem.seed,
- profileItem.quicSecurity,
- profileItem.quicKey,
- profileItem.mode,
- profileItem.serviceName,
- profileItem.authority,
- )
+ val sni = outboundBean?.streamSettings?.let {
+ V2rayConfigManager.populateTransportSettings(it, profileItem)
+ }
- outboundBean?.streamSettings?.populateTlsSettings(
- profileItem.security.orEmpty(),
- profileItem.insecure == true,
- if (profileItem.sni.isNullOrEmpty()) sni else profileItem.sni,
- profileItem.fingerPrint,
- profileItem.alpn,
- null,
- null,
- null
- )
+ outboundBean?.streamSettings?.let {
+ V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
+ }
return outboundBean
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt
index 90736c06..8f1cec84 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/fmt/WireguardFmt.kt
@@ -7,6 +7,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
+import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -24,16 +25,16 @@ object WireguardFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri)
- config.remarks = Utils.urlDecode(uri.fragment.orEmpty())
+ config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { if (it.isEmpty()) "none" else it }
config.server = uri.idnHost
config.serverPort = uri.port.toString()
config.secretKey = uri.userInfo.orEmpty()
- config.localAddress = (queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4)
+ config.localAddress = queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.publicKey = queryParam["publickey"].orEmpty()
- config.preSharedKey = queryParam["presharedkey"].orEmpty()
+ config.preSharedKey = queryParam["presharedkey"]?.takeIf { it.isNotEmpty() }
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
- config.reserved = (queryParam["reserved"] ?: "0,0,0")
+ config.reserved = queryParam["reserved"] ?: "0,0,0"
return config
}
@@ -83,7 +84,7 @@ object WireguardFmt : FmtBase() {
config.localAddress = interfaceParams["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
config.publicKey = peerParams["publickey"].orEmpty()
- config.preSharedKey = peerParams["presharedkey"].orEmpty()
+ config.preSharedKey = peerParams["presharedkey"]?.takeIf { it.isNotEmpty() }
val endpoint = peerParams["endpoint"].orEmpty()
val endpointParts = endpoint.split(":", limit = 2)
if (endpointParts.size == 2) {
@@ -105,18 +106,18 @@ object WireguardFmt : FmtBase() {
* @return the converted OutboundBean object, or null if conversion fails
*/
fun toOutbound(profileItem: ProfileItem): OutboundBean? {
- val outboundBean = OutboundBean.create(EConfigType.WIREGUARD)
+ val outboundBean = V2rayConfigManager.createInitOutbound(EConfigType.WIREGUARD)
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty()
- peer.preSharedKey = profileItem.preSharedKey.orEmpty()
+ peer.preSharedKey = profileItem.preSharedKey?.takeIf { it.isNotEmpty() }
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
}
wireguard.mtu = profileItem.mtu
- wireguard.reserved = profileItem.reserved?.split(",")?.map { it.toInt() }
+ wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
}
return outboundBean
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt
index c40a61c7..d24ae0c2 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt
@@ -7,7 +7,9 @@ import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R
-import com.v2ray.ang.dto.*
+import com.v2ray.ang.dto.EConfigType
+import com.v2ray.ang.dto.ProfileItem
+import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.fmt.ShadowsocksFmt
@@ -413,10 +415,15 @@ object AngConfigManager {
if (!it.second.enabled) {
return 0
}
- val url = HttpUtil.idnToASCII(it.second.url)
+ val url = HttpUtil.toIdnUrl(it.second.url)
if (!Utils.isValidUrl(url)) {
return 0
}
+ if (!it.second.allowInsecureUrl) {
+ if (!Utils.isValidSubUrl(url)) {
+ return 0
+ }
+ }
Log.i(AppConfig.TAG, url)
var configText = try {
@@ -430,7 +437,7 @@ object AngConfigManager {
configText = try {
HttpUtil.getUrlContentWithUserAgent(url)
} catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to get URL content with user agent", e)
+ Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
""
}
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt
index 3b934b2a..b2e23f7f 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt
@@ -16,6 +16,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RoutingType
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.V2rayConfig
+import com.v2ray.ang.dto.VpnInterfaceAddressConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerList
import com.v2ray.ang.util.JsonUtil
@@ -159,7 +160,7 @@ object SettingsManager {
* @return True if bypassing LAN, false otherwise.
*/
fun routingRulesetsBypassLan(): Boolean {
- val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "0"
+ val vpnBypassLan = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_BYPASS_LAN) ?: "1"
if (vpnBypassLan == "1") {
return true
} else if (vpnBypassLan == "2") {
@@ -216,7 +217,7 @@ object SettingsManager {
* @return The ProfileItem.
*/
fun getServerViaRemarks(remarks: String?): ProfileItem? {
- if (remarks == null) {
+ if (remarks.isNullOrEmpty()) {
return null
}
val serverList = decodeServerList()
@@ -242,7 +243,7 @@ object SettingsManager {
* @return The HTTP port.
*/
fun getHttpPort(): Int {
- return getSocksPort() + (if (Utils.isXray()) 0 else 1)
+ return getSocksPort() + if (Utils.isXray()) 0 else 1
}
/**
@@ -316,10 +317,10 @@ object SettingsManager {
*/
fun getDelayTestUrl(second: Boolean = false): String {
return if (second) {
- AppConfig.DelayTestUrl2
+ AppConfig.DELAY_TEST_URL2
} else {
MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL)
- ?: AppConfig.DelayTestUrl
+ ?: AppConfig.DELAY_TEST_URL
}
}
@@ -356,4 +357,17 @@ object SettingsManager {
"2" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}
+
+ /**
+ * Retrieves the currently selected VPN interface address configuration.
+ * This method reads the user's preference for VPN interface addressing and returns
+ * the corresponding configuration containing IPv4 and IPv6 addresses.
+ *
+ * @return The selected VpnInterfaceAddressConfig instance, or the default configuration
+ * if no valid selection is found or if the stored index is invalid.
+ */
+ fun getCurrentVpnInterfaceAddressConfig(): VpnInterfaceAddressConfig {
+ val selectedIndex = MmkvManager.decodeSettingsString(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX, "0")?.toInt()
+ return VpnInterfaceAddressConfig.getConfigByIndex(selectedIndex ?: 0)
+ }
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt
index bab1f1d4..e547c378 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt
@@ -6,8 +6,10 @@ import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
+import com.v2ray.ang.dto.IPAPIInfo
import com.v2ray.ang.extension.responseLength
import com.v2ray.ang.util.HttpUtil
+import com.v2ray.ang.util.JsonUtil
import kotlinx.coroutines.isActive
import libv2ray.Libv2ray
import java.io.IOException
@@ -164,6 +166,17 @@ object SpeedtestManager {
return Pair(elapsed, result)
}
+ fun getRemoteIPInfo(): String? {
+ val httpPort = SettingsManager.getHttpPort()
+ var content = HttpUtil.getUrlContent(AppConfig.IP_API_URL, 5000, httpPort) ?: return null
+
+ var ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
+ var ip = ipInfo.ip ?: ipInfo.clientIp ?: ipInfo.ip_addr ?: ipInfo.query
+ var country = ipInfo.country_code ?: ipInfo.country ?: ipInfo.countryCode
+
+ return "(${country ?: "unknown"}) $ip"
+ }
+
/**
* Gets the version of the V2Ray library.
*
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt
new file mode 100644
index 00000000..37b55c2e
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt
@@ -0,0 +1,107 @@
+package com.v2ray.ang.handler
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.BuildConfig
+import com.v2ray.ang.dto.CheckUpdateResult
+import com.v2ray.ang.dto.GitHubRelease
+import com.v2ray.ang.extension.concatUrl
+import com.v2ray.ang.util.HttpUtil
+import com.v2ray.ang.util.JsonUtil
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.FileOutputStream
+
+object UpdateCheckerManager {
+ suspend fun checkForUpdate(includePreRelease: Boolean = false): CheckUpdateResult = withContext(Dispatchers.IO) {
+ val url = if (includePreRelease) {
+ AppConfig.APP_API_URL
+ } else {
+ AppConfig.APP_API_URL.concatUrl("latest")
+ }
+
+ var response = HttpUtil.getUrlContent(url, 5000)
+ if (response.isNullOrEmpty()) {
+ val httpPort = SettingsManager.getHttpPort()
+ response = HttpUtil.getUrlContent(url, 5000, httpPort) ?: throw IllegalStateException("Failed to get response")
+ }
+
+ val latestRelease = if (includePreRelease) {
+ JsonUtil.fromJson(response, Array::class.java)
+ .firstOrNull()
+ ?: throw IllegalStateException("No pre-release found")
+ } else {
+ JsonUtil.fromJson(response, GitHubRelease::class.java)
+ }
+
+ val latestVersion = latestRelease.tagName.removePrefix("v")
+ Log.i(AppConfig.TAG, "Found new version: $latestVersion (current: ${BuildConfig.VERSION_NAME})")
+
+ return@withContext if (compareVersions(latestVersion, BuildConfig.VERSION_NAME) > 0) {
+ val downloadUrl = getDownloadUrl(latestRelease, Build.SUPPORTED_ABIS[0])
+ CheckUpdateResult(
+ hasUpdate = true,
+ latestVersion = latestVersion,
+ releaseNotes = latestRelease.body,
+ downloadUrl = downloadUrl,
+ isPreRelease = latestRelease.prerelease
+ )
+ } else {
+ CheckUpdateResult(hasUpdate = false)
+ }
+ }
+
+ suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
+ try {
+ val httpPort = SettingsManager.getHttpPort()
+ val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
+ ?: throw IllegalStateException("Failed to create connection")
+
+ try {
+ val apkFile = File(context.cacheDir, "update.apk")
+ Log.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
+
+ FileOutputStream(apkFile).use { outputStream ->
+ connection.inputStream.use { inputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ Log.i(AppConfig.TAG, "APK download completed")
+ return@withContext apkFile
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
+ return@withContext null
+ } finally {
+ try {
+ connection.disconnect()
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Error closing connection: ${e.message}")
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
+ return@withContext null
+ }
+ }
+
+ private fun compareVersions(version1: String, version2: String): Int {
+ val v1 = version1.split(".")
+ val v2 = version2.split(".")
+
+ for (i in 0 until maxOf(v1.size, v2.size)) {
+ val num1 = if (i < v1.size) v1[i].toInt() else 0
+ val num2 = if (i < v2.size) v2[i].toInt() else 0
+ if (num1 != num2) return num1 - num2
+ }
+ return 0
+ }
+
+ private fun getDownloadUrl(release: GitHubRelease, abi: String): String {
+ return release.assets.find { it.name.contains(abi) }?.browserDownloadUrl
+ ?: release.assets.firstOrNull()?.browserDownloadUrl
+ ?: throw IllegalStateException("No compatible APK found")
+ }
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt
index 0a08b1ec..f53697bb 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt
@@ -4,55 +4,33 @@ import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.DEFAULT_NETWORK
-import com.v2ray.ang.AppConfig.DNS_ALIDNS_ADDRESSES
-import com.v2ray.ang.AppConfig.DNS_ALIDNS_DOMAIN
-import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_ADDRESSES
-import com.v2ray.ang.AppConfig.DNS_CLOUDFLARE_DOMAIN
-import com.v2ray.ang.AppConfig.DNS_DNSPOD_ADDRESSES
-import com.v2ray.ang.AppConfig.DNS_DNSPOD_DOMAIN
-import com.v2ray.ang.AppConfig.DNS_GOOGLE_ADDRESSES
-import com.v2ray.ang.AppConfig.DNS_GOOGLE_DOMAIN
-import com.v2ray.ang.AppConfig.DNS_QUAD9_ADDRESSES
-import com.v2ray.ang.AppConfig.DNS_QUAD9_DOMAIN
-import com.v2ray.ang.AppConfig.DNS_YANDEX_ADDRESSES
-import com.v2ray.ang.AppConfig.DNS_YANDEX_DOMAIN
-import com.v2ray.ang.AppConfig.GEOIP_CN
-import com.v2ray.ang.AppConfig.GEOSITE_CN
-import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
-import com.v2ray.ang.AppConfig.GOOGLEAPIS_CN_DOMAIN
-import com.v2ray.ang.AppConfig.GOOGLEAPIS_COM_DOMAIN
-import com.v2ray.ang.AppConfig.HEADER_TYPE_HTTP
-import com.v2ray.ang.AppConfig.LOOPBACK
-import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
-import com.v2ray.ang.AppConfig.TAG_BLOCKED
-import com.v2ray.ang.AppConfig.TAG_DIRECT
-import com.v2ray.ang.AppConfig.TAG_FRAGMENT
-import com.v2ray.ang.AppConfig.TAG_PROXY
-import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
-import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.NetworkType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.V2rayConfig
+import com.v2ray.ang.dto.V2rayConfig.OutboundBean
+import com.v2ray.ang.dto.V2rayConfig.OutboundBean.OutSettingsBean
+import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean
import com.v2ray.ang.dto.V2rayConfig.RoutingBean.RulesBean
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.fmt.HttpFmt
-import com.v2ray.ang.fmt.Hysteria2Fmt
import com.v2ray.ang.fmt.ShadowsocksFmt
import com.v2ray.ang.fmt.SocksFmt
import com.v2ray.ang.fmt.TrojanFmt
import com.v2ray.ang.fmt.VlessFmt
import com.v2ray.ang.fmt.VmessFmt
import com.v2ray.ang.fmt.WireguardFmt
+import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
object V2rayConfigManager {
private var initConfigCache: String? = null
+ //region get config function
+
/**
* Retrieves the V2ray configuration for the given GUID.
*
@@ -104,8 +82,7 @@ object V2rayConfigManager {
*/
private fun getV2rayCustomConfig(guid: String, config: ProfileItem): ConfigResult {
val raw = MmkvManager.decodeServerRaw(guid) ?: return ConfigResult(false)
- val domainPort = config.getServerAddressAndPort()
- return ConfigResult(true, guid, raw, domainPort)
+ return ConfigResult(true, guid, raw)
}
/**
@@ -120,7 +97,7 @@ object V2rayConfigManager {
val result = ConfigResult(false)
val address = config.server ?: return result
- if (!Utils.isIpAddress(address)) {
+ if (!Utils.isPureIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
return result
@@ -131,29 +108,36 @@ object V2rayConfigManager {
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.remarks = config.remarks
- inbounds(v2rayConfig)
+ getInbounds(v2rayConfig)
- val isPlugin = config.configType == EConfigType.HYSTERIA2
- val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
- val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
+ if (config.configType == EConfigType.HYSTERIA2) {
+ result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result
+ } else {
+ getOutbounds(v2rayConfig, config) ?: return result
+ getMoreOutbounds(v2rayConfig, config.subscriptionId)
+ }
- routing(v2rayConfig)
+ getRouting(v2rayConfig)
- fakedns(v2rayConfig)
+ getFakeDns(v2rayConfig)
- dns(v2rayConfig)
+ getDns(v2rayConfig)
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true) {
- customLocalDns(v2rayConfig)
+ getCustomLocalDns(v2rayConfig)
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
v2rayConfig.stats = null
v2rayConfig.policy = null
}
+ //Resolve and add to DNS Hosts
+ if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") == "1") {
+ resolveOutboundDomainsToHosts(v2rayConfig)
+ }
+
result.status = true
- result.content = v2rayConfig.toPrettyPrinting()
- result.domainPort = if (retMore.first) retMore.second else retOut.second
+ result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: ""
result.guid = guid
return result
}
@@ -170,7 +154,7 @@ object V2rayConfigManager {
val result = ConfigResult(false)
val address = config.server ?: return result
- if (!Utils.isIpAddress(address)) {
+ if (!Utils.isPureIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.w(AppConfig.TAG, "$address is an invalid ip or domain")
return result
@@ -179,9 +163,12 @@ object V2rayConfigManager {
val v2rayConfig = initV2rayConfig(context) ?: return result
- val isPlugin = config.configType == EConfigType.HYSTERIA2
- val retOut = outbounds(v2rayConfig, config, isPlugin) ?: return result
- val retMore = moreOutbounds(v2rayConfig, config.subscriptionId, isPlugin)
+ if (config.configType == EConfigType.HYSTERIA2) {
+ result.socksPort = getPlusOutbounds(v2rayConfig, config) ?: return result
+ } else {
+ getOutbounds(v2rayConfig, config) ?: return result
+ getMoreOutbounds(v2rayConfig, config.subscriptionId)
+ }
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.inbounds.clear()
@@ -196,8 +183,7 @@ object V2rayConfigManager {
}
result.status = true
- result.content = v2rayConfig.toPrettyPrinting()
- result.domainPort = if (retMore.first) retMore.second else retOut.second
+ result.content = JsonUtil.toJsonPretty(v2rayConfig) ?: ""
result.guid = guid
return result
}
@@ -212,7 +198,6 @@ object V2rayConfigManager {
* @param context Android context used to access application assets
* @return V2rayConfig object parsed from the JSON configuration, or null if the configuration is empty
*/
-
private fun initV2rayConfig(context: Context): V2rayConfig? {
val assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
if (TextUtils.isEmpty(assets)) {
@@ -223,14 +208,28 @@ object V2rayConfigManager {
return config
}
- private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
+
+ //endregion
+
+
+ //region some sub function
+
+ /**
+ * Configures the inbound settings for V2ray.
+ *
+ * This function sets up the listening ports, sniffing options, and other inbound-related configurations.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ * @return true if inbound configuration was successful, false otherwise
+ */
+ private fun getInbounds(v2rayConfig: V2rayConfig): Boolean {
try {
val socksPort = SettingsManager.getSocksPort()
v2rayConfig.inbounds.forEach { curInbound ->
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
//bind all inbounds to localhost if the user requests
- curInbound.listen = LOOPBACK
+ curInbound.listen = AppConfig.LOOPBACK
}
}
v2rayConfig.inbounds[0].port = socksPort
@@ -261,44 +260,14 @@ object V2rayConfigManager {
return true
}
- private fun outbounds(v2rayConfig: V2rayConfig, config: ProfileItem, isPlugin: Boolean): Pair? {
- if (isPlugin) {
- val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
- val outboundNew = V2rayConfig.OutboundBean(
- mux = null,
- protocol = EConfigType.SOCKS.name.lowercase(),
- settings = V2rayConfig.OutboundBean.OutSettingsBean(
- servers = listOf(
- V2rayConfig.OutboundBean.OutSettingsBean.ServersBean(
- address = LOOPBACK,
- port = socksPort
- )
- )
- )
- )
- if (v2rayConfig.outbounds.isNotEmpty()) {
- v2rayConfig.outbounds[0] = outboundNew
- } else {
- v2rayConfig.outbounds.add(outboundNew)
- }
- return Pair(true, outboundNew.getServerAddressAndPort())
- }
-
- val outbound = getProxyOutbound(config) ?: return null
- val ret = updateOutboundWithGlobalSettings(outbound)
- if (!ret) return null
-
- if (v2rayConfig.outbounds.isNotEmpty()) {
- v2rayConfig.outbounds[0] = outbound
- } else {
- v2rayConfig.outbounds.add(outbound)
- }
-
- updateOutboundFragment(v2rayConfig)
- return Pair(true, config.getServerAddressAndPort())
- }
-
- private fun fakedns(v2rayConfig: V2rayConfig) {
+ /**
+ * Configures the fake DNS settings if enabled.
+ *
+ * Adds FakeDNS configuration to v2rayConfig if both local DNS and fake DNS are enabled.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ */
+ private fun getFakeDns(v2rayConfig: V2rayConfig) {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
) {
@@ -306,16 +275,24 @@ object V2rayConfigManager {
}
}
- private fun routing(v2rayConfig: V2rayConfig): Boolean {
+ /**
+ * Configures routing settings for V2ray.
+ *
+ * Sets up the domain strategy and adds routing rules from saved rulesets.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ * @return true if routing configuration was successful, false otherwise
+ */
+ private fun getRouting(v2rayConfig: V2rayConfig): Boolean {
try {
v2rayConfig.routing.domainStrategy =
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
- ?: "IPIfNonMatch"
+ ?: "AsIs"
val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key ->
- routingUserRule(key, v2rayConfig)
+ getRoutingUserRule(key, v2rayConfig)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to configure routing", e)
@@ -324,7 +301,13 @@ object V2rayConfigManager {
return true
}
- private fun routingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
+ /**
+ * Adds a specific ruleset item to the routing configuration.
+ *
+ * @param item The ruleset item to add
+ * @param v2rayConfig The V2ray configuration object to be modified
+ */
+ private fun getRoutingUserRule(item: RulesetItem?, v2rayConfig: V2rayConfig) {
try {
if (item == null || !item.enabled) {
return
@@ -339,14 +322,22 @@ object V2rayConfigManager {
}
}
- private fun userRule2Domain(tag: String): ArrayList {
+ /**
+ * Retrieves domain rules for a specific outbound tag.
+ *
+ * Searches through all rulesets to find domains targeting the specified tag.
+ *
+ * @param tag The outbound tag to search for
+ * @return ArrayList of domain rules matching the tag
+ */
+ private fun getUserRule2Domain(tag: String): ArrayList {
val domain = ArrayList()
val rulesetItems = MmkvManager.decodeRoutingRulesets()
rulesetItems?.forEach { key ->
if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
key.domain?.forEach {
- if (it != GEOSITE_PRIVATE
+ if (it != AppConfig.GEOSITE_PRIVATE
&& (it.startsWith("geosite:") || it.startsWith("domain:"))
) {
domain.add(it)
@@ -358,12 +349,20 @@ object V2rayConfigManager {
return domain
}
- private fun customLocalDns(v2rayConfig: V2rayConfig): Boolean {
+ /**
+ * Configures custom local DNS settings.
+ *
+ * Sets up DNS inbound, outbound, and routing rules for local DNS resolution.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ * @return true if custom local DNS configuration was successful, false otherwise
+ */
+ private fun getCustomLocalDns(v2rayConfig: V2rayConfig): Boolean {
try {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
- val geositeCn = arrayListOf(GEOSITE_CN)
- val proxyDomain = userRule2Domain(TAG_PROXY)
- val directDomain = userRule2Domain(TAG_DIRECT)
+ val geositeCn = arrayListOf(AppConfig.GEOSITE_CN)
+ val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
+ val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
// fakedns with all domains to make it always top priority
v2rayConfig.dns?.servers?.add(
0,
@@ -391,7 +390,7 @@ object V2rayConfigManager {
V2rayConfig.InboundBean(
tag = "dns-in",
port = localDnsPort,
- listen = LOOPBACK,
+ listen = AppConfig.LOOPBACK,
protocol = "dokodemo-door",
settings = dnsInboundSettings,
sniffing = null
@@ -427,14 +426,22 @@ object V2rayConfigManager {
return true
}
- private fun dns(v2rayConfig: V2rayConfig): Boolean {
+ /**
+ * Configures the DNS settings for V2ray.
+ *
+ * Sets up DNS servers, hosts, and routing rules for DNS resolution.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ * @return true if DNS configuration was successful, false otherwise
+ */
+ private fun getDns(v2rayConfig: V2rayConfig): Boolean {
try {
val hosts = mutableMapOf()
val servers = ArrayList()
//remote Dns
val remoteDns = SettingsManager.getRemoteDnsServers()
- val proxyDomain = userRule2Domain(TAG_PROXY)
+ val proxyDomain = getUserRule2Domain(AppConfig.TAG_PROXY)
remoteDns.forEach {
servers.add(it)
}
@@ -449,9 +456,9 @@ object V2rayConfigManager {
// domestic DNS
val domesticDns = SettingsManager.getDomesticDnsServers()
- val directDomain = userRule2Domain(TAG_DIRECT)
- val isCnRoutingMode = directDomain.contains(GEOSITE_CN)
- val geoipCn = arrayListOf(GEOIP_CN)
+ val directDomain = getUserRule2Domain(AppConfig.TAG_DIRECT)
+ val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
+ val geoipCn = arrayListOf(AppConfig.GEOIP_CN)
if (directDomain.isNotEmpty()) {
servers.add(
V2rayConfig.DnsBean.ServersBean(
@@ -466,7 +473,7 @@ object V2rayConfigManager {
if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add(
0, RulesBean(
- outboundTag = TAG_DIRECT,
+ outboundTag = AppConfig.TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
domain = null
@@ -474,6 +481,25 @@ object V2rayConfigManager {
)
}
+ //block dns
+ val blkDomain = getUserRule2Domain(AppConfig.TAG_BLOCKED)
+ if (blkDomain.isNotEmpty()) {
+ hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
+ }
+
+ // hardcode googleapi rule to fix play store problems
+ hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
+
+ // hardcode popular Android Private DNS rule to fix localhost DNS problem
+ hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
+ hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES
+ hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES
+ hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES
+ hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES
+ hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
+ hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
+ hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
+
//User DNS hosts
try {
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
@@ -488,24 +514,6 @@ object V2rayConfigManager {
Log.e(AppConfig.TAG, "Failed to configure user DNS hosts", e)
}
- //block dns
- val blkDomain = userRule2Domain(TAG_BLOCKED)
- if (blkDomain.isNotEmpty()) {
- hosts.putAll(blkDomain.map { it to LOOPBACK })
- }
-
- // hardcode googleapi rule to fix play store problems
- hosts[GOOGLEAPIS_CN_DOMAIN] = GOOGLEAPIS_COM_DOMAIN
-
- // hardcode popular Android Private DNS rule to fix localhost DNS problem
- hosts[DNS_ALIDNS_DOMAIN] = DNS_ALIDNS_ADDRESSES
- hosts[DNS_CLOUDFLARE_DOMAIN] = DNS_CLOUDFLARE_ADDRESSES
- hosts[DNS_DNSPOD_DOMAIN] = DNS_DNSPOD_ADDRESSES
- hosts[DNS_GOOGLE_DOMAIN] = DNS_GOOGLE_ADDRESSES
- hosts[DNS_QUAD9_DOMAIN] = DNS_QUAD9_ADDRESSES
- hosts[DNS_YANDEX_DOMAIN] = DNS_YANDEX_ADDRESSES
-
-
// DNS dns
v2rayConfig.dns = V2rayConfig.DnsBean(
servers = servers,
@@ -516,7 +524,7 @@ object V2rayConfigManager {
if (Utils.isPureIpAddress(remoteDns.first())) {
v2rayConfig.routing.rules.add(
0, RulesBean(
- outboundTag = TAG_PROXY,
+ outboundTag = AppConfig.TAG_PROXY,
port = "53",
ip = arrayListOf(remoteDns.first()),
domain = null
@@ -530,6 +538,138 @@ object V2rayConfigManager {
return true
}
+
+ //endregion
+
+
+ //region outbound related functions
+
+ /**
+ * Configures the primary outbound connection.
+ *
+ * Converts the profile to an outbound configuration and applies global settings.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ * @param config The profile item containing connection details
+ * @return true if outbound configuration was successful, null if there was an error
+ */
+ private fun getOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Boolean? {
+ val outbound = convertProfile2Outbound(config) ?: return null
+ val ret = updateOutboundWithGlobalSettings(outbound)
+ if (!ret) return null
+
+ if (v2rayConfig.outbounds.isNotEmpty()) {
+ v2rayConfig.outbounds[0] = outbound
+ } else {
+ v2rayConfig.outbounds.add(outbound)
+ }
+
+ updateOutboundFragment(v2rayConfig)
+ return true
+ }
+
+ /**
+ * Configures special outbound settings for Hysteria2 protocol.
+ *
+ * Creates a SOCKS outbound connection on a free port for protocols requiring special handling.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ * @param config The profile item containing connection details
+ * @return The port number for the SOCKS connection, or null if there was an error
+ */
+ private fun getPlusOutbounds(v2rayConfig: V2rayConfig, config: ProfileItem): Int? {
+ try {
+ val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
+
+ val outboundNew = OutboundBean(
+ mux = null,
+ protocol = EConfigType.SOCKS.name.lowercase(),
+ settings = OutSettingsBean(
+ servers = listOf(
+ OutSettingsBean.ServersBean(
+ address = AppConfig.LOOPBACK,
+ port = socksPort
+ )
+ )
+ )
+ )
+ if (v2rayConfig.outbounds.isNotEmpty()) {
+ v2rayConfig.outbounds[0] = outboundNew
+ } else {
+ v2rayConfig.outbounds.add(outboundNew)
+ }
+
+ return socksPort
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to configure plusOutbound", e)
+ return null
+ }
+ }
+
+ /**
+ * Configures additional outbound connections for proxy chaining.
+ *
+ * Sets up previous and next proxies in a subscription for advanced routing capabilities.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ * @param subscriptionId The subscription ID to look up related proxies
+ * @return true if additional outbounds were configured successfully, false otherwise
+ */
+ private fun getMoreOutbounds(v2rayConfig: V2rayConfig, subscriptionId: String): Boolean {
+ //fragment proxy
+ if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
+ return false
+ }
+
+ if (subscriptionId.isEmpty()) {
+ return false
+ }
+ try {
+ val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return false
+
+ //current proxy
+ val outbound = v2rayConfig.outbounds[0]
+
+ //Previous proxy
+ val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
+ if (prevNode != null) {
+ val prevOutbound = convertProfile2Outbound(prevNode)
+ if (prevOutbound != null) {
+ updateOutboundWithGlobalSettings(prevOutbound)
+ prevOutbound.tag = AppConfig.TAG_PROXY + "2"
+ v2rayConfig.outbounds.add(prevOutbound)
+ outbound.ensureSockopt().dialerProxy = prevOutbound.tag
+ }
+ }
+
+ //Next proxy
+ val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
+ if (nextNode != null) {
+ val nextOutbound = convertProfile2Outbound(nextNode)
+ if (nextOutbound != null) {
+ updateOutboundWithGlobalSettings(nextOutbound)
+ nextOutbound.tag = AppConfig.TAG_PROXY
+ v2rayConfig.outbounds.add(0, nextOutbound)
+ outbound.tag = AppConfig.TAG_PROXY + "1"
+ nextOutbound.ensureSockopt().dialerProxy = outbound.tag
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to configure more outbounds", e)
+ return false
+ }
+
+ return true
+ }
+
+ /**
+ * Updates outbound settings based on global preferences.
+ *
+ * Applies multiplexing and protocol-specific settings to an outbound connection.
+ *
+ * @param outbound The outbound connection to update
+ * @return true if the update was successful, false otherwise
+ */
private fun updateOutboundWithGlobalSettings(outbound: V2rayConfig.OutboundBean): Boolean {
try {
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
@@ -561,7 +701,7 @@ object V2rayConfigManager {
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
var localTunAddr = if (outbound.settings?.address == null) {
- listOf(WIREGUARD_LOCAL_ADDRESS_V4, WIREGUARD_LOCAL_ADDRESS_V6)
+ listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
} else {
outbound.settings?.address as List<*>
}
@@ -571,8 +711,8 @@ object V2rayConfigManager {
outbound.settings?.address = localTunAddr
}
- if (outbound.streamSettings?.network == DEFAULT_NETWORK
- && outbound.streamSettings?.tcpSettings?.header?.type == HEADER_TYPE_HTTP
+ if (outbound.streamSettings?.network == AppConfig.DEFAULT_NETWORK
+ && outbound.streamSettings?.tcpSettings?.header?.type == AppConfig.HEADER_TYPE_HTTP
) {
val path = outbound.streamSettings?.tcpSettings?.header?.request?.path
val host = outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host
@@ -582,7 +722,7 @@ object V2rayConfigManager {
}
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString,
- V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
+ StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
)
outbound.streamSettings?.tcpSettings?.header?.request?.path =
if (path.isNullOrEmpty()) {
@@ -601,6 +741,14 @@ object V2rayConfigManager {
return true
}
+ /**
+ * Updates the outbound with fragment settings for traffic optimization.
+ *
+ * Configures packet fragmentation for TLS and REALITY protocols if enabled.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ * @return true if fragment configuration was successful, false otherwise
+ */
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
try {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
@@ -614,8 +762,8 @@ object V2rayConfigManager {
val fragmentOutbound =
V2rayConfig.OutboundBean(
- protocol = PROTOCOL_FREEDOM,
- tag = TAG_FRAGMENT,
+ protocol = AppConfig.PROTOCOL_FREEDOM,
+ tag = AppConfig.TAG_FRAGMENT,
mux = null
)
@@ -631,8 +779,8 @@ object V2rayConfigManager {
packets = "tlshello"
}
- fragmentOutbound.settings = V2rayConfig.OutboundBean.OutSettingsBean(
- fragment = V2rayConfig.OutboundBean.OutSettingsBean.FragmentBean(
+ fragmentOutbound.settings = OutboundBean.OutSettingsBean(
+ fragment = OutboundBean.OutSettingsBean.FragmentBean(
packets = packets,
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
?: "50-100",
@@ -640,15 +788,15 @@ object V2rayConfigManager {
?: "10-20"
),
noises = listOf(
- V2rayConfig.OutboundBean.OutSettingsBean.NoiseBean(
+ OutboundBean.OutSettingsBean.NoiseBean(
type = "rand",
packet = "10-20",
delay = "10-16",
)
),
)
- fragmentOutbound.streamSettings = V2rayConfig.OutboundBean.StreamSettingsBean(
- sockopt = V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
+ fragmentOutbound.streamSettings = StreamSettingsBean(
+ sockopt = StreamSettingsBean.SockoptBean(
TcpNoDelay = true,
mark = 255
)
@@ -657,8 +805,8 @@ object V2rayConfigManager {
//proxy chain
v2rayConfig.outbounds[0].streamSettings?.sockopt =
- V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
- dialerProxy = TAG_FRAGMENT
+ StreamSettingsBean.SockoptBean(
+ dialerProxy = AppConfig.TAG_FRAGMENT
)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
@@ -667,80 +815,51 @@ object V2rayConfigManager {
return true
}
- private fun moreOutbounds(
- v2rayConfig: V2rayConfig,
- subscriptionId: String,
- isPlugin: Boolean
- ): Pair {
- val returnPair = Pair(false, "")
- var domainPort: String = ""
+ /**
+ * Resolves domain names to IP addresses in outbound connections.
+ *
+ * Pre-resolves domains to improve connection speed and reliability.
+ *
+ * @param v2rayConfig The V2ray configuration object to be modified
+ */
+ private fun resolveOutboundDomainsToHosts(v2rayConfig: V2rayConfig) {
+ val proxyOutboundList = v2rayConfig.getAllProxyOutbound()
+ val dns = v2rayConfig.dns ?: return
+ val newHosts = dns.hosts?.toMutableMap() ?: mutableMapOf()
+ val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true
- if (isPlugin) {
- return returnPair
- }
- //fragment proxy
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == true) {
- return returnPair
- }
+ for (item in proxyOutboundList) {
+ val domain = item.getServerAddress()
+ if (domain.isNullOrEmpty()) continue
- if (subscriptionId.isEmpty()) {
- return returnPair
- }
- try {
- val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return returnPair
-
- //current proxy
- val outbound = v2rayConfig.outbounds[0]
-
- //Previous proxy
- val prevNode = SettingsManager.getServerViaRemarks(subItem.prevProfile)
- if (prevNode != null) {
- val prevOutbound = getProxyOutbound(prevNode)
- if (prevOutbound != null) {
- updateOutboundWithGlobalSettings(prevOutbound)
- prevOutbound.tag = TAG_PROXY + "2"
- v2rayConfig.outbounds.add(prevOutbound)
- outbound.streamSettings?.sockopt =
- V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
- dialerProxy = prevOutbound.tag
- )
- domainPort = prevNode.getServerAddressAndPort()
- }
+ if (newHosts.containsKey(domain)) {
+ item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
+ continue
}
- //Next proxy
- val nextNode = SettingsManager.getServerViaRemarks(subItem.nextProfile)
- if (nextNode != null) {
- val nextOutbound = getProxyOutbound(nextNode)
- if (nextOutbound != null) {
- updateOutboundWithGlobalSettings(nextOutbound)
- nextOutbound.tag = TAG_PROXY
- v2rayConfig.outbounds.add(0, nextOutbound)
- outbound.tag = TAG_PROXY + "1"
- nextOutbound.streamSettings?.sockopt =
- V2rayConfig.OutboundBean.StreamSettingsBean.SockoptBean(
- dialerProxy = outbound.tag
- )
- }
+ val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6)
+ if (resolvedIps.isNullOrEmpty()) continue
+
+ item.ensureSockopt().domainStrategy = if (preferIpv6) "UseIPv6v4" else "UseIPv4v6"
+ newHosts[domain] = if (resolvedIps.size == 1) {
+ resolvedIps[0]
+ } else {
+ resolvedIps
}
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to configure more outbounds", e)
- return returnPair
}
- if (domainPort.isNotEmpty()) {
- return Pair(true, domainPort)
- }
- return returnPair
+ dns.hosts = newHosts
}
/**
- * Retrieves the proxy outbound configuration for the given profile item.
+ * Converts a profile item to an outbound configuration.
*
- * @param profileItem The profile item for which to get the proxy outbound configuration.
- * @return The proxy outbound configuration as a V2rayConfig.OutboundBean, or null if not found.
+ * Creates appropriate outbound settings based on the protocol type.
+ *
+ * @param profileItem The profile item to convert
+ * @return OutboundBean configuration for the profile, or null if not supported
*/
- fun getProxyOutbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
+ private fun convertProfile2Outbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
return when (profileItem.configType) {
EConfigType.VMESS -> VmessFmt.toOutbound(profileItem)
EConfigType.CUSTOM -> null
@@ -749,10 +868,224 @@ object V2rayConfigManager {
EConfigType.VLESS -> VlessFmt.toOutbound(profileItem)
EConfigType.TROJAN -> TrojanFmt.toOutbound(profileItem)
EConfigType.WIREGUARD -> WireguardFmt.toOutbound(profileItem)
- EConfigType.HYSTERIA2 -> Hysteria2Fmt.toOutbound(profileItem)
+ EConfigType.HYSTERIA2 -> null
EConfigType.HTTP -> HttpFmt.toOutbound(profileItem)
}
-
}
+ /**
+ * Creates an initial outbound configuration for a specific protocol type.
+ *
+ * Provides a template configuration for different protocol types.
+ *
+ * @param configType The type of configuration to create
+ * @return An initial OutboundBean for the specified configuration type, or null for custom types
+ */
+ fun createInitOutbound(configType: EConfigType): OutboundBean? {
+ return when (configType) {
+ EConfigType.VMESS,
+ EConfigType.VLESS ->
+ return OutboundBean(
+ protocol = configType.name.lowercase(),
+ settings = OutSettingsBean(
+ vnext = listOf(
+ OutSettingsBean.VnextBean(
+ users = listOf(OutSettingsBean.VnextBean.UsersBean())
+ )
+ )
+ ),
+ streamSettings = StreamSettingsBean()
+ )
+
+ EConfigType.SHADOWSOCKS,
+ EConfigType.SOCKS,
+ EConfigType.HTTP,
+ EConfigType.TROJAN,
+ EConfigType.HYSTERIA2 ->
+ return OutboundBean(
+ protocol = configType.name.lowercase(),
+ settings = OutSettingsBean(
+ servers = listOf(OutSettingsBean.ServersBean())
+ ),
+ streamSettings = StreamSettingsBean()
+ )
+
+ EConfigType.WIREGUARD ->
+ return OutboundBean(
+ protocol = configType.name.lowercase(),
+ settings = OutSettingsBean(
+ secretKey = "",
+ peers = listOf(OutSettingsBean.WireGuardBean())
+ )
+ )
+
+ EConfigType.CUSTOM -> null
+ }
+ }
+
+ /**
+ * Configures transport settings for an outbound connection.
+ *
+ * Sets up protocol-specific transport options based on the profile settings.
+ *
+ * @param streamSettings The stream settings to configure
+ * @param profileItem The profile containing transport configuration
+ * @return The Server Name Indication (SNI) value to use, or null if not applicable
+ */
+ fun populateTransportSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem): String? {
+ val transport = profileItem.network.orEmpty()
+ val headerType = profileItem.headerType
+ val host = profileItem.host
+ val path = profileItem.path
+ val seed = profileItem.seed
+// val quicSecurity = profileItem.quicSecurity
+// val key = profileItem.quicKey
+ val mode = profileItem.mode
+ val serviceName = profileItem.serviceName
+ val authority = profileItem.authority
+ val xhttpMode = profileItem.xhttpMode
+ val xhttpExtra = profileItem.xhttpExtra
+
+ var sni: String? = null
+ streamSettings.network = if (transport.isEmpty()) NetworkType.TCP.type else transport
+ when (streamSettings.network) {
+ NetworkType.TCP.type -> {
+ val tcpSetting = StreamSettingsBean.TcpSettingsBean()
+ if (headerType == AppConfig.HEADER_TYPE_HTTP) {
+ tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
+ if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
+ val requestObj = StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
+ requestObj.headers.Host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ requestObj.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ tcpSetting.header.request = requestObj
+ sni = requestObj.headers.Host?.getOrNull(0)
+ }
+ } else {
+ tcpSetting.header.type = "none"
+ sni = host
+ }
+ streamSettings.tcpSettings = tcpSetting
+ }
+
+ NetworkType.KCP.type -> {
+ val kcpsetting = StreamSettingsBean.KcpSettingsBean()
+ kcpsetting.header.type = headerType ?: "none"
+ if (seed.isNullOrEmpty()) {
+ kcpsetting.seed = null
+ } else {
+ kcpsetting.seed = seed
+ }
+ if (host.isNullOrEmpty()) {
+ kcpsetting.header.domain = null
+ } else {
+ kcpsetting.header.domain = host
+ }
+ streamSettings.kcpSettings = kcpsetting
+ }
+
+ NetworkType.WS.type -> {
+ val wssetting = StreamSettingsBean.WsSettingsBean()
+ wssetting.headers.Host = host.orEmpty()
+ sni = host
+ wssetting.path = path ?: "/"
+ streamSettings.wsSettings = wssetting
+ }
+
+ NetworkType.HTTP_UPGRADE.type -> {
+ val httpupgradeSetting = StreamSettingsBean.HttpupgradeSettingsBean()
+ httpupgradeSetting.host = host.orEmpty()
+ sni = host
+ httpupgradeSetting.path = path ?: "/"
+ streamSettings.httpupgradeSettings = httpupgradeSetting
+ }
+
+ NetworkType.XHTTP.type -> {
+ val xhttpSetting = StreamSettingsBean.XhttpSettingsBean()
+ xhttpSetting.host = host.orEmpty()
+ sni = host
+ xhttpSetting.path = path ?: "/"
+ xhttpSetting.mode = xhttpMode
+ xhttpSetting.extra = JsonUtil.parseString(xhttpExtra)
+ streamSettings.xhttpSettings = xhttpSetting
+ }
+
+ NetworkType.H2.type, NetworkType.HTTP.type -> {
+ streamSettings.network = NetworkType.H2.type
+ val h2Setting = StreamSettingsBean.HttpSettingsBean()
+ h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
+ sni = h2Setting.host.getOrNull(0)
+ h2Setting.path = path ?: "/"
+ streamSettings.httpSettings = h2Setting
+ }
+
+// "quic" -> {
+// val quicsetting = QuicSettingBean()
+// quicsetting.security = quicSecurity ?: "none"
+// quicsetting.key = key.orEmpty()
+// quicsetting.header.type = headerType ?: "none"
+// quicSettings = quicsetting
+// }
+
+ NetworkType.GRPC.type -> {
+ val grpcSetting = StreamSettingsBean.GrpcSettingsBean()
+ grpcSetting.multiMode = mode == "multi"
+ grpcSetting.serviceName = serviceName.orEmpty()
+ grpcSetting.authority = authority.orEmpty()
+ grpcSetting.idle_timeout = 60
+ grpcSetting.health_check_timeout = 20
+ sni = authority
+ streamSettings.grpcSettings = grpcSetting
+ }
+ }
+ return sni
+ }
+
+ /**
+ * Configures TLS or REALITY security settings for an outbound connection.
+ *
+ * Sets up security-related parameters like certificates, fingerprints, and SNI.
+ *
+ * @param streamSettings The stream settings to configure
+ * @param profileItem The profile containing security configuration
+ * @param sniExt An external SNI value to use if the profile doesn't specify one
+ */
+ fun populateTlsSettings(streamSettings: StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
+ val streamSecurity = profileItem.security.orEmpty()
+ val allowInsecure = profileItem.insecure == true
+ val sni = if (profileItem.sni.isNullOrEmpty()) {
+ when {
+ sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
+ profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server
+ else -> sniExt
+ }
+ } else {
+ profileItem.sni
+ }
+ val fingerprint = profileItem.fingerPrint
+ val alpns = profileItem.alpn
+ val publicKey = profileItem.publicKey
+ val shortId = profileItem.shortId
+ val spiderX = profileItem.spiderX
+
+ streamSettings.security = if (streamSecurity.isEmpty()) null else streamSecurity
+ if (streamSettings.security == null) return
+ val tlsSetting = StreamSettingsBean.TlsSettingsBean(
+ allowInsecure = allowInsecure,
+ serverName = if (sni.isNullOrEmpty()) null else sni,
+ fingerprint = if (fingerprint.isNullOrEmpty()) null else fingerprint,
+ alpn = if (alpns.isNullOrEmpty()) null else alpns.split(",").map { it.trim() }.filter { it.isNotEmpty() },
+ publicKey = if (publicKey.isNullOrEmpty()) null else publicKey,
+ shortId = if (shortId.isNullOrEmpty()) null else shortId,
+ spiderX = if (spiderX.isNullOrEmpty()) null else spiderX,
+ )
+ if (streamSettings.security == AppConfig.TLS) {
+ streamSettings.tlsSettings = tlsSetting
+ streamSettings.realitySettings = null
+ } else if (streamSettings.security == AppConfig.REALITY) {
+ streamSettings.tlsSettings = null
+ streamSettings.realitySettings = tlsSetting
+ }
+ }
+
+ //endregion
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt
index 44a9853b..b98129a7 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/SimpleItemTouchHelperCallback.kt
@@ -107,7 +107,7 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
addUpdateListener { animation ->
val value = animation.animatedValue as Float
viewHolder.itemView.translationX = value
- viewHolder.itemView.alpha = (1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD))
+ viewHolder.itemView.alpha = 1f - abs(value) / (viewHolder.itemView.width * SWIPE_THRESHOLD)
}
interpolator = DecelerateInterpolator()
duration = ANIMATION_DURATION
@@ -144,4 +144,4 @@ class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter
private const val SWIPE_THRESHOLD = 0.25f
private const val ANIMATION_DURATION: Long = 200
}
-}
\ No newline at end of file
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt
index cb9ad811..92c551a6 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/NotificationService.kt
@@ -158,6 +158,7 @@ object NotificationService {
mBuilder = null
speedNotificationJob?.cancel()
speedNotificationJob = null
+ mNotificationManager = null
}
/**
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt
index db12287d..7aecf634 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/QSTileService.kt
@@ -25,14 +25,13 @@ class QSTileService : TileService() {
* @param state The state to set.
*/
fun setState(state: Int) {
+ qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
if (state == Tile.STATE_INACTIVE) {
qsTile?.state = Tile.STATE_INACTIVE
qsTile?.label = getString(R.string.app_name)
- qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
} else if (state == Tile.STATE_ACTIVE) {
qsTile?.state = Tile.STATE_ACTIVE
qsTile?.label = V2RayServiceManager.getRunningServerName()
- qsTile?.icon = Icon.createWithResource(applicationContext, R.drawable.ic_stat_name)
}
qsTile?.updateTile()
@@ -45,7 +44,11 @@ class QSTileService : TileService() {
override fun onStartListening() {
super.onStartListening()
- setState(Tile.STATE_INACTIVE)
+ if (V2RayServiceManager.isRunning()) {
+ setState(Tile.STATE_ACTIVE)
+ } else {
+ setState(Tile.STATE_INACTIVE)
+ }
mMsgReceive = ReceiveMessageHandler(this)
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_ACTIVITY)
ContextCompat.registerReceiver(applicationContext, mMsgReceive, mFilter, Utils.receiverFlags())
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt
index 66d74e86..25fcd1a6 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayProxyOnlyService.kt
@@ -27,7 +27,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
* @return The start mode.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- V2RayServiceManager.startV2rayPoint()
+ V2RayServiceManager.startCoreLoop()
return START_STICKY
}
@@ -36,7 +36,7 @@ class V2RayProxyOnlyService : Service(), ServiceControl {
*/
override fun onDestroy() {
super.onDestroy()
- V2RayServiceManager.stopV2rayPoint()
+ V2RayServiceManager.stopCoreLoop()
}
/**
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt
index d1c17cf1..4f42ca23 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayServiceManager.kt
@@ -15,6 +15,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
+import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.handler.V2rayConfigManager
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.PluginUtil
@@ -23,14 +24,14 @@ import go.Seq
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import libv2ray.CoreCallbackHandler
+import libv2ray.CoreController
import libv2ray.Libv2ray
-import libv2ray.V2RayPoint
-import libv2ray.V2RayVPNServiceSupportsSet
import java.lang.ref.SoftReference
object V2RayServiceManager {
- private val v2rayPoint: V2RayPoint = Libv2ray.newV2RayPoint(V2RayCallback(), Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
+ private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback())
private val mMsgReceive = ReceiveMessageHandler()
private var currentConfig: ProfileItem? = null
@@ -38,7 +39,7 @@ object V2RayServiceManager {
set(value) {
field = value
Seq.setContext(value?.get()?.getService()?.applicationContext)
- Libv2ray.initV2Env(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
+ Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
}
/**
@@ -80,7 +81,7 @@ object V2RayServiceManager {
* Checks if the V2Ray service is running.
* @return True if the service is running, false otherwise.
*/
- fun isRunning() = v2rayPoint.isRunning
+ fun isRunning() = coreController.isRunning
/**
* Gets the name of the currently running server.
@@ -90,15 +91,18 @@ object V2RayServiceManager {
/**
* Starts the context service for V2Ray.
+ * Chooses between VPN service or Proxy-only service based on user settings.
* @param context The context from which the service is started.
*/
private fun startContextService(context: Context) {
- if (v2rayPoint.isRunning) return
+ if (coreController.isRunning) {
+ return
+ }
val guid = MmkvManager.getSelectServer() ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return
if (config.configType != EConfigType.CUSTOM
&& !Utils.isValidUrl(config.server)
- && !Utils.isIpAddress(config.server)
+ && !Utils.isPureIpAddress(config.server.orEmpty())
) return
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) return
@@ -123,18 +127,19 @@ object V2RayServiceManager {
/**
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
- * Starts the V2Ray point.
+ * Starts the V2Ray core service.
*/
- fun startV2rayPoint() {
- val service = getService() ?: return
- val guid = MmkvManager.getSelectServer() ?: return
- val config = MmkvManager.decodeServerConfig(guid) ?: return
- if (v2rayPoint.isRunning) {
- return
+ fun startCoreLoop(): Boolean {
+ if (coreController.isRunning) {
+ return false
}
+
+ val service = getService() ?: return false
+ val guid = MmkvManager.getSelectServer() ?: return false
+ val config = MmkvManager.decodeServerConfig(guid) ?: return false
val result = V2rayConfigManager.getV2rayConfig(service, guid)
if (!result.status)
- return
+ return false
try {
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
@@ -144,39 +149,49 @@ object V2RayServiceManager {
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
+ return false
}
- v2rayPoint.configureFileContent = result.content
- v2rayPoint.domainName = result.domainPort
currentConfig = config
try {
- v2rayPoint.runLoop(MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
+ coreController.startLoop(result.content)
} catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to start V2Ray loop", e)
+ Log.e(AppConfig.TAG, "Failed to start Core loop", e)
+ return false
}
- if (v2rayPoint.isRunning) {
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
- NotificationService.showNotification(currentConfig)
-
- PluginUtil.runPlugin(service, config, result.domainPort)
- } else {
+ if (coreController.isRunning == false) {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
NotificationService.cancelNotification()
+ return false
}
+
+ try {
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
+ NotificationService.showNotification(currentConfig)
+ NotificationService.startSpeedNotification(currentConfig)
+
+ PluginUtil.runPlugin(service, config, result.socksPort)
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to startup service", e)
+ return false
+ }
+ return true
}
/**
- * Stops the V2Ray point.
+ * Stops the V2Ray core service.
+ * Unregisters broadcast receivers, stops notifications, and shuts down plugins.
+ * @return True if the core was stopped successfully, false otherwise.
*/
- fun stopV2rayPoint() {
- val service = getService() ?: return
+ fun stopCoreLoop(): Boolean {
+ val service = getService() ?: return false
- if (v2rayPoint.isRunning) {
+ if (coreController.isRunning) {
CoroutineScope(Dispatchers.IO).launch {
try {
- v2rayPoint.stopLoop()
+ coreController.stopLoop()
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
}
@@ -192,6 +207,8 @@ object V2RayServiceManager {
Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
}
PluginUtil.stopPlugin()
+
+ return true
}
/**
@@ -201,40 +218,52 @@ object V2RayServiceManager {
* @return The statistics value.
*/
fun queryStats(tag: String, link: String): Long {
- return v2rayPoint.queryStats(tag, link)
+ return coreController.queryStats(tag, link)
}
/**
- * Measures the delay for V2Ray.
+ * Measures the connection delay for the current V2Ray configuration.
+ * Tests with primary URL first, then falls back to alternative URL if needed.
+ * Also fetches remote IP information if the delay test was successful.
*/
private fun measureV2rayDelay() {
+ if (coreController.isRunning == false) {
+ return
+ }
+
CoroutineScope(Dispatchers.IO).launch {
val service = getService() ?: return@launch
var time = -1L
- var errstr = ""
- if (v2rayPoint.isRunning) {
- try {
- time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl())
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
- errstr = e.message?.substringAfter("\":") ?: "empty message"
- }
- if (time == -1L) {
- try {
- time = v2rayPoint.measureDelay(SettingsManager.getDelayTestUrl(true))
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
- errstr = e.message?.substringAfter("\":") ?: "empty message"
- }
- }
+ var errorStr = ""
+
+ try {
+ time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
+ errorStr = e.message?.substringAfter("\":") ?: "empty message"
}
- val result = if (time == -1L) {
- service.getString(R.string.connection_test_error, errstr)
- } else {
- service.getString(R.string.connection_test_available, time)
+ if (time == -1L) {
+ try {
+ time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
+ errorStr = e.message?.substringAfter("\":") ?: "empty message"
+ }
}
+ val result = if (time >= 0) {
+ service.getString(R.string.connection_test_available, time)
+ } else {
+ service.getString(R.string.connection_test_error, errorStr)
+ }
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
+
+ // Only fetch IP info if the delay test was successful
+ if (time >= 0) {
+ SpeedtestManager.getRemoteIPInfo()?.let { ip ->
+ MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
+ }
+ }
}
}
@@ -246,10 +275,25 @@ object V2RayServiceManager {
return serviceControl?.get()?.getService()
}
- private class V2RayCallback : V2RayVPNServiceSupportsSet {
+ /**
+ * Core callback handler implementation for handling V2Ray core events.
+ * Handles startup, shutdown, socket protection, and status emission.
+ */
+ private class CoreCallback : CoreCallbackHandler {
+ /**
+ * Called when V2Ray core starts up.
+ * @return 0 for success, any other value for failure.
+ */
+ override fun startup(): Long {
+ return 0
+ }
+
+ /**
+ * Called when V2Ray core shuts down.
+ * @return 0 for success, any other value for failure.
+ */
override fun shutdown(): Long {
val serviceControl = serviceControl?.get() ?: return -1
- // called by go
return try {
serviceControl.stopService()
0
@@ -259,46 +303,25 @@ object V2RayServiceManager {
}
}
- override fun prepare(): Long {
- return 0
- }
-
- override fun protect(l: Long): Boolean {
- val serviceControl = serviceControl?.get() ?: return true
- return serviceControl.vpnProtect(l.toInt())
- }
-
/**
- * Called by Go to emit status.
- * @param l The status code.
- * @param s The status message.
- * @return The status code.
+ * Called when V2Ray core emits status information.
+ * @param l Status code.
+ * @param s Status message.
+ * @return Always returns 0.
*/
override fun onEmitStatus(l: Long, s: String?): Long {
return 0
}
-
- /**
- * Called by Go to set up the service.
- * @param s The setup string.
- * @return The status code.
- */
- override fun setup(s: String): Long {
- val serviceControl = serviceControl?.get() ?: return -1
- return try {
- serviceControl.startService()
- NotificationService.startSpeedNotification(currentConfig)
- 0
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to setup service in callback", e)
- -1
- }
- }
}
+ /**
+ * Broadcast receiver for handling messages sent to the service.
+ * Handles registration, service control, and screen events.
+ */
private class ReceiveMessageHandler : BroadcastReceiver() {
/**
* Handles received broadcast messages.
+ * Processes service control messages and screen state changes.
* @param ctx The context in which the receiver is running.
* @param intent The intent being received.
*/
@@ -306,7 +329,7 @@ object V2RayServiceManager {
val serviceControl = serviceControl?.get() ?: return
when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_REGISTER_CLIENT -> {
- if (v2rayPoint.isRunning) {
+ if (coreController.isRunning) {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
} else {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt
index 180dc24b..3fef1ae1 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayTestService.kt
@@ -32,7 +32,7 @@ class V2RayTestService : Service() {
override fun onCreate() {
super.onCreate()
Seq.setContext(this)
- Libv2ray.initV2Env(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
+ Libv2ray.initCoreEnv(Utils.userAssetPath(this), Utils.getDeviceIdForXUDPBaseKey())
}
/**
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt
index 6bed1c1e..d734c299 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/service/V2RayVpnService.kt
@@ -20,7 +20,6 @@ import androidx.annotation.RequiresApi
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
-import com.v2ray.ang.R
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.MyContextWrapper
@@ -34,12 +33,7 @@ import java.lang.ref.SoftReference
class V2RayVpnService : VpnService(), ServiceControl {
companion object {
private const val VPN_MTU = 1500
- private const val PRIVATE_VLAN4_CLIENT = "10.10.14.1"
- private const val PRIVATE_VLAN4_ROUTER = "10.10.14.2"
- private const val PRIVATE_VLAN6_CLIENT = "fc00::10:10:14:1"
- private const val PRIVATE_VLAN6_ROUTER = "fc00::10:10:14:2"
private const val TUN2SOCKS = "libtun2socks.so"
-
}
private lateinit var mInterface: ParcelFileDescriptor
@@ -105,7 +99,9 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- V2RayServiceManager.startV2rayPoint()
+ if (V2RayServiceManager.startCoreLoop()) {
+ startService()
+ }
return START_STICKY
//return super.onStartCommand(intent, flags, startId)
}
@@ -159,14 +155,15 @@ class V2RayVpnService : VpnService(), ServiceControl {
// If the old interface has exactly the same parameters, use it!
// Configure a builder while parsing the parameters.
val builder = Builder()
+ val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
//val enableLocalDns = defaultDPreference.getPrefBoolean(AppConfig.PREF_LOCAL_DNS_ENABLED, false)
builder.setMtu(VPN_MTU)
- builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
+ builder.addAddress(vpnConfig.ipv4Client, 30)
//builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
val bypassLan = SettingsManager.routingRulesetsBypassLan()
if (bypassLan) {
- resources.getStringArray(R.array.bypass_private_ip_address).forEach {
+ AppConfig.ROUTED_IP_LIST.forEach {
val addr = it.split('/')
builder.addRoute(addr[0], addr[1].toInt())
}
@@ -175,9 +172,10 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true) {
- builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
+ builder.addAddress(vpnConfig.ipv6Client, 126)
if (bypassLan) {
builder.addRoute("2000::", 3) //currently only 1/8 of total ipV6 is in use
+ builder.addRoute("fc00::", 18) //Xray-core default FakeIPv6 Pool
} else {
builder.addRoute("::", 0)
}
@@ -256,10 +254,12 @@ class V2RayVpnService : VpnService(), ServiceControl {
* Starts the tun2socks process with the appropriate parameters.
*/
private fun runTun2socks() {
+ Log.i(AppConfig.TAG, "Start run $TUN2SOCKS")
val socksPort = SettingsManager.getSocksPort()
+ val vpnConfig = SettingsManager.getCurrentVpnInterfaceAddressConfig()
val cmd = arrayListOf(
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
- "--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
+ "--netif-ipaddr", vpnConfig.ipv4Router,
"--netif-netmask", "255.255.255.252",
"--socks-server-addr", "$LOOPBACK:${socksPort}",
"--tunmtu", VPN_MTU.toString(),
@@ -270,7 +270,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6)) {
cmd.add("--netif-ip6addr")
- cmd.add(PRIVATE_VLAN6_ROUTER)
+ cmd.add(vpnConfig.ipv6Router)
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED)) {
val localDnsPort = Utils.parseInt(MmkvManager.decodeSettingsString(AppConfig.PREF_LOCAL_DNS_PORT), AppConfig.PORT_LOCAL_DNS.toInt())
@@ -294,11 +294,11 @@ class V2RayVpnService : VpnService(), ServiceControl {
runTun2socks()
}
}.start()
- Log.i(AppConfig.TAG, process.toString())
+ Log.i(AppConfig.TAG, "$TUN2SOCKS process info : ${process.toString()}")
sendFd()
} catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to start tun2socks process", e)
+ Log.e(AppConfig.TAG, "Failed to start $TUN2SOCKS process", e)
}
}
@@ -309,13 +309,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
private fun sendFd() {
val fd = mInterface.fileDescriptor
val path = File(applicationContext.filesDir, "sock_path").absolutePath
- Log.i(AppConfig.TAG, path)
+ Log.i(AppConfig.TAG, "LocalSocket path : $path")
CoroutineScope(Dispatchers.IO).launch {
var tries = 0
while (true) try {
Thread.sleep(50L shl tries)
- Log.i(AppConfig.TAG, "sendFd tries: $tries")
+ Log.i(AppConfig.TAG, "LocalSocket sendFd tries: $tries")
LocalSocket().use { localSocket ->
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.setFileDescriptorsForSend(arrayOf(fd))
@@ -349,13 +349,13 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
try {
- Log.i(AppConfig.TAG, "tun2socks destroy")
+ Log.i(AppConfig.TAG, "$TUN2SOCKS destroy")
process.destroy()
} catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to destroy tun2socks process", e)
+ Log.e(AppConfig.TAG, "Failed to destroy $TUN2SOCKS process", e)
}
- V2RayServiceManager.stopV2rayPoint()
+ V2RayServiceManager.stopCoreLoop()
if (isForced) {
//stopSelf has to be called ahead of mInterface.close(). otherwise v2ray core cannot be stooped
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt
index 648996bc..1931cb45 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt
@@ -16,6 +16,7 @@ import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
+import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil
@@ -98,11 +99,11 @@ class AboutActivity : BaseActivity() {
}
binding.layoutSoureCcode.setOnClickListener {
- Utils.openUri(this, AppConfig.v2rayNGUrl)
+ Utils.openUri(this, AppConfig.APP_URL)
}
binding.layoutFeedback.setOnClickListener {
- Utils.openUri(this, AppConfig.v2rayNGIssues)
+ Utils.openUri(this, AppConfig.APP_ISSUES_URL)
}
binding.layoutOssLicenses.setOnClickListener {
@@ -116,11 +117,11 @@ class AboutActivity : BaseActivity() {
}
binding.layoutTgChannel.setOnClickListener {
- Utils.openUri(this, AppConfig.TgChannelUrl)
+ Utils.openUri(this, AppConfig.TG_CHANNEL_URL)
}
binding.layoutPrivacyPolicy.setOnClickListener {
- Utils.openUri(this, AppConfig.v2rayNGPrivacyPolicy)
+ Utils.openUri(this, AppConfig.APP_PRIVACY_POLICY)
}
"v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt
new file mode 100644
index 00000000..a9b698c5
--- /dev/null
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt
@@ -0,0 +1,77 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.util.Log
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.lifecycleScope
+import com.v2ray.ang.AppConfig
+import com.v2ray.ang.BuildConfig
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityCheckUpdateBinding
+import com.v2ray.ang.dto.CheckUpdateResult
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.extension.toastError
+import com.v2ray.ang.extension.toastSuccess
+import com.v2ray.ang.handler.MmkvManager
+import com.v2ray.ang.handler.SpeedtestManager
+import com.v2ray.ang.handler.UpdateCheckerManager
+import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.launch
+
+class CheckUpdateActivity : BaseActivity() {
+
+ private val binding by lazy { ActivityCheckUpdateBinding.inflate(layoutInflater) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+
+ title = getString(R.string.update_check_for_update)
+
+ binding.layoutCheckUpdate.setOnClickListener {
+ checkForUpdates(binding.checkPreRelease.isChecked)
+ }
+
+ binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
+ MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
+ }
+ binding.checkPreRelease.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, false)
+
+ "v${BuildConfig.VERSION_NAME} (${SpeedtestManager.getLibVersion()})".also {
+ binding.tvVersion.text = it
+ }
+
+ checkForUpdates(binding.checkPreRelease.isChecked)
+ }
+
+ private fun checkForUpdates(includePreRelease: Boolean) {
+ toast(R.string.update_checking_for_update)
+
+ lifecycleScope.launch {
+ try {
+ val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
+ if (result.hasUpdate) {
+ showUpdateDialog(result)
+ } else {
+ toastSuccess(R.string.update_already_latest_version)
+ }
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
+ toastError(e.message ?: getString(R.string.toast_failure))
+ }
+ }
+ }
+
+ private fun showUpdateDialog(result: CheckUpdateResult) {
+ AlertDialog.Builder(this)
+ .setTitle(getString(R.string.update_new_version_found, result.latestVersion))
+ .setMessage(result.releaseNotes)
+ .setPositiveButton(R.string.update_now) { _, _ ->
+ result.downloadUrl?.let {
+ Utils.openUri(this, it)
+ }
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt
index e6abbba3..0c7584d8 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt
@@ -87,9 +87,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
Action.IMPORT_QR_CODE_CONFIG ->
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
-// Action.IMPORT_QR_CODE_URL ->
-// scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
-
Action.READ_CONTENT_FROM_URI ->
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
@@ -110,8 +107,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
enum class Action {
NONE,
IMPORT_QR_CODE_CONFIG,
-
- //IMPORT_QR_CODE_URL,
READ_CONTENT_FROM_URI,
POST_NOTIFICATIONS
}
@@ -129,12 +124,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
}
-// private val scanQRCodeForUrlToCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
-// if (it.resultCode == RESULT_OK) {
-// importConfigCustomUrl(it.data?.getStringExtra("SCAN_RESULT"))
-// }
-// }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
@@ -325,7 +314,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.import_qrcode -> {
- importQRcode(true)
+ importQRcode()
true
}
@@ -379,26 +368,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
true
}
-// R.id.import_config_custom_clipboard -> {
-// importConfigCustomClipboard()
-// true
-// }
-//
-// R.id.import_config_custom_local -> {
-// importConfigCustomLocal()
-// true
-// }
-//
-// R.id.import_config_custom_url -> {
-// importConfigCustomUrlClipboard()
-// true
-// }
-//
-// R.id.import_config_custom_url_scan -> {
-// importQRcode(false)
-// true
-// }
-
R.id.export_all -> {
exportAll()
true
@@ -462,16 +431,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from qrcode
*/
- private fun importQRcode(forConfig: Boolean): Boolean {
+ private fun importQRcode(): Boolean {
val permission = Manifest.permission.CAMERA
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
- if (forConfig) {
- scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
- } else {
- //scanQRCodeForUrlToCustomConfig.launch(Intent(this, ScannerActivity::class.java))
- }
+ scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
} else {
- pendingAction = Action.IMPORT_QR_CODE_CONFIG//if (forConfig) Action.IMPORT_QR_CODE_CONFIG else Action.IMPORT_QR_CODE_URL
+ pendingAction = Action.IMPORT_QR_CODE_CONFIG
requestPermissionLauncher.launch(permission)
}
return true
@@ -535,77 +500,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
-// private fun importConfigCustomClipboard()
-// : Boolean {
-// try {
-// val configText = Utils.getClipboard(this)
-// if (TextUtils.isEmpty(configText)) {
-// toast(R.string.toast_none_data_clipboard)
-// return false
-// }
-// importCustomizeConfig(configText)
-// return true
-// } catch (e: Exception) {
-// e.printStackTrace()
-// return false
-// }
-// }
-
- /**
- * import config from local config file
- */
-// private fun importConfigCustomLocal(): Boolean {
-// try {
-// showFileChooser()
-// } catch (e: Exception) {
-// e.printStackTrace()
-// return false
-// }
-// return true
-// }
-//
-// private fun importConfigCustomUrlClipboard()
-// : Boolean {
-// try {
-// val url = Utils.getClipboard(this)
-// if (TextUtils.isEmpty(url)) {
-// toast(R.string.toast_none_data_clipboard)
-// return false
-// }
-// return importConfigCustomUrl(url)
-// } catch (e: Exception) {
-// e.printStackTrace()
-// return false
-// }
-// }
-
- /**
- * import config from url
- */
-// private fun importConfigCustomUrl(url: String?): Boolean {
-// try {
-// if (!Utils.isValidUrl(url)) {
-// toast(R.string.toast_invalid_url)
-// return false
-// }
-// lifecycleScope.launch(Dispatchers.IO) {
-// val configText = try {
-// HttpUtil.getUrlContentWithUserAgent(url)
-// } catch (e: Exception) {
-// e.printStackTrace()
-// ""
-// }
-// launch(Dispatchers.Main) {
-// importCustomizeConfig(configText)
-// }
-// }
-// } catch (e: Exception) {
-// e.printStackTrace()
-// return false
-// }
-// return true
-// }
-
/**
* import config from sub
*/
@@ -755,29 +649,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
}
-// /**
-// * import customize config
-// */
-// private fun importCustomizeConfig(server: String?) {
-// try {
-// if (server == null || TextUtils.isEmpty(server)) {
-// toast(R.string.toast_none_data)
-// return
-// }
-// if (mainViewModel.appendCustomConfigServer(server)) {
-// mainViewModel.reloadServerList()
-// toastSuccess(R.string.toast_success)
-// } else {
-// toastError(R.string.toast_failure)
-// }
-// //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
-// } catch (e: Exception) {
-// ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
-// e.printStackTrace()
-// return
-// }
-// }
-
private fun setTestState(content: String?) {
binding.tvTestState.text = content
}
@@ -812,8 +683,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
.putExtra("isRunning", mainViewModel.isRunning.value == true)
)
- R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
+ R.id.promotion -> Utils.openUri(this, "${Utils.decode(AppConfig.APP_PROMOTION_URL)}?t=${System.currentTimeMillis()}")
R.id.logcat -> startActivity(Intent(this, LogcatActivity::class.java))
+ R.id.check_for_update -> startActivity(Intent(this, CheckUpdateActivity::class.java))
R.id.about -> startActivity(Intent(this, AboutActivity::class.java))
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt
index 179f70ae..e7ea6211 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt
@@ -27,6 +27,7 @@ import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.service.V2RayServiceManager
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -220,10 +221,15 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter? = null
@@ -86,6 +85,10 @@ class PerAppProxyActivity : BaseActivity() {
MmkvManager.encodeSettings(AppConfig.PREF_BYPASS_APPS, isChecked)
}
binding.switchBypassApps.isChecked = MmkvManager.decodeSettingsBool(AppConfig.PREF_BYPASS_APPS, false)
+
+ binding.layoutSwitchBypassAppsTips.setOnClickListener {
+ Toasty.info(this, R.string.summary_pref_per_app_proxy, Toast.LENGTH_LONG, true).show()
+ }
}
override fun onPause() {
@@ -157,7 +160,7 @@ class PerAppProxyActivity : BaseActivity() {
toast(R.string.msg_downloading_content)
binding.pbWaiting.show()
- val url = AppConfig.androidpackagenamelistUrl
+ val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000)
if (content.isNullOrEmpty()) {
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt
index 104e5049..e585cff8 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt
@@ -7,8 +7,6 @@ import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
-import android.view.View
-import android.widget.AdapterView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
@@ -67,15 +65,9 @@ class RoutingSettingActivity : BaseActivity() {
mItemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter))
mItemTouchHelper?.attachToRecyclerView(binding.recyclerView)
- val found = Utils.arrayFind(routing_domain_strategy, MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: "")
- found.let { binding.spDomainStrategy.setSelection(if (it >= 0) it else 0) }
- binding.spDomainStrategy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
- override fun onNothingSelected(parent: AdapterView<*>?) {
- }
-
- override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
- MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, routing_domain_strategy[position])
- }
+ binding.tvDomainStrategySummary.text = getDomainStrategy()
+ binding.layoutDomainStrategy.setOnClickListener {
+ setDomainStrategy()
}
}
@@ -98,6 +90,22 @@ class RoutingSettingActivity : BaseActivity() {
else -> super.onOptionsItemSelected(item)
}
+ private fun getDomainStrategy(): String {
+ return MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY) ?: routing_domain_strategy.first()
+ }
+
+ private fun setDomainStrategy() {
+ android.app.AlertDialog.Builder(this).setItems(routing_domain_strategy.asList().toTypedArray()) { _, i ->
+ try {
+ val value = routing_domain_strategy[i]
+ MmkvManager.encodeSettings(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY, value)
+ binding.tvDomainStrategySummary.text = value
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to set domain strategy", e)
+ }
+ }.show()
+ }
+
private fun importPredefined() {
AlertDialog.Builder(this).setItems(preset_rulesets.asList().toTypedArray()) { _, i ->
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt
index 58b80d29..e9bdad66 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt
@@ -2,7 +2,6 @@ package com.v2ray.ang.ui
import android.os.Bundle
import android.text.TextUtils
-import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -19,7 +18,6 @@ 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
-import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R
import com.v2ray.ang.dto.EConfigType
@@ -328,7 +326,7 @@ class ServerActivity : BaseActivity() {
et_preshared_key?.text = Utils.getEditable(config.preSharedKey.orEmpty())
et_reserved1?.text = Utils.getEditable(config.reserved ?: "0,0,0")
et_local_address?.text = Utils.getEditable(
- config.localAddress ?: "$WIREGUARD_LOCAL_ADDRESS_V4,$WIREGUARD_LOCAL_ADDRESS_V6"
+ config.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4
)
et_local_mtu?.text = Utils.getEditable(config.mtu?.toString() ?: WIREGUARD_LOCAL_MTU)
} else if (config.configType == EConfigType.HYSTERIA2) {
@@ -421,7 +419,7 @@ class ServerActivity : BaseActivity() {
et_public_key?.text = null
et_reserved1?.text = Utils.getEditable("0,0,0")
et_local_address?.text =
- Utils.getEditable("${WIREGUARD_LOCAL_ADDRESS_V4},${WIREGUARD_LOCAL_ADDRESS_V6}")
+ Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4)
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
return true
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt
index 9423310c..6af64e3a 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SettingsActivity.kt
@@ -44,6 +44,7 @@ class SettingsActivity : BaseActivity() {
private val localDnsPort by lazy { findPreference(AppConfig.PREF_LOCAL_DNS_PORT) }
private val vpnDns by lazy { findPreference(AppConfig.PREF_VPN_DNS) }
private val vpnBypassLan by lazy { findPreference(AppConfig.PREF_VPN_BYPASS_LAN) }
+ private val vpnInterfaceAddress by lazy { findPreference(AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX) }
private val mux by lazy { findPreference(AppConfig.PREF_MUX_ENABLED) }
private val muxConcurrency by lazy { findPreference(AppConfig.PREF_MUX_CONCURRENCY) }
@@ -161,7 +162,7 @@ class SettingsActivity : BaseActivity() {
}
delayTestUrl?.setOnPreferenceChangeListener { _, any ->
val nval = any as String
- delayTestUrl?.summary = if (nval == "") AppConfig.DelayTestUrl else nval
+ delayTestUrl?.summary = if (nval == "") AppConfig.DELAY_TEST_URL else nval
true
}
mode?.setOnPreferenceChangeListener { _, newValue ->
@@ -202,7 +203,7 @@ class SettingsActivity : BaseActivity() {
remoteDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_REMOTE_DNS, AppConfig.DNS_PROXY)
domesticDns?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DOMESTIC_DNS, AppConfig.DNS_DIRECT)
dnsHosts?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
- delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DelayTestUrl)
+ delayTestUrl?.summary = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL, AppConfig.DELAY_TEST_URL)
initSharedPreference()
}
@@ -249,12 +250,14 @@ class SettingsActivity : BaseActivity() {
listOf(
AppConfig.PREF_VPN_BYPASS_LAN,
+ AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
AppConfig.PREF_MUX_XUDP_QUIC,
AppConfig.PREF_FRAGMENT_PACKETS,
AppConfig.PREF_LANGUAGE,
AppConfig.PREF_UI_MODE_NIGHT,
AppConfig.PREF_LOGLEVEL,
+ AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
AppConfig.PREF_MODE
).forEach { key ->
if (MmkvManager.decodeSettingsString(key) != null) {
@@ -273,7 +276,7 @@ class SettingsActivity : BaseActivity() {
localDnsPort?.isEnabled = vpn
vpnDns?.isEnabled = vpn
vpnBypassLan?.isEnabled = vpn
- vpn
+ vpnInterfaceAddress?.isEnabled = vpn
if (vpn) {
updateLocalDns(
MmkvManager.decodeSettingsBool(
@@ -364,6 +367,6 @@ class SettingsActivity : BaseActivity() {
}
fun onModeHelpClicked(view: View) {
- Utils.openUri(this, AppConfig.v2rayNGWikiMode)
+ Utils.openUri(this, AppConfig.APP_WIKI_MODE)
}
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt
index 3f5f9280..f85382f1 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt
@@ -6,6 +6,7 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
+import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivitySubEditBinding
import com.v2ray.ang.dto.SubscriptionItem
@@ -46,6 +47,7 @@ class SubEditActivity : BaseActivity() {
binding.etFilter.text = Utils.getEditable(subItem.filter)
binding.chkEnable.isChecked = subItem.enabled
binding.autoUpdateCheck.isChecked = subItem.autoUpdate
+ binding.allowInsecureUrl.isChecked = subItem.allowInsecureUrl
binding.etPreProfile.text = Utils.getEditable(subItem.prevProfile)
binding.etNextProfile.text = Utils.getEditable(subItem.nextProfile)
return true
@@ -77,6 +79,7 @@ class SubEditActivity : BaseActivity() {
subItem.autoUpdate = binding.autoUpdateCheck.isChecked
subItem.prevProfile = binding.etPreProfile.text.toString()
subItem.nextProfile = binding.etNextProfile.text.toString()
+ subItem.allowInsecureUrl = binding.allowInsecureUrl.isChecked
if (TextUtils.isEmpty(subItem.remarks)) {
toast(R.string.sub_setting_remarks)
@@ -90,7 +93,9 @@ class SubEditActivity : BaseActivity() {
if (!Utils.isValidSubUrl(subItem.url)) {
toast(R.string.toast_insecure_url_protocol)
- //return false
+ if (!subItem.allowInsecureUrl) {
+ return false
+ }
}
}
@@ -105,19 +110,28 @@ class SubEditActivity : BaseActivity() {
*/
private fun deleteServer(): Boolean {
if (editSubId.isNotEmpty()) {
- AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
- .setPositiveButton(android.R.string.ok) { _, _ ->
- lifecycleScope.launch(Dispatchers.IO) {
- MmkvManager.removeSubscription(editSubId)
- launch(Dispatchers.Main) {
- finish()
+ if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
+ AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ lifecycleScope.launch(Dispatchers.IO) {
+ MmkvManager.removeSubscription(editSubId)
+ launch(Dispatchers.Main) {
+ finish()
+ }
}
}
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
+ // do nothing
+ }
+ .show()
+ } else {
+ lifecycleScope.launch(Dispatchers.IO) {
+ MmkvManager.removeSubscription(editSubId)
+ launch(Dispatchers.Main) {
+ finish()
+ }
}
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- // do nothing
- }
- .show()
+ }
}
return true
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt
index bb364c1b..cc2d5404 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingRecyclerAdapter.kt
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
@@ -20,6 +21,8 @@ import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter {
@@ -46,6 +49,10 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
)
}
+ holder.itemSubSettingBinding.layoutRemove.setOnClickListener {
+ removeSubscription(subId, position)
+ }
+
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if (!it.isPressed) return@setOnCheckedChangeListener
subItem.enabled = isChecked
@@ -54,9 +61,11 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
}
if (TextUtils.isEmpty(subItem.url)) {
+ holder.itemSubSettingBinding.layoutUrl.visibility = View.GONE
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
holder.itemSubSettingBinding.chkEnable.visibility = View.INVISIBLE
} else {
+ holder.itemSubSettingBinding.layoutUrl.visibility = View.VISIBLE
holder.itemSubSettingBinding.layoutShare.visibility = View.VISIBLE
holder.itemSubSettingBinding.chkEnable.visibility = View.VISIBLE
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
@@ -90,6 +99,32 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
}
}
+ private fun removeSubscription(subId: String, position: Int) {
+ if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE) == true) {
+ AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ removeSubscriptionSub(subId, position)
+ }
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
+ //do noting
+ }
+ .show()
+ } else {
+ removeSubscriptionSub(subId, position)
+ }
+ }
+
+ private fun removeSubscriptionSub(subId: String, position: Int) {
+ mActivity.lifecycleScope.launch(Dispatchers.IO) {
+ MmkvManager.removeSubscription(subId)
+ launch(Dispatchers.Main) {
+ notifyItemRemoved(position)
+ notifyItemRangeChanged(position, mActivity.subscriptions.size)
+ mActivity.refreshData()
+ }
+ }
+ }
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
return MainViewHolder(
ItemRecyclerSubSettingBinding.inflate(
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt
index f911bd89..efc0a39c 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt
@@ -21,9 +21,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
-import com.v2ray.ang.databinding.ActivitySubSettingBinding
+import com.v2ray.ang.databinding.ActivityUserAssetBinding
import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding
import com.v2ray.ang.dto.AssetUrlItem
+import com.v2ray.ang.extension.concatUrl
import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
@@ -42,8 +43,7 @@ import java.text.DateFormat
import java.util.Date
class UserAssetActivity : BaseActivity() {
- private val binding by lazy { ActivitySubSettingBinding.inflate(layoutInflater) }
-
+ private val binding by lazy { ActivityUserAssetBinding.inflate(layoutInflater) }
val extDir by lazy { File(Utils.userAssetPath(this)) }
val builtInGeoFiles = arrayOf("geosite.dat", "geoip.dat")
@@ -90,6 +90,11 @@ class UserAssetActivity : BaseActivity() {
binding.recyclerView.layoutManager = LinearLayoutManager(this)
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
binding.recyclerView.adapter = UserAssetAdapter()
+
+ binding.tvGeoFilesSourcesSummary.text = getGeoFilesSources()
+ binding.layoutGeoFilesSources.setOnClickListener {
+ setGeoFilesSources()
+ }
}
override fun onResume() {
@@ -111,6 +116,22 @@ class UserAssetActivity : BaseActivity() {
else -> super.onOptionsItemSelected(item)
}
+ private fun getGeoFilesSources(): String {
+ return MmkvManager.decodeSettingsString(AppConfig.PREF_GEO_FILES_SOURCES) ?: AppConfig.GEO_FILES_SOURCES.first()
+ }
+
+ private fun setGeoFilesSources() {
+ AlertDialog.Builder(this).setItems(AppConfig.GEO_FILES_SOURCES.toTypedArray()) { _, i ->
+ try {
+ val value = AppConfig.GEO_FILES_SOURCES[i]
+ MmkvManager.encodeSettings(AppConfig.PREF_GEO_FILES_SOURCES, value)
+ binding.tvGeoFilesSourcesSummary.text = value
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to set geo files sources", e)
+ }
+ }.show()
+ }
+
private fun showFileChooser() {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
@@ -264,7 +285,8 @@ class UserAssetActivity : BaseActivity() {
list.add(
Utils.getUuid() to AssetUrlItem(
it,
- AppConfig.GeoUrl + it
+ String.format(AppConfig.GITHUB_DOWNLOAD_URL, getGeoFilesSources()).concatUrl(it),
+ locked = true
)
)
}
@@ -315,7 +337,7 @@ class UserAssetActivity : BaseActivity() {
holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
}
- if (item.second.remarks in builtInGeoFiles && item.second.url == AppConfig.GeoUrl + item.second.remarks) {
+ if (item.second.locked == true) {
holder.itemUserAssetBinding.layoutEdit.visibility = GONE
//holder.itemUserAssetBinding.layoutRemove.visibility = GONE
} else {
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt
index 956000de..577698ea 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/AppManagerUtil.kt
@@ -25,7 +25,7 @@ object AppManagerUtil {
val appName = applicationInfo.loadLabel(packageManager).toString()
val appIcon = applicationInfo.loadIcon(packageManager) ?: continue
- val isSystemApp = (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) > 0
+ val isSystemApp = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM > 0
val appInfo = AppInfo(appName, pkg.packageName, appIcon, isSystemApp, 0)
apps.add(appInfo)
@@ -33,4 +33,8 @@ object AppManagerUtil {
return@withContext apps
}
-}
\ No newline at end of file
+
+ fun getLastUpdateTime(context: Context): Long =
+ context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
+
+}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
index b2a64777..7172728e 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt
@@ -7,20 +7,26 @@ import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode
import com.v2ray.ang.util.Utils.urlDecode
import java.io.IOException
-import java.net.*
-import java.util.*
+import java.net.HttpURLConnection
+import java.net.IDN
+import java.net.Inet6Address
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.Proxy
+import java.net.URL
object HttpUtil {
-
/**
- * Converts a URL string to its ASCII representation.
+ * Converts the domain part of a URL string to its IDN (Punycode, ASCII Compatible Encoding) format.
*
- * @param str The URL string to convert.
- * @return The ASCII representation of the URL.
+ * For example, a URL like "https://例子.中国/path" will be converted to "https://xn--fsqu00a.xn--fiqs8s/path".
+ *
+ * @param str The URL string to convert (can contain non-ASCII characters in the domain).
+ * @return The URL string with the domain part converted to ASCII-compatible (Punycode) format.
*/
- fun idnToASCII(str: String): String {
- val url = URI(str)
+ fun toIdnUrl(str: String): String {
+ val url = URL(str)
val host = url.host
val asciiHost = IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED)
if (host != asciiHost) {
@@ -30,6 +36,67 @@ object HttpUtil {
}
}
+ /**
+ * Converts a Unicode domain name to its IDN (Punycode, ASCII Compatible Encoding) format.
+ * If the input is an IP address or already an ASCII domain, returns the original string.
+ *
+ * @param domain The domain string to convert (can include non-ASCII internationalized characters).
+ * @return The domain in ASCII-compatible (Punycode) format, or the original string if input is an IP or already ASCII.
+ */
+ fun toIdnDomain(domain: String): String {
+ // Return as is if it's a pure IP address (IPv4 or IPv6)
+ if (Utils.isPureIpAddress(domain)) {
+ return domain
+ }
+
+ // Return as is if already ASCII (English domain or already punycode)
+ if (domain.all { it.code < 128 }) {
+ return domain
+ }
+
+ // Otherwise, convert to ASCII using IDN
+ return IDN.toASCII(domain, IDN.ALLOW_UNASSIGNED)
+ }
+
+ /**
+ * Resolves a hostname to an IP address, returns original input if it's already an IP
+ *
+ * @param host The hostname or IP address to resolve
+ * @param ipv6Preferred Whether to prefer IPv6 addresses, defaults to false
+ * @return The resolved IP address or the original input (if it's already an IP or resolution fails)
+ */
+ fun resolveHostToIP(host: String, ipv6Preferred: Boolean = false): List? {
+ try {
+ // If it's already an IP address, return it as a list
+ if (Utils.isPureIpAddress(host)) {
+ return null
+ }
+
+ // Get all IP addresses
+ val addresses = InetAddress.getAllByName(host)
+ if (addresses.isEmpty()) {
+ return null
+ }
+
+ // Sort addresses based on preference
+ val sortedAddresses = if (ipv6Preferred) {
+ addresses.sortedWith(compareByDescending { it is Inet6Address })
+ } else {
+ addresses.sortedWith(compareBy { it is Inet6Address })
+ }
+
+ val ipList = sortedAddresses.mapNotNull { it.hostAddress }
+
+ Log.i(AppConfig.TAG, "Resolved IPs for $host: ${ipList.joinToString()}")
+
+ return ipList
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to resolve host to IP", e)
+ return null
+ }
+ }
+
+
/**
* Retrieves the content of a URL as a string.
*
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt
index bcf30a3b..2b9f71aa 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PluginUtil.kt
@@ -23,20 +23,24 @@ object PluginUtil {
*
* @param context The context to use.
* @param config The profile configuration.
- * @param domainPort The domain and port information.
+ * @param socksPort The port information.
*/
- fun runPlugin(context: Context, config: ProfileItem?, domainPort: String?) {
+ fun runPlugin(context: Context, config: ProfileItem?, socksPort: Int?) {
Log.i(AppConfig.TAG, "Starting plugin execution")
if (config == null) {
Log.w(AppConfig.TAG, "Cannot run plugin: config is null")
return
}
-
+
try {
if (config.configType == EConfigType.HYSTERIA2) {
+ if (socksPort == null) {
+ Log.w(AppConfig.TAG, "Cannot run plugin: socksPort is null")
+ return
+ }
Log.i(AppConfig.TAG, "Running Hysteria2 plugin")
- val configFile = genConfigHy2(context, config, domainPort) ?: return
+ val configFile = genConfigHy2(context, config, socksPort) ?: return
val cmd = genCmdHy2(context, configFile)
procService.runProcess(context, cmd)
@@ -66,7 +70,7 @@ object PluginUtil {
if (config?.configType?.equals(EConfigType.HYSTERIA2) == true) {
val socksPort = Utils.findFreePort(listOf(0))
- val configFile = genConfigHy2(context, config, "0:${socksPort}") ?: return retFailure
+ val configFile = genConfigHy2(context, config, socksPort) ?: return retFailure
val cmd = genCmdHy2(context, configFile)
val proc = ProcessService()
@@ -85,14 +89,12 @@ object PluginUtil {
*
* @param context The context to use.
* @param config The profile configuration.
- * @param domainPort The domain and port information.
+ * @param socksPort The port information.
* @return The generated configuration file.
*/
- private fun genConfigHy2(context: Context, config: ProfileItem, domainPort: String?): File? {
+ private fun genConfigHy2(context: Context, config: ProfileItem, socksPort: Int): File? {
Log.i(AppConfig.TAG, "runPlugin $HYSTERIA2")
- val socksPort = domainPort?.split(":")?.last()
- .let { if (it.isNullOrEmpty()) return null else it.toInt() }
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return null
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt
index d83a9989..148ce4ec 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt
@@ -17,10 +17,12 @@ import android.webkit.URLUtil
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.v2ray.ang.AppConfig
-import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
+import com.v2ray.ang.BuildConfig
import java.io.IOException
+import java.net.InetAddress
import java.net.ServerSocket
+import java.net.URI
import java.net.URLDecoder
import java.net.URLEncoder
import java.util.Locale
@@ -196,6 +198,21 @@ object Utils {
return isIpv4Address(value) || isIpv6Address(value)
}
+ /**
+ * Check if a string is a valid domain name.
+ *
+ * A valid domain name must not be an IP address and must be a valid URL format.
+ *
+ * @param input The string to check.
+ * @return True if the string is a valid domain name, false otherwise.
+ */
+ fun isDomainName(input: String?): Boolean {
+ if (input.isNullOrEmpty()) return false
+
+ // Must not be an IP address and must be a valid URL format
+ return !isPureIpAddress(input) && isValidUrl(input)
+ }
+
/**
* Check if a string is a valid IPv4 address.
*
@@ -461,13 +478,23 @@ object Utils {
fun isValidSubUrl(value: String?): Boolean {
if (value.isNullOrEmpty()) return false
- return try {
- URLUtil.isHttpsUrl(value) ||
- (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK))
+ try {
+ if (URLUtil.isHttpsUrl(value)) return true
+ if (URLUtil.isHttpUrl(value)) {
+ if (value.contains(LOOPBACK)) return true
+
+ //Check private ip address
+ val uri = URI(fixIllegalUrl(value))
+ if (isIpAddress(uri.host)) {
+ AppConfig.PRIVATE_IP_LIST.forEach {
+ if (isIpInCidr(uri.host, it)) return true
+ }
+ }
+ }
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to validate subscription URL", e)
- false
}
+ return false
}
/**
@@ -486,7 +513,58 @@ object Utils {
*
* @return True if the package is Xray, false otherwise.
*/
- fun isXray(): Boolean = ANG_PACKAGE.startsWith("com.v2ray.ang")
+ fun isXray(): Boolean = BuildConfig.APPLICATION_ID.startsWith("com.v2ray.ang")
+ /**
+ * Check if it is the Google Play version.
+ *
+ * @return True if the package is Google Play, false otherwise.
+ */
+ fun isGoogleFlavor(): Boolean = BuildConfig.FLAVOR == "playstore"
+
+ /**
+ * Converts an InetAddress to its long representation
+ *
+ * @param ip The InetAddress to convert
+ * @return The long representation of the IP address
+ */
+ private fun inetAddressToLong(ip: InetAddress): Long {
+ val bytes = ip.address
+ var result: Long = 0
+ for (i in bytes.indices) {
+ result = result shl 8 or (bytes[i].toInt() and 0xff).toLong()
+ }
+ return result
+ }
+
+ /**
+ * Check if an IP address is within a CIDR range
+ *
+ * @param ip The IP address to check
+ * @param cidr The CIDR notation range (e.g., "192.168.1.0/24")
+ * @return True if the IP is within the CIDR range, false otherwise
+ */
+ fun isIpInCidr(ip: String, cidr: String): Boolean {
+ try {
+ if (!isIpAddress(ip)) return false
+
+ // Parse CIDR (e.g., "192.168.1.0/24")
+ val (cidrIp, prefixLen) = cidr.split("/")
+ val prefixLength = prefixLen.toInt()
+
+ // Convert IP and CIDR's IP portion to Long
+ val ipLong = inetAddressToLong(InetAddress.getByName(ip))
+ val cidrIpLong = inetAddressToLong(InetAddress.getByName(cidrIp))
+
+ // Calculate subnet mask (e.g., /24 → 0xFFFFFF00)
+ val mask = if (prefixLength == 0) 0L else (-1L shl (32 - prefixLength))
+
+ // Check if they're in the same subnet
+ return (ipLong and mask) == (cidrIpLong and mask)
+ } catch (e: Exception) {
+ Log.e(AppConfig.TAG, "Failed to check if IP is in CIDR", e)
+ return false
+ }
+ }
}
diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt
index d78b1307..7ac5d60f 100644
--- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt
+++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/SettingsViewModel.kt
@@ -41,6 +41,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_MODE,
AppConfig.PREF_VPN_DNS,
AppConfig.PREF_VPN_BYPASS_LAN,
+ AppConfig.PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX,
AppConfig.PREF_REMOTE_DNS,
AppConfig.PREF_DOMESTIC_DNS,
AppConfig.PREF_DNS_HOSTS,
@@ -48,6 +49,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
AppConfig.PREF_LOCAL_DNS_PORT,
AppConfig.PREF_SOCKS_PORT,
AppConfig.PREF_LOGLEVEL,
+ AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD,
AppConfig.PREF_LANGUAGE,
AppConfig.PREF_UI_MODE_NIGHT,
AppConfig.PREF_ROUTING_DOMAIN_STRATEGY,
diff --git a/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml b/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml
new file mode 100644
index 00000000..5a7c2fd0
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable-night/ic_check_update_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml b/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml
new file mode 100644
index 00000000..b0f163c6
--- /dev/null
+++ b/V2rayNG/app/src/main/res/drawable/ic_check_update_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml
index 55db5240..0fc67600 100644
--- a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml
+++ b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml
@@ -5,7 +5,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
- android:orientation="vertical">
+ android:orientation="vertical"
+ tools:context=".ui.PerAppProxyActivity">
+
+
+
+
+
+
@@ -83,7 +101,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
- app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
- tools:context=".ui.PerAppProxyActivity" />
+ app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" />
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/activity_check_update.xml b/V2rayNG/app/src/main/res/layout/activity_check_update.xml
new file mode 100644
index 00000000..29345075
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_check_update.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml b/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml
index e04cff2a..53a49fec 100644
--- a/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml
+++ b/V2rayNG/app/src/main/res/layout/activity_routing_edit.xml
@@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
- tools:context=".ui.SubSettingActivity">
+ tools:context=".ui.RoutingEditActivity">
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center|start"
+ android:orientation="vertical"
+ android:padding="@dimen/padding_spacing_dp16">
+ android:text="@string/routing_settings_domain_strategy"
+ android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
-
+ android:maxLines="1"
+ android:textAppearance="@style/TextAppearance.AppCompat.Small" />
+ tools:context=".ui.SubEditActivity">
@@ -138,6 +138,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml
index aa8d9812..47c2b4c5 100644
--- a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml
+++ b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml
@@ -15,97 +15,144 @@
android:clickable="true"
android:focusable="true"
android:gravity="center"
- android:nextFocusRight="@+id/layout_edit"
+ android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp8">
-
-
-
-
-
-
-
-
+ android:orientation="vertical">
+ android:paddingStart="@dimen/padding_spacing_dp8">
-
+
+ android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+ android:orientation="horizontal"
+ android:paddingStart="@dimen/padding_spacing_dp8"
+ android:paddingEnd="@dimen/padding_spacing_dp8">
-
+
+
+
+
+
+
+ android:layout_marginTop="@dimen/padding_spacing_dp8"
+ android:orientation="horizontal">
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/menu/menu_drawer.xml b/V2rayNG/app/src/main/res/menu/menu_drawer.xml
index c134759d..4e204e62 100644
--- a/V2rayNG/app/src/main/res/menu/menu_drawer.xml
+++ b/V2rayNG/app/src/main/res/menu/menu_drawer.xml
@@ -35,6 +35,10 @@
android:id="@+id/logcat"
android:icon="@drawable/ic_logcat_24dp"
android:title="@string/title_logcat" />
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- الكتابة يدويًا [Trojan]
الكتابة يدويًا [Wireguard]
Type manually[Hysteria2]
- تكوين مخصص
- استيراد تكوين مخصص من الحافظة
- استيراد تكوين مخصص من الجهاز
- استيراد تكوين مخصص من عنوان URL
- استيراد تكوين مخصص مسح عنوان URL
تأكيد الحذف؟
Please test before deleting! Confirm delete ?
ملاحظات
@@ -127,6 +122,7 @@
إضافة عنوان URL للأصل
الملف غير موجود
الملاحظات موجودة بالفعل
+ Geo files source (optional)
جار التحميل
@@ -145,6 +141,7 @@
الإعدادات
إعدادات متقدمة
+ إعدادات النواة
إعدادات VPN
الوكيل لكل تطبيق
عام: التطبيق المحدد هو وكيل، غير المحدد اتصال مباشر؛ \nوضع التجاوز: التطبيق المحدد متصل مباشرة، غير المحدد وكيل. \nخيار تحديد تطبيق الوكيل تلقائيًا في القائمة
@@ -185,6 +182,8 @@
VPN DNS (IPv4/v6 فقط)
Does VPN bypass LAN
+ VPN Interface Address
+
DNS المحلي (اختياري)
DNS
@@ -242,6 +241,7 @@
فاصل التحديث التلقائي (بالدقائق، الحد الأدنى للقيمة 15)
مستوى السجل
+ Outbound domain pre-resolve method
الوضع
انقر هنا للحصول على مزيد من المساعدة
اللغة
@@ -262,9 +262,10 @@
Remarks regular filter
تفعيل التحديث
تفعيل التحديث التلقائي
- Previous proxy remarks
- Next proxy remarks
- The remarks exists and is unique
+ Allow insecure HTTP address
+ Previous proxy configuration remarks
+ Next proxy configuration remarks
+ The configuration remarks exists and is unique
تحديث الاشتراك (أول خطوة)
Tcping لجميع الإعدادات
اختبر جميع الإعدادات (3)
@@ -314,6 +315,13 @@
فاصل الجزء (الحد الأدنى - الحد الأقصى)
تفعيل الجزء
+ Check for update
+ Already on the latest version
+ New version found: %s
+ Update now
+ Check Pre-release
+ Checking for update…
+
- رمز استجابة سريعة (QRcode)
- تصدير إلى الحافظة
@@ -350,4 +358,10 @@
- Not Bypass
+
+ - Do not resolve
+ - Resolve and add to DNS Hosts
+ - Resolve and replace domain
+
+
diff --git a/V2rayNG/app/src/main/res/values-bn/strings.xml b/V2rayNG/app/src/main/res/values-bn/strings.xml
index 5f37e6de..f36c9d3a 100644
--- a/V2rayNG/app/src/main/res/values-bn/strings.xml
+++ b/V2rayNG/app/src/main/res/values-bn/strings.xml
@@ -35,11 +35,6 @@
ম্যানুয়ালি টাইপ করুন [Trojan]
ম্যানুয়ালি টাইপ করুন [Wireguard]
Type manually[Hysteria2]
- কাস্টম কনফিগারেশন
- ক্লিপবোর্ড থেকে কাস্টম কনফিগারেশন আমদানি করুন
- স্থানীয়ভাবে কাস্টম কনফিগারেশন আমদানি করুন
- URL থেকে কাস্টম কনফিগারেশন আমদানি করুন
- কাস্টম কনফিগারেশন স্ক্যান URL আমদানি করুন
মুছে ফেলুন নিশ্চিত করুন?
Please test before deleting! Confirm delete ?
মন্তব্য
@@ -126,6 +121,7 @@
অ্যাসেট URL যোগ করুন
ফাইল খুঁজে পাওয়া যায়নি
মন্তব্য ইতিমধ্যে বিদ্যমান
+ Geo files source (optional)
লোড হচ্ছে
@@ -143,6 +139,7 @@
সেটিংস
এডভান্সড সেটিংস
+ কোর সেটিংস
VPN সেটিংস
প্রতি-অ্যাপ প্রক্সি
সাধারণ: চেকড অ্যাপ প্রক্সি, আনচেকড সরাসরি সংযোগ; \nবাইপাস মোড: চেকড অ্যাপ সরাসরি সংযুক্ত, আনচেকড প্রক্সি। \nমেনুতে প্রক্সি অ্যাপ্লিকেশন স্বয়ংক্রিয়ভাবে নির্বাচন করার বিকল্প
@@ -185,6 +182,8 @@
VPN DNS (শুধুমাত্র IPv4/v6)
Does VPN bypass LAN
+ VPN Interface Address
+
ঘরোয়া DNS (ঐচ্ছিক)
DNS
@@ -242,6 +241,7 @@
অটো আপডেট ইন্টারভ্যাল (মিনিট, সর্বনিম্ন মান ১৫)
লগ স্তর
+ Outbound domain pre-resolve method
মোড
আরো সাহায্যের জন্য ক্লিক করুন
ভাষা
@@ -262,9 +262,10 @@
Remarks regular filter
আপডেট সক্রিয় করুন
স্বয়ংক্রিয় আপডেট সক্রিয় করুন
- Previous proxy remarks
- Next proxy remarks
- The remarks exists and is unique
+ Allow insecure HTTP address
+ Previous proxy configuration remarks
+ Next proxy configuration remarks
+ The configuration remarks exists and is unique
সাবস্ক্রিপশন আপডেট
সব কনফিগারেশন TCPing
সব কনফিগারেশন প্রকৃত বিলম্ব
@@ -313,6 +314,13 @@
ফ্র্যাগমেন্ট ইন্টারভ্যাল (ন্যূনতম-সর্বাধিক)
ফ্র্যাগমেন্ট সক্রিয় করুন
+ Check for update
+ Already on the latest version
+ New version found: %s
+ Update now
+ Check Pre-release
+ Checking for update…
+
- QR কোড
- ক্লিপবোর্ডে রপ্তানি করুন
@@ -355,4 +363,10 @@
- Not Bypass
+
+ - Do not resolve
+ - Resolve and add to DNS Hosts
+ - Resolve and replace domain
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml
index 65d501b1..90e5cc26 100644
--- a/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml
+++ b/V2rayNG/app/src/main/res/values-bqi-rIR/strings.xml
@@ -7,9 +7,9 @@
بستن نومگه کشاری
مووفقیت من جاگورویی داده
جاگورویی داده ٱنجوم نگرؽڌ
- Please pull down to refresh!
+ سی وانۊ کردن، بکشینس بلم!
-
+ ↓
واڌاشتن
گرؽڌن موجوز مومکن نؽڌ
گرؽڌن موجوز وارسۊوی مومکن نؽڌ
@@ -26,7 +26,7 @@
پاک کردن کانفیگ
و من ٱووردن کانفیگ ز QRcode
و من ٱووردن کانفیگ ز کلیپ بورد
- Import config from locally
+ و من ٱووردن کانفیگ ز مهلی
هؽل دستی[VMess]
هؽل دستی[VLESS]
هؽل دستی[Shadowsocks]
@@ -35,17 +35,12 @@
هؽل دستی[Trojan]
هؽل دستی[Wireguard]
هؽل دستی[Hysteria2]
- کانفیگ سفارشی
- کانفیگ سفارشین ز کلیپ بورد و من بیار
- کانفیگ سفارشین ز مهلی و من بیار
- کانفیگ سفارشین ز آدرس اینترنتی و من بیار
- نشۊوی اینترنتی اسکن کانفیگ سفارشین بزݩ
پاک بۊ؟
پؽش ز پاک کردن کانفیگ نا موئتبر واجۊری کوݩ! پاک کردن کانفیگن قوۊل اکۊنی؟
نیشتنا
- آدرس
+ نشۊوی
پورت
- نوم من توری
+ نوم منتوری
شناسه جایگۊزین
ٱمنیت
شبکه
@@ -73,21 +68,21 @@
Alpn
اجازه نا ٱمن
SNI
- آدرس
+ نشۊوی
پورت
رزم
ٱمنیت
رزم (اختیاری)
- نوم من توری (اختیاری)
+ نوم منتوری (اختیاری)
رزم نگاری
جریان
کیلیت پوی وولاتی
- کیلیت رمز ناهاڌن ازاف (اختیاری)
+ کیلیت رزم ناهاڌن ازاف (اختیاری)
ShortID
SpiderX
کیلیت سیخومی
Reserved(اختیاری، وا کاما ز یک جوڌا ابۊن)
- آدرس مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن)
+ نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن)
Mtu(اختیاری، پؽش فرز 1420)
وا مووفقیت ٱنجوم وابی
شکست خرد
@@ -101,11 +96,11 @@
موئتوا
هیچ داده ای من کلیپ بورد وۊجۊڌ نڌاره
نشۊوی اینترنتی نا موئتبر هڌ
- آدرس اشتراک پوروتوکول نا ٱمن HTTP ن و کار مبرین
+ نشۊوی اشتراک پوروتوکول نا ٱمن HTTP ن و کار مبرین
موتمعن بۊین ک پورت وۊرۊڌی وا سامووا ی جۊر هڌ
کانفیگ زبال نؽڌ
هاست(SNI)(اختیاری)
- ای کار ممنۊ هڌ
+ ای کار ممنۊع هڌ
رزم obfs
پورت گوم (درگا سرورن ز نۊ هؽل اکونه)
فاسله پورت گوم (سانیه)
@@ -121,29 +116,31 @@
ازاف کردن فایل
ازاف کردن لینگ
اسکن QRcode
- آدرس اینترنتی
+ نشۊوی اینترنتی
دانلود فایلا
- آدرس اینترنتی دارایین ازاف کۊنین
+ نشۊوی اینترنتی دارایین ازاف کۊنین
فایلن نجوست
- ائزارات ز زیتر بیڌسۉݩ
+ نوم ز زیتر بیڌس
+ بونچک فایلا جوقرافیایی (اختیاری)
- هون بار ونی بۊ
+ هونی بار ونی بۊ
پیتینیڌن
پسند پوی
- رزمان بزنین
+ رزما ن بزنین
هالت Bypass
پسند خوتکار پروکسی برنومه
موئتوا هونی دانلود ابۊن
و در کشیڌن من کلیپ بورد
و من ٱووردن ز کلیپ بورد
- Per-app settings
- Enable per-app
+ سامووا ب تفکیک برنومه
+ ر وندن ب تفکیک برنومه
سامووا
سامووا پؽش رئڌه
+ سامووا هسته
سامووا VPN
پروکسی و ری برنومه
پوی وولاتی: برنومه واجۊری بیڌه پروکسی هڌ، منپیز موستقیم بؽ نشووه هڌ. هالت دور زیڌن: برنومه نشووک ناڌه موستقیمن منپیز هڌ، پروکسی نشووک زیڌه نؽڌ. گۊزینه پسند خوتکار برنومه پروکسی من نومگه
@@ -152,10 +149,10 @@
سامووا Mux
ر وندن Mux
- زل تر، ٱما گاشڌ منپیز زی قت بۊ بارت دؽوۉداری، TCP، UDP و QUIC ن ای لم سفارشی کۊنین.
+ زل تر، ٱما گاشڌ منپیز زی قت بۊ\nمخزن ترافیک TCP وا 8 منپیز پؽش فرز، بارت دؽوۉداری UDP وو QUIC ن ای لم سفارشی کۊنین.
منپیزا TCP (تلایه منجا 1-1024)
منپیزا XUDP (تلایه منجا 1-1024)
- دؽوۉداری QUIC من تونل mux
+ دؽوۉداری QUIC من تۊنل mux
- رڌ کردن
- موجاز
@@ -168,34 +165,36 @@
ر وندن Sniffing
دامنه sniff ن ز کتن امتهۉݩ کۊنین (پؽش فرز رۊشن)
ر وندن routeOnly
- ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو آدرس مورد نزرن و عونوان آدرس IP ووردارین.
+ ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو نشۊوی مۉرد نزرن و عونوان نشۊوی IP ووردارین.
ر وندن DNS مهلی
- DNS پردازشت وابیڌه و دس هسته ماژول DNS (پؽشنهاڌ ابۊ، ٱر نیاز هڌ ک جوستن تور وو ولات ٱسلین دور زنی)
+ درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین فعال بۊ)
ر وندن DNS جئلی
- DNS مهلی آدرسا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومیل کار نکونه)
+ DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه)
ترجی IPv6
- ترجی داڌن نشۊوی وو تورا IPv6
+ تورا IPv6 ن فعال کۊنین وو نشۊویا IPv6 ن ترجی بڌین
- DNS ز ر دیر (اختیاری) (udp/tcp/https/quic) (اختیاری)
+ ز ر دیر (اختیاری) DNS (udp/tcp/https/quic) (اختیاری)
DNS
VPN DNS (تینا IPv4/v6)
- VPN ز شبکه مهلی اگوڌرته؟
+ ز شبکه مهلی اگوڌرته؟ VPN
- DNS منی (اختیاری)
+ نشۊوی رابت VPN
+
+ منی (اختیاری) DNS
DNS
- DNS هاست موستقیم (قالوو: دامنه: آدرس،...)
- دامنه:آدرس،...
+ هاست موستقیم (قالوو: دامنه: نشۊوی،...) DNS
+ دامنه:نشۊوی،...
- آدرس اینترنتی آزمایش تئخیر واقعی (http/https)
+ نشۊوی اینترنتی آزمایش تئخیر واقعی (http/https)
نشۊوی اینترنتی
هشتن منپیزا ز شبکه مهلی
- پوی دسگایل ترن وا آدرس IP ایسا، ز ر socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ.
+ پوی دسگایل ترن وا نشۊوی IP ایسا، ز ر socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ.
منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین.
اجازه نا ٱمن
@@ -208,20 +207,20 @@
پورت DNS مهلی
قوۊل کردن پاک کردن کانفیگ
- سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ
+ سی پاک وابیڌن فایل کانفیگ نیاز به قوۊل کردن دووارته ز سمت منتور هڌ.
زی اسکنن ر ون
- شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، اندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین.
+ شؽواتگرن سی اسکن، زی مجال ر وندن بۊگۊشین، ٱندی ترین کودن اسکن کۊنین یا شؽواتی ن منه نوار ٱوزار پسند کۊنین.
پروکسی HTTP ن و VPN ازاف کۊنین
- پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومیل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.
+ پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.
- Enable double column display
- The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect.
+ ر وندن نشۉݩ داڌن دو سۊتۊنی
+ نومگه نمایه یل من دو سۊتۊن نشۉݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ر وستن، وا برنومه ن ز نۊ ر ونین.
فشناڌن منشڌ
- فشناڌن منشڌ یا داسوو موشکلا من Github
+ فشناڌن منشڌ یا داسووݩ موشکلا من Github
ٱووڌن من جرگه تلگرام
برنومه تلگرامن نجوست
هریم سیخومی
@@ -242,6 +241,7 @@
فاسله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ)
سئت داسووا
+ بارت پؽش هل دامنه دری
هالت
سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ
زۉݩ
@@ -262,16 +262,17 @@
نوم موستعار فیلتر
فعال بیڌن ورۊ کردن
فعال بیڌن ورۊ کردن خوتکار
+ موجاز کردن نشۊوی HTTP نا ٱمن
نوم موستعار پروکسی دیندایی
نوم موستعار پروکسی نیایی
موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی
ورۊ کردن اشتراک جرگه سکویی
Tcping کانفیگا جرگه سکویی
تئخیر واقعی کانفیگا جرگه سکویی
- Asset files
- ترتیب و ری نتیجیل آزمایش
+ فایلا بونچک جوقرافیایی
+ ترتیب و ری نتیجه یل آزمایش
فیلتر کردن کانفیگا
- پوی جرگیل
+ پوی جرگه یل کانفیگ
پاک کردن %d کانفیگ تکراری
پاک کردن %d کانفیگ
@@ -307,7 +308,7 @@
منپیزن واجۊری کوݩ
هونی آزمایش ابۊ…
%d کانفیگ هونی آزمایش ابۊ...
- مووفق بی: منپیز HTTP %dms تۊل کشی
+ مووفق بی: منپیز %dms تۊل کشی
منپیز و اینترنتن نجوست: %s
اینترنت من دسرس نؽ
کود ختا: #%d
@@ -316,11 +317,18 @@
اشتراک وا مووفقیت زفت زابی
اشتراک زفت نوابی
- سامووا Fragment
- Fragment Packets
- Fragment Length (min-max)
- Fragment Interval (min-max)
- ر وندن Fragment
+ سامووا فرگمنت
+ کتنا فرگمنت
+ تۊل کتنا فرگمنت (هدقل-هدکسر)
+ فاسله منجا کتنا فرگمنت (هدقل-هدکسر)
+ ر وندن فرگمنت
+
+ واجۊری سی ورۊ رسۊوی
+ سکو نوسخه دیندایی پۊرنیڌه هڌ
+ نوسخه نۊ ن جوست: %s
+ سکو ورۊ رسۊوی کۊنین
+ واجۊری نوسخه یل پؽش ز تیجنیڌن
+ ورۊ رسۊوی ن هونی واجۊری اکونه...
- QRcode
@@ -330,10 +338,10 @@
- QRcode
- - Export to clipboard
- - Export full configuration to clipboard
- - Edit
- - Delete
+ - و در کشیڌن من کلیپ بورد
+ - و در کشیڌن پوی کانفیگ من کلیپ بورد
+ - آلشت
+ - پاک کردن
@@ -365,4 +373,10 @@
- دور زیڌه نبۊ
+
+ - هل وو فسل مکۊنین
+ - هل وو ٱووردن و میزبووݩ یل دامنه DNS
+ - هل وو جایونی دامنه
+
+
diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml
index 790ed3de..081571a3 100644
--- a/V2rayNG/app/src/main/res/values-fa/strings.xml
+++ b/V2rayNG/app/src/main/res/values-fa/strings.xml
@@ -35,11 +35,6 @@
تایپ دستی[TROJAN]
تایپ دستی[WIREGUARD]
تایپ دستی[HYSTERIA2]
- کانفیگ سفارشی
- کانفیگ سفارشی را از کلیپ بورد وارد کنید
- کانفیگ سفارشی را به صورت محلی وارد کنید
- کانفیگ سفارشی را از طریق نشانی اینترنتی وارد کنید
- نشانی اینترنتی اسکن کانفیگ سفارشی را وارد کنید
حذف شود؟
لطفا قبل از حذف کانفیگ نامعتبر بررسی کنید! حذف کانفیگ را تایید می کنید؟
ملاحظات
@@ -124,6 +119,7 @@
آدرس اینترنتی را اضافه کنید
فایل پیدا نشد
نام قبلاً وجود دارد
+ منبع فایل های جغرافیایی (اختیاری)
بارگذاری
@@ -141,6 +137,7 @@
تنظیمات
تنظیمات پیشرفته
+ تنظیمات هسته
تنظیمات VPN
پروکسی به تفکیک برنامه
عمومی: برنامه انتخاب شده از طریق یک پروکسی متصل می شود، برنامه انتخاب نشده مستقیماً متصل می شود. \nحالت دور زدن: برنامه انتخاب شده مستقیماً متصل می شود، برنامه انتخاب نشده از طریق یک پروکسی متصل می شود. \nانتخاب خودکار برنامه های پراکسی در منو امکان پذیر است.
@@ -175,7 +172,7 @@
دی ان اس محلی آدرس های آیپی جعلی را بر می گرداند (سریع تر می باشد و تاخیر را کاهش می دهد اما ممکن است برای برخی از برنامه ها کار نکند)
ترجیح دادن IPV6
- ترجیح دادن نشانی و مسیر های IPv6
+ مسیرهای IPv6 را فعال کنید و آدرسهای IPv6 را ترجیح دهید
DNS از راه دور (اختیاری) (udp/tcp/https/quic)
DNS
@@ -183,6 +180,8 @@
VPN DNS (فقط IPv4/v6)
آیا VPN از شبکه محلی عبور می کند؟
+ VPN Interface Address
+
DNS داخلی (اختیاری)
DNS
@@ -239,6 +238,7 @@
اشتراک های خود را به طور خودکار با فاصله زمانی در پس زمینه به روز کنید. بسته به دستگاه، این ویژگی ممکن است همیشه کار نکند.
فاصله به روزرسانی خودکار ( حداقل مقدار ، 15 دقیقه )
سطح گزارشات
+ Outbound domain pre-resolve method
حالت
برای اطلاعات و راهنمایی بیشتر، روی این متن کلیک کنید
زبان
@@ -259,6 +259,7 @@
نام مستعار فیلتر
فعال کردن بهروزرسانی
فعال سازی بهروزرسانی خودکار
+ مجاز کردن آدرس HTTP ناامن
نام مستعار پروکسی قبلی
نام مستعار پروکسی بعدی
لطفاً مطمئن شوید که نام مستعار وجود دارد و منحصر به فرد است
@@ -304,7 +305,7 @@
اتصال را بررسی کنید
در حال آزمایش...
تست کردن %d کانفیگ…
- موفقیت: اتصال HTTP %dms طول کشید
+ موفقیت: اتصال %dms طول کشید
اتصال به اینترنت شناسایی نشد: %s
اینترنت در دسترس نیست
کد خطا: #%d
@@ -319,6 +320,13 @@
فاصله بین بسته های فرگمنت (حداقل-حداکثر)
فعال کردن فرگمنت
+ بررسی به روز رسانی
+ در حال حاضر آخرین نسخه نصب شده است
+ نسخه جدید پیدا شد: %s
+ اکنون به روز رسانی کنید
+ بررسی نسخه پیش از انتشار
+ در حال بررسی برای بهروزرسانی…
+
- QRcode
- خروجی گرفتن در کلیپ بورد
@@ -364,4 +372,10 @@
- دور زده نشود
+
+ - Do not resolve
+ - Resolve and add to DNS Hosts
+ - Resolve and replace domain
+
+
diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml
index 46a0a5f3..c71054a0 100644
--- a/V2rayNG/app/src/main/res/values-ru/strings.xml
+++ b/V2rayNG/app/src/main/res/values-ru/strings.xml
@@ -35,11 +35,6 @@
Ручной ввод Trojan
Ручной ввод WireGuard
Ручной ввод Hysteria2
- Другой профиль
- Импорт из буфера обмена
- Импорт из файла
- Импорт из URL
- Импорт сканированием URL
Подтверждаете удаление?
Выполните проверку перед удалением! Подтверждаете удаление?
Название
@@ -126,6 +121,7 @@
Добавить URL ресурса
Файл не найден
Название уже существует
+ Источник геофайлов (необязательно)
Загрузка…
@@ -143,6 +139,7 @@
Настройки
Расширенные настройки
+ Настройки ядра
Настройки VPN
Прокси для выбранных приложений
Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.
@@ -176,7 +173,7 @@
Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)
Предпочитать IPv6
- Предпочитать IPv6-адреса и маршрутизацию
+ Использовать маршрутизацию IPv6 предпочитать IPv6-адреса
Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)
DNS
@@ -184,6 +181,8 @@
VPN DNS (только IPv4/v6)
VPN пропускает LAN
+ VPN частный IP
+
Внутренняя DNS (необязательно)
DNS
@@ -241,6 +240,7 @@
Интервал автообновления (минут, не менее 15)
Подробность ведения журнала
+ Outbound domain pre-resolve method
Режим
Нажмите для получения дополнительной информации
Язык
@@ -261,9 +261,10 @@
Название фильтра
Использовать обновление
Использовать автообновление
- Название предыдущего прокси
- Название следующего прокси
- Название должно существовать и быть уникальным
+ Разрешать незащищённые HTTP-адреса
+ Предыдущая конфигурация прокси
+ Следующая конфигурация прокси
+ Конфигурация должна быть уникальной
Обновить подписку группы
Проверка профилей группы
Время отклика профилей группы
@@ -306,7 +307,7 @@
Проверить подключение
Проверка…
Проверка профилей (%d)
- Успешно: HTTP-соединение заняло %d мс
+ Успешно: соединение заняло %d мс
Сбой проверки интернет-соединения: %s
Интернет недоступен
Код ошибки: #%d
@@ -321,6 +322,13 @@
Интервал фрагментов (от - до)
Использовать фрагментирование
+ Проверить обновление
+ Установлена последняя версия
+ Найдена новая версия: %s
+ Обновить
+ Искать предварительный выпуск
+ Проверка обновления…
+
- QR-код
- Экспорт в буфер обмена
@@ -364,4 +372,10 @@
- Не пропускает
+
+ - Do not resolve
+ - Resolve and add to DNS Hosts
+ - Resolve and replace domain
+
+
diff --git a/V2rayNG/app/src/main/res/values-vi/strings.xml b/V2rayNG/app/src/main/res/values-vi/strings.xml
index 0be072c2..86238b79 100644
--- a/V2rayNG/app/src/main/res/values-vi/strings.xml
+++ b/V2rayNG/app/src/main/res/values-vi/strings.xml
@@ -35,11 +35,6 @@
Nhập thủ công [Trojan]
Nhập thủ công [WireGuard]
Type manually[Hysteria2]
- Nâng cao / Cấu hình tùy chỉnh
- Nhập cấu hình tùy chỉnh từ Clipboard
- Nhập cấu hình tùy chỉnh từ Tệp
- Nhập cấu hình tùy chỉnh từ URL
- Nhập cấu hình tùy chỉnh quét URL
Xác nhận xóa?
Please test before deleting! Confirm delete ?
Tên cấu hình
@@ -124,6 +119,7 @@
Thêm URL nội dung
Không tìm thấy tập tin!
Nhận xét đã tồn tại!
+ Geo files source (optional)
Đang tải...
@@ -142,6 +138,7 @@
Cài đặt
Cài đặt nâng cao
+ Cài đặt lõi
Cài đặt VPN
Proxy theo Ứng dụng
- Bình thường: Ứng dụng đã chọn sẽ kết nối thông qua Proxy, chưa chọn sẽ kết nối trực tiếp. \n- Chế độ Bypass: Ứng dụng đã chọn sẽ kết nối trực tiếp, chưa chọn sẽ kết nối qua Proxy. \n- Nếu bạn đang ở Trung Quốc thì vào Menu, chọn Tự động chọn ứng dụng Proxy.
@@ -185,6 +182,8 @@
VPN DNS (Chỉ IPv4 / IPv6)
Does VPN bypass LAN
+ VPN Interface Address
+
DNS nội địa (Không bắt buộc)
DNS
@@ -242,6 +241,7 @@
Thời gian cập nhật tự động (Phút, giá trị tối thiểu là 15)
Cấp độ nhật ký
+ Outbound domain pre-resolve method
Chế độ kết nối
Nhấn vào đây nếu bạn cần trợ giúp!
Ngôn ngữ
@@ -262,9 +262,10 @@
Remarks regular filter
Sử dụng gói đăng ký này
Bật tự động cập nhật
- Previous proxy remarks
- Next proxy remarks
- The remarks exists and is unique
+ Allow insecure HTTP address
+ Previous proxy configuration remarks
+ Next proxy configuration remarks
+ The configuration remarks exists and is unique
Cập nhật các gói đăng ký
Ping tất cả máy chủ
Kiểm tra HTTP tất cả máy chủ
@@ -309,6 +310,19 @@
Nhập gói đăng ký thành công!
Nhập gói đăng ký không thành công!
+ Fragment Settings
+ Fragment Packets
+ Fragment Length (min-max)
+ Fragment Interval (min-max)
+ Enable Fragment
+
+ Check for update
+ Already on the latest version
+ New version found: %s
+ Update now
+ Check Pre-release
+ Checking for update…
+
- Xuất ra mã QR (Chụp màn hình để lưu)
- Sao chép vào Clipboard
@@ -340,16 +354,16 @@
- Sáng
- Tối
- Fragment Settings
- Fragment Packets
- Fragment Length (min-max)
- Fragment Interval (min-max)
- Enable Fragment
-
- Follow config
- Bypass
- Not Bypass
+
+ - Do not resolve
+ - Resolve and add to DNS Hosts
+ - Resolve and replace domain
+
+
diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml
index dedfdb86..a8eec856 100644
--- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml
+++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml
@@ -35,11 +35,6 @@
手动输入 [Trojan]
手动输入 [Wireguard]
手动输入 [Hysteria2]
- 自定义配置
- 从剪贴板导入自定义配置
- 从本地导入自定义配置
- 剪贴板 URL 导入自定义配置
- 扫描 URL 导入自定义配置
确认删除?
删除前请先测试!确认删除?
别名 (remarks)
@@ -124,6 +119,7 @@
添加资产网址
文件未找到
备注已经存在
+ Geo 文件来源 (可选)
正在加载
@@ -141,6 +137,7 @@
设置
进阶设置
+ 核心设置
VPN 设置
分应用
常规: 勾选的 App 被代理, 未勾选的直连;\n绕行模式: 勾选的 App 直连, 未勾选的被代理.\n不明白者在菜单中选择自动选中需代理应用
@@ -182,6 +179,8 @@
VPN DNS (仅支持 IPv4/v6)
VPN 是否绕过局域网
+ VPN 接口地址
+
境内 DNS (可选)
DNS
@@ -239,6 +238,7 @@
自动更新间隔(分钟,最小值 15)
日志级别
+ Outbound 域名预解析方式
模式
点此查看更多帮助
语言
@@ -259,9 +259,10 @@
别名正则过滤
启用更新
启用自动更新
- 前置代理别名
- 落地代理別名
- 请确保别名存在并唯一
+ 允许不安全的 HTTP 地址
+ 前置代理配置文件别名
+ 落地代理配置文件別名
+ 请确保配置文件别名存在并唯一
更新当前组订阅
测试当前组配置 Tcping
测试当前组配置真连接
@@ -313,6 +314,13 @@
分片间隔(最小 - 最大)
启用分片(Fragment)
+ 检查更新
+ 目前已是最新版本
+ 发现新版本: %s
+ 立即更新
+ 检查 Pre-release
+ 正在检查更新中…
+
- 二维码
- 导出至剪贴板
@@ -356,4 +364,10 @@
- 不绕过
+
+ - 不解析
+ - 解析后添加至 DNS Hosts
+ - 解析后替换原域名
+
+
diff --git a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml
index 8861d19c..f8b938c5 100644
--- a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml
+++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml
@@ -35,11 +35,6 @@
手動鍵入 [Trojan]
手動鍵入 [Wireguard]
手動鍵入 [Hysteria2]
- 自訂設定
- 從剪貼簿匯入自訂設定
- 從本地匯入自訂設定
- 從 URL 匯入自訂設定
- 掃描 URL 匯入自訂設定
確定刪除?
刪除前請先測試!確認刪除?
備註
@@ -124,6 +119,7 @@
新增資產網址
文件未找到
備註已經存在
+ Geo 檔案來源 (可選)
載入
@@ -142,6 +138,7 @@
設定
進階
+ 核心設定
VPN 設定
Proxy 個別應用程式
常規:勾選的 App 啟用 Proxy,未勾選的直接連線;\n繞行模式:勾選的 App 直接連線,未勾選的啟用 Proxy。\n可在選單中選擇自動選中需 Proxy 應用
@@ -184,6 +181,8 @@
VPN DNS (僅支援 IPv4/v6)
VPN 是否繞過區域網
+ VPN 介面位址
+
DNS
境内 DNS (可选)
DNS hosts (格式: 網域:位址,…)
@@ -240,6 +239,7 @@
自動更新間隔(分鐘,最小值 15)
記錄層級
+ Outbound 網域預解析方式
模式
輕觸以檢視說明
語言
@@ -260,9 +260,10 @@
別名正規過濾
啟用更新
啟用自動更新
- 前置代理别名
- 落地代理別名
- 请确保别名存在并唯一
+ 允許不安全的 HTTP 位址
+ 前置代理設定檔别名
+ 落地代理設定檔別名
+ 请确保設定檔别名存在并唯一
更新目前群組訂閱
偵測目前群組設定 Tcping
偵測目前群組設定真延遲
@@ -313,6 +314,13 @@
分片間隔(最小-最大)
啟用分片(Fragment)
+ 檢查更新
+ 当前已是最新版本
+ 發現新版本: %s
+ 立即更新
+ 檢查 Pre-release
+ 正在檢查更新中…
+
- QR Code
- 匯出至剪貼簿
@@ -356,4 +364,10 @@
- 不繞過
+
+ - 不解析
+ - 解析後加入 DNS Hosts
+ - 解析後替換原網域名稱
+
+
diff --git a/V2rayNG/app/src/main/res/values/arrays.xml b/V2rayNG/app/src/main/res/values/arrays.xml
index f758f2b4..27f0846e 100644
--- a/V2rayNG/app/src/main/res/values/arrays.xml
+++ b/V2rayNG/app/src/main/res/values/arrays.xml
@@ -124,43 +124,6 @@
- xtls-rprx-vision-udp443
-
-
-
- - 0.0.0.0/5
- - 8.0.0.0/7
- - 11.0.0.0/8
- - 12.0.0.0/6
- - 16.0.0.0/4
- - 32.0.0.0/3
- - 64.0.0.0/2
- - 128.0.0.0/3
- - 160.0.0.0/5
- - 168.0.0.0/6
- - 172.0.0.0/12
- - 172.32.0.0/11
- - 172.64.0.0/10
- - 172.128.0.0/9
- - 173.0.0.0/8
- - 174.0.0.0/7
- - 176.0.0.0/4
- - 192.0.0.0/9
- - 192.128.0.0/11
- - 192.160.0.0/13
- - 192.169.0.0/16
- - 192.170.0.0/15
- - 192.172.0.0/14
- - 192.176.0.0/12
- - 192.192.0.0/10
- - 193.0.0.0/8
- - 194.0.0.0/7
- - 196.0.0.0/6
- - 200.0.0.0/5
- - 208.0.0.0/4
- - 240.0.0.0/4
-
-
-
- auto
- English
@@ -219,4 +182,30 @@
- 2
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+
+
+
+ - 10.10.14.x
+ - 10.1.0.x
+ - 10.0.0.x
+ - 172.31.0.x
+ - 172.20.0.x
+ - 172.16.0.x
+ - 192.168.100.x
+
+
+
+ - 0
+ - 1
+ - 2
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml
index b085a3df..57106306 100644
--- a/V2rayNG/app/src/main/res/values/strings.xml
+++ b/V2rayNG/app/src/main/res/values/strings.xml
@@ -36,11 +36,6 @@
Type manually[Trojan]
Type manually[Wireguard]
Type manually[Hysteria2]
- Custom config
- Import custom config from Clipboard
- Import custom config from locally
- Import custom config from URL
- Import custom config scan URL
Confirm delete ?
Please test before deleting! Confirm delete ?
remarks
@@ -127,6 +122,7 @@
Add asset URL
File not found
The remarks already exists
+ Geo files source (optional)
Loading
@@ -144,6 +140,7 @@
Settings
Advanced Settings
+ Core Settings
VPN Settings
Per-app proxy
General: Checked apps use proxy, unchecked apps connect directly; \nBypass mode: checked apps connect directly, unchecked apps use proxy. \nThe option to automatically select proxy applications is in the menu
@@ -178,7 +175,7 @@
Local DNS returns fake IP addresses (faster, but it may not work for some apps)
Prefer IPv6
- Prefer IPv6 addresses and routes
+ Enable IPv6 routes and Prefer IPv6 addresses
Remote DNS (udp/tcp/https/quic)(Optional)
DNS
@@ -186,6 +183,8 @@
VPN DNS (only IPv4/v6)
Does VPN bypass LAN
+ VPN Interface Address
+
Domestic DNS (Optional)
DNS
@@ -243,6 +242,7 @@
Auto Update Interval (Minutes, Min value 15)
Log Level
+ Outbound domain pre-resolve method
Mode
Click me for more help
Language
@@ -263,9 +263,10 @@
Remarks regular filter
Enable update
Enable automatic update
- Previous proxy remarks
- Next proxy remarks
- The remarks exists and is unique
+ Allow insecure HTTP address
+ Previous proxy configuration remarks
+ Next proxy configuration remarks
+ The configuration remarks exists and is unique
Update current group subscription
Tcping current group configuration
Real delay current group configuration
@@ -308,7 +309,7 @@
Check Connectivity
Testing…
Testing %d configurations…
- Success: HTTP connection took %dms
+ Success: Connection took %dms
Fail to detect internet connection: %s
Internet Unavailable
Error code: #%d
@@ -323,6 +324,13 @@
Fragment Interval (min-max)
Enable Fragment
+ Check for update
+ Already on the latest version
+ New version found: %s
+ Update now
+ Check Pre-release
+ Checking for update…
+
- QRcode
- Export to clipboard
@@ -366,4 +374,10 @@
- Not Bypass
+
+ - Do not resolve
+ - Resolve and add to DNS Hosts
+ - Resolve and replace domain
+
+
diff --git a/V2rayNG/app/src/main/res/xml/network_security_config.xml b/V2rayNG/app/src/main/res/xml/network_security_config.xml
index 134bc5fa..24579b10 100644
--- a/V2rayNG/app/src/main/res/xml/network_security_config.xml
+++ b/V2rayNG/app/src/main/res/xml/network_security_config.xml
@@ -1,6 +1,6 @@
-
+
+
+
+
+
@@ -166,14 +179,7 @@
android:title="@string/title_pref_auto_update_interval" />
-
-
-
+
-
-
+
+
+
+
+
+
+
+
-
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt b/V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt
new file mode 100644
index 00000000..07d87f4d
--- /dev/null
+++ b/V2rayNG/app/src/test/java/com/v2ray/ang/HttpUtilTest.kt
@@ -0,0 +1,41 @@
+package com.v2ray.ang
+
+import com.v2ray.ang.util.HttpUtil
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class HttpUtilTest {
+
+ @Test
+ fun testIdnToASCII() {
+ // Regular URL remains unchanged
+ val regularUrl = "https://example.com/path"
+ assertEquals(regularUrl, HttpUtil.toIdnUrl(regularUrl))
+
+ // Non-ASCII URL converts to ASCII (Punycode)
+ val nonAsciiUrl = "https://例子.测试/path"
+ val expectedNonAscii = "https://xn--fsqu00a.xn--0zwm56d/path"
+ assertEquals(expectedNonAscii, HttpUtil.toIdnUrl(nonAsciiUrl))
+
+ // Mixed URL only converts the host part
+ val mixedUrl = "https://例子.com/测试"
+ val expectedMixed = "https://xn--fsqu00a.com/测试"
+ assertEquals(expectedMixed, HttpUtil.toIdnUrl(mixedUrl))
+
+ // URL with Basic Authentication using regular domain
+ val basicAuthUrl = "https://user:password@example.com/path"
+ assertEquals(basicAuthUrl, HttpUtil.toIdnUrl(basicAuthUrl))
+
+ // URL with Basic Authentication using non-ASCII domain
+ val basicAuthNonAscii = "https://user:password@例子.测试/path"
+ val expectedBasicAuthNonAscii = "https://user:password@xn--fsqu00a.xn--0zwm56d/path"
+ assertEquals(expectedBasicAuthNonAscii, HttpUtil.toIdnUrl(basicAuthNonAscii))
+
+ // URL with non-ASCII username and password
+ val nonAsciiAuth = "https://用户:密码@example.com/path"
+ // Basic auth credentials should remain unchanged as they're percent-encoded separately
+ assertEquals(nonAsciiAuth, HttpUtil.toIdnUrl(nonAsciiAuth))
+ }
+
+
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/AngUnitTest.kt b/V2rayNG/app/src/test/java/com/v2ray/ang/UtilsTest.kt
similarity index 73%
rename from V2rayNG/app/src/test/java/com/v2ray/ang/AngUnitTest.kt
rename to V2rayNG/app/src/test/java/com/v2ray/ang/UtilsTest.kt
index 547d15d3..77dd53ab 100644
--- a/V2rayNG/app/src/test/java/com/v2ray/ang/AngUnitTest.kt
+++ b/V2rayNG/app/src/test/java/com/v2ray/ang/UtilsTest.kt
@@ -11,7 +11,7 @@ import org.junit.Test
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
-class AngUnitTest {
+class UtilsTest {
@Test
fun test_parseInt() {
@@ -45,4 +45,18 @@ class AngUnitTest {
assertTrue(Utils.isIpAddress("240e:1234:abcd:12::/64"))
}
+ @Test
+ fun test_IsIpInCidr() {
+ assertTrue(Utils.isIpInCidr("192.168.1.1", "192.168.1.0/24"))
+ assertTrue(Utils.isIpInCidr("192.168.1.254", "192.168.1.0/24"))
+ assertFalse(Utils.isIpInCidr("192.168.2.1", "192.168.1.0/24"))
+
+ assertTrue(Utils.isIpInCidr("10.0.0.0", "10.0.0.0/8"))
+ assertTrue(Utils.isIpInCidr("10.255.255.255", "10.0.0.0/8"))
+ assertFalse(Utils.isIpInCidr("11.0.0.0", "10.0.0.0/8"))
+
+ assertFalse(Utils.isIpInCidr("invalid-ip", "192.168.1.0/24"))
+ assertFalse(Utils.isIpInCidr("192.168.1.1", "invalid-cidr"))
+ }
+
}
\ No newline at end of file
diff --git a/V2rayNG/gradle/libs.versions.toml b/V2rayNG/gradle/libs.versions.toml
index fb625399..04900e8c 100644
--- a/V2rayNG/gradle/libs.versions.toml
+++ b/V2rayNG/gradle/libs.versions.toml
@@ -1,27 +1,27 @@
[versions]
-agp = "8.9.1"
-desugar_jdk_libs = "2.1.5"
+agp = "8.10.1"
+desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
-kotlin = "2.1.20"
-coreKtx = "1.15.0"
+kotlin = "2.1.21"
+coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
-appcompat = "1.7.0"
+appcompat = "1.7.1"
material = "1.12.0"
activity = "1.10.1"
constraintlayout = "2.2.1"
mmkvStatic = "1.3.12"
gson = "2.12.1"
quickieFoss = "1.14.0"
-kotlinx-coroutines-android = "1.10.1"
-kotlinx-coroutines-core = "1.10.1"
+kotlinxCoroutinesAndroid = "1.10.2"
+kotlinxCoroutinesCore = "1.10.2"
swiperefreshlayout = "1.1.0"
toasty = "1.5.2"
editorkit = "2.9.0"
core = "3.5.3"
-workRuntimeKtx = "2.10.0"
-lifecycleViewmodelKtx = "2.8.7"
+workRuntimeKtx = "2.10.2"
+lifecycleViewmodelKtx = "2.9.1"
multidex = "2.0.1"
mockitoMockitoInline = "5.2.0"
flexbox = "3.0.0"
@@ -30,7 +30,7 @@ recyclerview = "1.4.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
-desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
+desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }
gradle-license-plugin = { module = "com.jaredsburrows:gradle-license-plugin", version.ref = "gradleLicensePlugin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@@ -42,8 +42,8 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
mmkv-static = { module = "com.tencent:mmkv-static", version.ref = "mmkvStatic" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" }
-kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version = "kotlinx-coroutines-android" }
-kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "kotlinx-coroutines-core" }
+kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
+kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
toasty = { module = "com.github.GrenderG:Toasty", version.ref = "toasty" }
editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" }
language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" }
diff --git a/V2rayNG/gradle/wrapper/gradle-wrapper.properties b/V2rayNG/gradle/wrapper/gradle-wrapper.properties
index f221584f..b2eeb9db 100644
--- a/V2rayNG/gradle/wrapper/gradle-wrapper.properties
+++ b/V2rayNG/gradle/wrapper/gradle-wrapper.properties
@@ -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-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/hysteria b/hysteria
index 245c6e9b..2adeec29 160000
--- a/hysteria
+++ b/hysteria
@@ -1 +1 @@
-Subproject commit 245c6e9bd17b1ef644f81fc4dafd0a1e1933da85
+Subproject commit 2adeec2900a7a0e3689f118580174cc528f9995a