Compare commits

...

25 Commits

Author SHA1 Message Date
2dust ef13e336da up 2.1.8 2026-05-19 16:56:12 +08:00
2dust d16cb21059 Update AndroidLibXrayLite 2026-05-19 11:37:28 +08:00
2dust 237837e7be Refactor speed notification and add outbound stats
Add OutboundTrafficStat DTO and a CoreServiceManager.queryAllOutboundTrafficStats() method to fetch/reset all outbound counters in one core call and parse Go-side payload. Remove ProfileItem.getAllOutboundTags() and simplify NotificationManager API (startSpeedNotification()/stopSpeedNotification()) to use aggregated stats via the new core query. Replace per-tag polling with a single aggregated update loop (updateSpeedNotificationOnce) to reduce IPC and CPU usage, and update CoreServiceManager call sites accordingly.
2026-05-19 11:33:03 +08:00
2dust 2a6edd98be Add SOCKS4/5 and map-based parser lookup
https://github.com/2dust/v2rayNG/issues/5649
2026-05-19 09:48:49 +08:00
DHR60 9238b80675 Fix (#5651) 2026-05-18 09:18:53 +08:00
skl256 bb5e6f1959 Fix https://github.com/2dust/v2rayNG/issues/5619 (#5632)
Added "Level 0" matching to perform a full check of remarks, server, port, and password before falling back to partial matches. This ensures that identical profiles with the same remarks are correctly identified
2026-05-13 18:48:22 +08:00
2dust 1cc0865411 Add profile remark dropdowns to editor
Replace plain EditText fields for pre/next profile with AutoCompleteTextView + dropdown ImageButton in activity_sub_edit.xml and wire them up in SubEditActivity.kt. New setupProfileRemarkInputs() loads distinct non-blank server remarks from MmkvManager, sets an ArrayAdapter (threshold=0) and shows the dropdown when the field or button is clicked to make selecting existing profile remarks easier. Also adds necessary imports.
2026-05-12 11:37:47 +08:00
2dust cb8d979f23 Add dto.entities package 2026-05-12 11:25:37 +08:00
2dust 5932b0db29 Add UrlContentRequest and refactor HttpUtil
Introduce UrlContentRequest DTO to encapsulate url, timeout, httpPort, proxy credentials and userAgent. Refactor HttpUtil APIs (getUrlContent, getUrlContentWithUserAgent, downloadToFile) to accept UrlContentRequest and update callers accordingly in AngConfigManager, SpeedtestManager, UpdateCheckerManager, PerAppProxyActivity and UserAssetViewModel. This centralizes HTTP request parameters, reduces duplicated parameter lists, and preserves existing behaviors (redirect handling, userAgent fallback, proxy auth).
2026-05-12 11:05:11 +08:00
Hossein Abaspanah dbe4fe78b5 Update Luri Bakhtiari translation (#5628)
* Update Luri Bakhtiari translation

* Update strings new.xml

* Update strings.xml
2026-05-12 08:57:54 +08:00
2dust b5bcd81ae4 up 2.1.7 2026-05-10 14:17:02 +08:00
2dust d45ccd7953 Update AndroidLibXrayLite 2026-05-10 14:16:54 +08:00
solokot 6815563902 Update Russian translation (#5621) 2026-05-10 14:07:00 +08:00
2dust 7c38ad7c57 Improve WireGuard addresses 2026-05-09 16:48:09 +08:00
2dust 885aeb384e Refactor TCP HTTP request builder, relax Pragma
https://github.com/2dust/v2rayNG/issues/5623
2026-05-09 16:35:11 +08:00
2dust 57f01412e7 up 2.1.6 2026-05-09 11:42:37 +08:00
2dust fd84ea4114 Code cleanup: imports, formatting, and minor fixes 2026-05-09 11:42:23 +08:00
2dust a4e2e6d6a2 Fix,reverse proxy chain profiles order 2026-05-09 11:05:27 +08:00
2dust b8cfe9bc82 bump AGP to 9.2.1 2026-05-09 11:04:59 +08:00
DHR60 b945f4a3c7 OkHttp Dialer (#5579)
AI
Add WebView dialer

Co-authored-by: fish4terrisa-MSDSM <flyingfish.msdsm@gmail.com>
2026-05-07 13:43:34 +08:00
fanymagnet cfb6776e8f Get profile desc from server.address/port for vless (#5610)
Co-authored-by: fanymagnet <>
2026-05-07 10:03:47 +08:00
fanymagnet ebaa5088b6 Show the save button and reboot the core upon saving (#5600)
Co-authored-by: fanymagnet <>
2026-05-05 19:23:41 +08:00
2dust 209679f098 Improve V2Ray config/error handling and startup
Add richer error reporting and simplify control flow across core components. Key changes:

- Add errorMessage to ConfigResult and populate it on failures in CoreConfigManager.getV2rayConfig/getV2rayConfig4Speedtest.
- Replace many nullable/boolean error returns in CoreConfigManager with explicit errors (error()/exceptions) and non-null return types; tighten validation (invalid servers, missing assets, parsing errors) and simplify inbound/dns/routing/outbound setup logic.
- Make getInbounds/getRouting/getDns/getOutbounds/getCustomLocalDns/getRoutingUserRule/getBalance non-failing (throw on error) and remove silent try/catch patterns; improve logging and fallback behaviors.
- CoreServiceManager: wrap startContextService calls with try/catch to surface errors to UI; startContextService now throws on invalid state/config and moves startup loop into doStartCoreLoop which registers receivers, starts core loop, and reports detailed failures to UI.
- Show notification when VPN service starts (CoreVpnService) to ensure foreground visibility.
- MainViewModel: display detailed start failure message from intent when available instead of only a generic toast.

These changes improve diagnostics, reduce silent failures, and centralize error handling during startup.
2026-05-05 18:45:47 +08:00
2dust 19ad248cfe Apply Basic auth from URL userinfo
https://github.com/2dust/v2rayNG/issues/5583
2026-05-05 10:40:09 +08:00
solokot 35f1f58476 Update Russian translation (#5595) 2026-05-04 10:52:11 +08:00
81 changed files with 2015 additions and 894 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 24
targetSdk = 36
versionCode = 725
versionName = "2.1.5"
versionCode = 728
versionName = "2.1.8"
multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -180,6 +180,8 @@ object AppConfig {
const val CUSTOM = ""
const val SHADOWSOCKS = "ss://"
const val SOCKS = "socks://"
const val SOCKS4 = "socks4://"
const val SOCKS5 = "socks5://"
const val HTTP = "http://"
const val VLESS = "vless://"
const val TROJAN = "trojan://"
@@ -1,6 +1,6 @@
package com.v2ray.ang.contracts
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
interface MainAdapterListener : BaseAdapterListener {
@@ -3,7 +3,7 @@ package com.v2ray.ang.core
import android.content.Context
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.CoreConfigContext
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.CoreResolvedType
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty
@@ -97,8 +97,7 @@ object CoreConfigContextBuilder {
.filter { it.configType != EConfigType.POLICYGROUP }
.filter { it.configType != EConfigType.PROXYCHAIN }
.toList()
}
catch (e: Exception) {
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for config '${config.remarks}'", e)
return listOf(config)
}
@@ -120,8 +119,8 @@ object CoreConfigContextBuilder {
.filter { it.configType != EConfigType.POLICYGROUP }
.filter { it.configType != EConfigType.PROXYCHAIN }
.toList()
}
catch (e: Exception) {
.reversed()
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles for config '${config.remarks}'", e)
return listOf(config)
}
File diff suppressed because it is too large Load Diff
@@ -43,6 +43,15 @@ object CoreNativeManager {
}
}
fun reconcileBrowserDialer(dialerAddr: String) {
try {
Libv2ray.reconcileBrowserDialer(dialerAddr)
LogUtil.i(AppConfig.TAG, "Browser dialer reconciled successfully with address: $dialerAddr")
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to reconcile browser dialer with address: $dialerAddr", e)
}
}
/**
* Get V2Ray core version.
@@ -1,10 +1,9 @@
package com.v2ray.ang.core
import android.text.TextUtils
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
@@ -73,41 +72,6 @@ object CoreOutboundBuilder {
outbound.mux?.concurrency = -1
}
if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
var localTunAddr = if (outbound.settings?.address == null) {
listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
} else {
outbound.settings?.address as List<*>
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) != true) {
localTunAddr = listOf(localTunAddr.first())
}
outbound.settings?.address = localTunAddr
}
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
val requestString: String by lazy {
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":["no-cache"]}}"""
}
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString,
OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
)
outbound.streamSettings?.tcpSettings?.header?.request?.path =
if (path.isNullOrEmpty()) {
listOf("/")
} else {
path
}
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
return false
@@ -291,9 +255,23 @@ object CoreOutboundBuilder {
private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? {
val outboundBean = createInitOutbound(EConfigType.WIREGUARD)
val rawAddresses = profileItem.localAddress
?.split(",")
?.map { it.trim() }
?.filter { it.isNotEmpty() }
?.ifEmpty { null }
?: listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
val addresses = if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) == true) {
rawAddresses
} else {
val ipv4Addresses = rawAddresses.filter { !it.contains(":") }
ipv4Addresses.ifEmpty { listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4) }
}
outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = profileItem.secretKey
wireguard.address = (profileItem.localAddress ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).split(",")
wireguard.address = addresses
wireguard.peers?.firstOrNull()?.let { peer ->
peer.publicKey = profileItem.publicKey.orEmpty()
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
@@ -328,6 +306,23 @@ object CoreOutboundBuilder {
return outboundBean
}
private fun createTcpHttpRequest(
host: String?,
path: String?
): OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean {
val requestString =
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
val request = JsonUtil.fromJson(
requestString,
OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
) ?: OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
val parsedHost = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
request.headers.Host = parsedHost.ifEmpty { null }
request.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }.ifEmpty { listOf("/") }
return request
}
/**
* Configures transport settings for an outbound connection.
*
@@ -358,13 +353,9 @@ object CoreOutboundBuilder {
val tcpSetting = OutboundBean.StreamSettingsBean.TcpSettingsBean()
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(path)) {
val requestObj = OutboundBean.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)
}
val requestObj = createTcpHttpRequest(host, path)
tcpSetting.header.request = requestObj
sni = requestObj.headers.Host?.getOrNull(0)
} else {
tcpSetting.header.type = "none"
sni = host
@@ -13,7 +13,8 @@ import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.OutboundTrafficStat
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
@@ -22,6 +23,9 @@ import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.service.CoreProxyOnlyService
import com.v2ray.ang.service.CoreVpnService
import com.v2ray.ang.service.DialerNativeService
import com.v2ray.ang.service.DialerWebviewService
import com.v2ray.ang.service.IDialerService
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils
@@ -40,6 +44,7 @@ object CoreServiceManager {
private val mMsgReceive = ReceiveMessageHandler()
private var currentConfig: ProfileItem? = null
private var processFinder: XrayProcessFinder? = null
private var browserDialer: IDialerService? = null
var serviceControl: SoftReference<ServiceControl>? = null
set(value) {
@@ -62,7 +67,13 @@ object CoreServiceManager {
context.toast(R.string.app_tile_first_use)
return false
}
startContextService(context)
try {
startContextService(context)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: ${e.message}", e)
context.toast(e.message ?: e.javaClass.simpleName)
return false
}
return true
}
@@ -78,7 +89,12 @@ object CoreServiceManager {
MmkvManager.setSelectServer(guid)
}
startContextService(context)
try {
startContextService(context)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: ${e.message}", e)
context.toast(e.message ?: e.javaClass.simpleName)
}
}
/**
@@ -106,7 +122,11 @@ object CoreServiceManager {
* 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.
* @throws IllegalStateException if the core is already running, no server is selected,
* server config cannot be decoded, or server configuration is invalid.
* @throws Exception if the foreground service fails to start.
*/
@Throws(Exception::class)
private fun startContextService(context: Context) {
if (coreController.isRunning) {
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
@@ -114,16 +134,16 @@ object CoreServiceManager {
}
val guid = MmkvManager.getSelectServer()
if (guid == null) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
return
}
?: run {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
error(context.getString(R.string.app_tile_first_use))
}
val config = MmkvManager.decodeServerConfig(guid)
if (config == null) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
return
}
?: run {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
error(context.getString(R.string.toast_config_file_invalid))
}
if (config.configType != EConfigType.CUSTOM
&& config.configType != EConfigType.POLICYGROUP
@@ -132,13 +152,14 @@ object CoreServiceManager {
&& !Utils.isPureIpAddress(config.server.orEmpty())
) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
return
error(context.getString(R.string.toast_config_file_invalid))
}
// refresh socks port when enabled dynamic socks port
SettingsManager.refreshRuntimeSocksPort()
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) return
// if (!result.status) error(result.errorMessage.ifBlank { "Failed to get V2Ray config" })
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING)) {
context.toast(R.string.toast_warning_pref_proxysharing_short)
@@ -155,11 +176,7 @@ object CoreServiceManager {
Intent(context.applicationContext, CoreProxyOnlyService::class.java)
}
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to start service", e)
}
ContextCompat.startForegroundService(context, intent)
}
/**
@@ -179,67 +196,70 @@ object CoreServiceManager {
return false
}
val guid = MmkvManager.getSelectServer()
if (guid == null) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
try {
doStartCoreLoop(service, vpnInterface)
return true
} catch (e: Exception) {
val message = e.message?.takeUnless { it.isBlank() } ?: e.javaClass.simpleName
LogUtil.e(AppConfig.TAG, "StartCore-Manager: $message", e)
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, message)
NotificationManager.cancelNotification()
return false
}
}
val config = MmkvManager.decodeServerConfig(guid)
if (config == null) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
return false
}
@Throws(Exception::class)
private fun doStartCoreLoop(service: Service, vpnInterface: ParcelFileDescriptor?) {
val guid = MmkvManager.getSelectServer() ?: error("No server selected")
val config = MmkvManager.decodeServerConfig(guid) ?: error("Failed to decode server config")
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
val result = CoreConfigManager.getV2rayConfig(service, guid)
LogUtil.d(AppConfig.TAG, result.content)
if (!result.status) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to get V2Ray config")
return false
error(result.errorMessage.ifBlank { "Failed to get V2Ray config" })
}
try {
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
mFilter.addAction(Intent.ACTION_SCREEN_ON)
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to register receiver", e)
return false
}
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
mFilter.addAction(Intent.ACTION_SCREEN_ON)
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
mFilter.addAction(Intent.ACTION_USER_PRESENT)
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
currentConfig = config
var tunFd = vpnInterface?.fd ?: 0
val dialerAddr = if (currentConfig?.browserDialerMode.isNullOrEmpty()) {
""
} else {
"127.0.0.1:${Utils.findRandomFreePort()}"
}
if (SettingsManager.isUsingHevTun()) {
tunFd = 0
}
try {
NotificationManager.showNotification(currentConfig)
coreController.startLoop(result.content, tunFd)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to start core loop", e)
return false
NotificationManager.showNotification(currentConfig)
CoreNativeManager.reconcileBrowserDialer(dialerAddr)
coreController.startLoop(result.content, tunFd)
if (!coreController.isRunning) {
error("Core failed to start")
}
if (coreController.isRunning == false) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Core failed to start")
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
NotificationManager.cancelNotification()
return false
if (browserDialer != null) {
browserDialer!!.stop()
browserDialer = null
}
if (config.browserDialerMode == "OkHttp") {
browserDialer = DialerNativeService()
browserDialer!!.start(service, dialerAddr)
} else if (config.browserDialerMode == "WebView") {
browserDialer = DialerWebviewService()
browserDialer!!.start(service, dialerAddr)
}
try {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
NotificationManager.startSpeedNotification(currentConfig)
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to complete startup", e)
return false
}
return true
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
NotificationManager.startSpeedNotification()
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
}
/**
@@ -260,6 +280,13 @@ object CoreServiceManager {
}
}
// Close existing browser dialer
CoreNativeManager.reconcileBrowserDialer("")
if (browserDialer != null) {
browserDialer!!.stop()
browserDialer = null
}
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
NotificationManager.cancelNotification()
@@ -273,13 +300,32 @@ object CoreServiceManager {
}
/**
* Queries the statistics for a given tag and link.
* @param tag The tag to query.
* @param link The link to query.
* @return The statistics value.
* Queries and resets all outbound traffic counters in one core call.
* Go side format: tag,direction,value;tag,direction,value;
*/
fun queryStats(tag: String, link: String): Long {
return coreController.queryStats(tag, link)
fun queryAllOutboundTrafficStats(): List<OutboundTrafficStat> {
val payload = coreController.queryAllOutboundTrafficStats()
val result = ArrayList<OutboundTrafficStat>()
payload.split(';').forEach { entry ->
if (entry.isBlank()) return@forEach
val parts = entry.split(',', limit = 3)
if (parts.size != 3) return@forEach
val value = parts[2].toLongOrNull() ?: return@forEach
result.add(
OutboundTrafficStat(
tag = parts[0],
direction = parts[1],
value = value,
)
)
}
// LogUtil.d(AppConfig.TAG, "Queried outbound traffic stats: $result")
return result
}
/**
@@ -406,7 +452,7 @@ object CoreServiceManager {
//LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid,${PackageUidResolver.uidToPackageName(uid.toString())}")
uid
} catch (e: Exception) {
} catch (_: Exception) {
-1L
}
}
@@ -462,12 +508,12 @@ object CoreServiceManager {
when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off")
NotificationManager.stopSpeedNotification(currentConfig)
NotificationManager.stopSpeedNotification()
}
Intent.ACTION_SCREEN_ON -> {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on")
NotificationManager.startSpeedNotification(currentConfig)
NotificationManager.startSpeedNotification()
}
}
}
@@ -4,5 +4,6 @@ data class ConfigResult(
var status: Boolean,
var guid: String? = null,
var content: String = "",
var errorMessage: String = "",
)
@@ -1,6 +1,7 @@
package com.v2ray.ang.dto
import android.content.Context
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.CoreResolvedType
/**
@@ -0,0 +1,7 @@
package com.v2ray.ang.dto
data class OutboundTrafficStat(
val tag: String,
val direction: String,
val value: Long,
)
@@ -0,0 +1,10 @@
package com.v2ray.ang.dto
data class UrlContentRequest(
val url: String?,
val timeout: Int = 15000,
val httpPort: Int = 0,
val proxyUsername: String? = null,
val proxyPassword: String? = null,
val userAgent: String? = null
)
@@ -181,7 +181,7 @@ data class V2rayConfig(
@SerializedName("Accept-Encoding")
val acceptEncoding: List<String>? = null,
val Connection: List<String>? = null,
val Pragma: String? = null
val Pragma: Any? = null
)
}
}
@@ -344,19 +344,19 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.first()?.address
return settings?.vnext?.firstOrNull()?.address ?: settings?.address as? String
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
) {
return settings?.servers?.first()?.address
return settings?.servers?.firstOrNull()?.address
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.first()?.endpoint?.substringBeforeLast(":")
return settings?.peers?.firstOrNull()?.endpoint?.substringBeforeLast(":")
} else if (protocol.equals(EConfigType.HYSTERIA2.name, true)
|| protocol.equals(EConfigType.HYSTERIA.name, true)
) {
return settings?.address as String?
return settings?.address as? String
}
return null
}
@@ -365,15 +365,15 @@ data class V2rayConfig(
if (protocol.equals(EConfigType.VMESS.name, true)
|| protocol.equals(EConfigType.VLESS.name, true)
) {
return settings?.vnext?.first()?.port
return settings?.vnext?.firstOrNull()?.port ?: settings?.port
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|| protocol.equals(EConfigType.SOCKS.name, true)
|| protocol.equals(EConfigType.HTTP.name, true)
|| protocol.equals(EConfigType.TROJAN.name, true)
) {
return settings?.servers?.first()?.port
return settings?.servers?.firstOrNull()?.port
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
return settings?.peers?.first()?.endpoint?.substringAfterLast(":")?.toInt()
return settings?.peers?.firstOrNull()?.endpoint?.substringAfterLast(":")?.toInt()
} else if (protocol.equals(EConfigType.HYSTERIA2.name, true)
|| protocol.equals(EConfigType.HYSTERIA.name, true)
) {
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
data class AssetUrlCache(
val guid: String,
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
data class AssetUrlItem(
var remarks: String = "",
@@ -1,10 +1,6 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.AppConfig.PORT_SOCKS
import com.v2ray.ang.AppConfig.TAG_BLOCKED
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.AppConfig
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.util.Utils
@@ -71,6 +67,8 @@ data class ProfileItem(
var policyGroupFilter: String? = null,
var proxyChainProfiles: String? = null,
var browserDialerMode: String? = null,
) {
companion object {
fun create(configType: EConfigType): ProfileItem {
@@ -78,13 +76,9 @@ data class ProfileItem(
}
}
fun getAllOutboundTags(): MutableList<String> {
return mutableListOf(TAG_PROXY, TAG_DIRECT, TAG_BLOCKED)
}
fun getServerAddressAndPort(): String {
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
return "$LOOPBACK:$PORT_SOCKS"
return "${AppConfig.LOOPBACK}:${AppConfig.PORT_SOCKS}"
}
return Utils.getIpv6Address(server) + ":" + serverPort
}
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
data class RulesetItem(
var remarks: String? = "",
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
fun getTestDelayString(): String {
@@ -7,4 +7,4 @@ data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
}
return testDelayMillis.toString() + "ms"
}
}
}
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
data class ServersCache(
val guid: String,
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
data class SubscriptionCache(
val guid: String,
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
data class SubscriptionItem(
var remarks: String = "",
@@ -1,4 +1,4 @@
package com.v2ray.ang.dto
package com.v2ray.ang.dto.entities
data class WebDavConfig(
val baseUrl: String,
@@ -6,4 +6,4 @@ data class WebDavConfig(
val password: String? = null,
val remoteBasePath: String = "/",
val timeoutSeconds: Long = 30
)
)
@@ -1,6 +1,6 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.util.JsonUtil
@@ -20,7 +20,7 @@ object CustomFmt : FmtBase() {
config.remarks = fullConfig?.remarks ?: System.currentTimeMillis().toString()
config.server = outbound?.getServerAddress()
config.serverPort = outbound?.getServerPort().toString()
config.serverPort = outbound?.getServerPort()?.toString()
return config
}
@@ -1,10 +1,9 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.idnHost
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.idnHost
@@ -1,6 +1,6 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.idnHost
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.handler.MmkvManager
@@ -2,7 +2,7 @@ package com.v2ray.ang.fmt
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
@@ -1,7 +1,7 @@
package com.v2ray.ang.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.nullIfBlank
@@ -4,13 +4,13 @@ import android.content.Context
import android.graphics.Bitmap
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreConfigManager
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.dto.SubscriptionUpdateResult
import com.v2ray.ang.dto.UrlContentRequest
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.fmt.CustomFmt
@@ -30,6 +30,21 @@ import java.net.URI
object AngConfigManager {
// Parser mapping for different config types (lazy initialized)
private val configFmtParsers: Map<String, (String) -> ProfileItem?> by lazy {
mapOf(
EConfigType.VMESS.protocolScheme to VmessFmt::parse,
EConfigType.SHADOWSOCKS.protocolScheme to ShadowsocksFmt::parse,
EConfigType.SOCKS.protocolScheme to SocksFmt::parse,
AppConfig.SOCKS4 to SocksFmt::parse,
AppConfig.SOCKS5 to SocksFmt::parse,
EConfigType.TROJAN.protocolScheme to TrojanFmt::parse,
EConfigType.VLESS.protocolScheme to VlessFmt::parse,
EConfigType.WIREGUARD.protocolScheme to WireguardFmt::parse,
EConfigType.HYSTERIA2.protocolScheme to Hysteria2Fmt::parse,
AppConfig.HY2 to Hysteria2Fmt::parse
)
}
/**
* Shares the configuration to the clipboard.
@@ -310,6 +325,16 @@ object AngConfigManager {
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
if (keyToProfile.isEmpty() || target == null) return null
// Level 0: Full match (remarks + server + port + password)
if (target.remarks.isNotBlank()) {
keyToProfile.entries.firstOrNull { (_, saved) ->
isSameText(saved.remarks, target.remarks) &&
isSameText(saved.server, target.server) &&
isSameText(saved.serverPort, target.serverPort) &&
isSameText(saved.password, target.password)
}?.key?.let { return it }
}
// Level 1: Match by remarks
if (target.remarks.isNotBlank()) {
keyToProfile.entries.firstOrNull { (_, saved) ->
@@ -442,22 +467,8 @@ object AngConfigManager {
return null
}
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
VmessFmt.parse(str)
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
ShadowsocksFmt.parse(str)
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
SocksFmt.parse(str)
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
TrojanFmt.parse(str)
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
VlessFmt.parse(str)
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
WireguardFmt.parse(str)
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
Hysteria2Fmt.parse(str)
} else {
null
val config = configFmtParsers.firstNotNullOfOrNull { (scheme, parser) ->
if (str.startsWith(scheme)) parser(str) else null
}
if (config == null) {
@@ -535,14 +546,28 @@ object AngConfigManager {
var configText = try {
val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort, proxyUsername, proxyPassword)
HttpUtil.getUrlContentWithUserAgent(
UrlContentRequest(
url = url,
userAgent = userAgent,
timeout = 15000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
)
)
} catch (e: Exception) {
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
""
}
if (configText.isEmpty()) {
configText = try {
HttpUtil.getUrlContentWithUserAgent(url, userAgent)
HttpUtil.getUrlContentWithUserAgent(
UrlContentRequest(
url = url,
userAgent = userAgent
)
)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
""
@@ -4,14 +4,14 @@ import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.DEFAULT_SUBSCRIPTION_ID
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
import com.v2ray.ang.dto.AssetUrlCache
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerAffiliationInfo
import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.WebDavConfig
import com.v2ray.ang.dto.entities.AssetUrlCache
import com.v2ray.ang.dto.entities.AssetUrlItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.dto.entities.ServerAffiliationInfo
import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.dto.entities.WebDavConfig
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
@@ -14,7 +14,7 @@ import androidx.core.app.NotificationCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.extension.toSpeedString
import com.v2ray.ang.ui.MainActivity
import com.v2ray.ang.util.LogUtil
@@ -43,53 +43,15 @@ object NotificationManager {
* Starts the speed notification.
* @param currentConfig The current profile configuration.
*/
fun startSpeedNotification(currentConfig: ProfileItem?) {
fun startSpeedNotification() {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) return
if (speedNotificationJob != null || CoreServiceManager.isRunning() == false) return
var lastZeroSpeed = false
val outboundTags = currentConfig?.getAllOutboundTags()
outboundTags?.remove(AppConfig.TAG_DIRECT)
speedNotificationJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
val queryTime = System.currentTimeMillis()
val sinceLastQueryIn = (queryTime - lastQueryTime)
// If the query interval is too short, skip this round to avoid excessive CPU usage
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
lastQueryTime = queryTime
delay(QUERY_INTERVAL_MS)
continue
}
val sinceLastQueryInSeconds = sinceLastQueryIn / 1000.0
var proxyTotal = 0L
val text = StringBuilder()
outboundTags?.forEach {
val up = CoreServiceManager.queryStats(it, AppConfig.UPLINK)
val down = CoreServiceManager.queryStats(it, AppConfig.DOWNLINK)
if (up + down > 0) {
appendSpeedString(text, it, up / sinceLastQueryInSeconds, down / sinceLastQueryInSeconds)
proxyTotal += up + down
}
}
val directUplink = CoreServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.UPLINK)
val directDownlink = CoreServiceManager.queryStats(AppConfig.TAG_DIRECT, AppConfig.DOWNLINK)
val zeroSpeed = proxyTotal == 0L && directUplink == 0L && directDownlink == 0L
if (!zeroSpeed || !lastZeroSpeed) {
if (proxyTotal == 0L) {
appendSpeedString(text, outboundTags?.firstOrNull(), 0.0, 0.0)
}
appendSpeedString(
text, AppConfig.TAG_DIRECT, directUplink / sinceLastQueryInSeconds,
directDownlink / sinceLastQueryInSeconds
)
updateNotification(text.toString(), proxyTotal, directDownlink + directUplink)
}
lastZeroSpeed = zeroSpeed
lastQueryTime = queryTime
lastZeroSpeed = updateSpeedNotificationOnce(lastZeroSpeed)
delay(QUERY_INTERVAL_MS)
}
}
@@ -168,13 +130,12 @@ object NotificationManager {
/**
* Stops the speed notification.
* @param currentConfig The current profile configuration.
*/
fun stopSpeedNotification(currentConfig: ProfileItem?) {
fun stopSpeedNotification() {
speedNotificationJob?.let {
it.cancel()
speedNotificationJob = null
updateNotification(currentConfig?.remarks, 0, 0)
updateNotification("", 0, 0)
}
}
@@ -247,6 +208,69 @@ object NotificationManager {
text.append("${up.toLong().toSpeedString()}${down.toLong().toSpeedString()}\n")
}
/**
* Updates the speed notification once.
* Queries traffic stats, separates proxy and direct, and updates the notification.
* @param lastZeroSpeed The previous zero speed state.
* @return The current zero speed state.
*/
private fun updateSpeedNotificationOnce(lastZeroSpeed: Boolean): Boolean {
val queryTime = System.currentTimeMillis()
val sinceLastQueryIn = (queryTime - lastQueryTime)
// If the query interval is too short, skip this round to avoid excessive CPU usage
if (sinceLastQueryIn < QUERY_INTERVAL_MS) {
LogUtil.w(AppConfig.TAG, "Query interval too short: ${sinceLastQueryIn}ms, skipping")
lastQueryTime = queryTime
return lastZeroSpeed
}
val sinceLastQueryInSeconds = sinceLastQueryIn / 1000.0
var proxyUplink = 0L
var proxyDownlink = 0L
var directUplink = 0L
var directDownlink = 0L
CoreServiceManager.queryAllOutboundTrafficStats().forEach { stat ->
when {
stat.tag == AppConfig.TAG_DIRECT -> {
when (stat.direction) {
AppConfig.UPLINK -> directUplink += stat.value
AppConfig.DOWNLINK -> directDownlink += stat.value
}
}
stat.tag.startsWith(AppConfig.TAG_PROXY) -> {
when (stat.direction) {
AppConfig.UPLINK -> proxyUplink += stat.value
AppConfig.DOWNLINK -> proxyDownlink += stat.value
}
}
}
}
val proxyTotal = proxyUplink + proxyDownlink
val directTotal = directUplink + directDownlink
val zeroSpeed = proxyTotal + directTotal == 0L
if (!zeroSpeed || !lastZeroSpeed) {
val text = StringBuilder()
appendSpeedString(
text, AppConfig.TAG_PROXY,
proxyUplink / sinceLastQueryInSeconds,
proxyDownlink / sinceLastQueryInSeconds
)
appendSpeedString(
text, AppConfig.TAG_DIRECT,
directUplink / sinceLastQueryInSeconds,
directDownlink / sinceLastQueryInSeconds
)
updateNotification(text.toString(), proxyTotal, directTotal)
}
lastQueryTime = queryTime
return zeroSpeed
}
/**
* Gets the service instance.
* @return The service instance.
@@ -12,9 +12,9 @@ import com.v2ray.ang.AppConfig.GEOIP_PRIVATE
import com.v2ray.ang.AppConfig.GEOSITE_PRIVATE
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.VPN
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.Language
@@ -1,9 +1,8 @@
package com.v2ray.ang.handler
import android.os.SystemClock
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.IPAPIInfo
import com.v2ray.ang.dto.UrlContentRequest
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
@@ -90,7 +89,15 @@ object SpeedtestManager {
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
if (httpPort == 0) return null
val content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: return null
val content = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
)
) ?: return null
val ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
val ip = listOf(
@@ -13,10 +13,10 @@ import androidx.work.workDataOf
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.enums.NotificationChannelType
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.NotificationHelper
import com.v2ray.ang.enums.NotificationChannelType
import java.util.concurrent.TimeUnit
object SubscriptionUpdater {
@@ -1,19 +1,17 @@
package com.v2ray.ang.handler
import android.content.Context
import android.os.Build
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.dto.UrlContentRequest
import com.v2ray.ang.extension.concatUrl
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.LogUtil
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) {
@@ -26,10 +24,23 @@ object UpdateCheckerManager {
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var response = HttpUtil.getUrlContent(url, 5000)
var response = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000
)
)
if (response.isNullOrEmpty()) {
val httpPort = SettingsManager.getHttpPort()
response = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword)
response = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
)
)
?: throw IllegalStateException("Failed to get response")
}
@@ -1,7 +1,7 @@
package com.v2ray.ang.handler
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.WebDavConfig
import com.v2ray.ang.dto.entities.WebDavConfig
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -4,9 +4,9 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.v2ray.ang.AppConfig
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SubscriptionUpdater
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.util.LogUtil
class BootReceiver : BroadcastReceiver() {
@@ -6,8 +6,8 @@ import android.content.Intent
import android.os.IBinder
import com.v2ray.ang.AppConfig
import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MyContextWrapper
import java.lang.ref.SoftReference
@@ -5,15 +5,15 @@ import android.content.Intent
import android.os.IBinder
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreNativeManager
import com.v2ray.ang.dto.RealPingEvent
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.enums.NotificationChannelType
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.core.CoreNativeManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.NotificationHelper
import com.v2ray.ang.enums.NotificationChannelType
import java.util.Collections
class CoreTestService : Service() {
@@ -68,7 +68,9 @@ class CoreTestService : Service() {
when (message.key) {
AppConfig.MSG_MEASURE_CONFIG_START -> handleMeasureStart(message, startId)
AppConfig.MSG_MEASURE_CONFIG_CANCEL -> handleMeasureCancel()
else -> { NotificationHelper.stopForeground(this); stopSelf(startId) }
else -> {
NotificationHelper.stopForeground(this); stopSelf(startId)
}
}
return START_NOT_STICKY
}
@@ -94,7 +96,7 @@ class CoreTestService : Service() {
worker = RealPingWorkerService(
context = this,
guids = guidsList,
onEvent = { event -> handleWorkerEvent(event) { activeWorkers.remove(worker) } }
onEvent = { event -> handleWorkerEvent(event) { activeWorkers.remove(worker) } }
)
activeWorkers.add(worker)
worker.start()
@@ -114,10 +116,12 @@ class CoreTestService : Service() {
)
MessageUtil.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, event.text)
}
is RealPingEvent.Result -> {
MmkvManager.encodeServerTestDelayMillis(event.guid, event.delayMillis)
MessageUtil.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, event.guid)
}
is RealPingEvent.Finish -> {
MessageUtil.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_FINISH, event.status)
onWorkerDone()
@@ -20,10 +20,10 @@ import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.contracts.Tun2SocksControl
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.NotificationManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils
@@ -113,6 +113,7 @@ class CoreVpnService : VpnService(), ServiceControl {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
LogUtil.i(AppConfig.TAG, "StartCore-VPN: Service command received")
NotificationManager.showNotification(null)
setupVpnService()
startService()
return START_STICKY
@@ -0,0 +1,774 @@
package com.v2ray.ang.service
import android.content.Context
import com.v2ray.ang.AppConfig
import com.v2ray.ang.util.LogUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.json.JSONArray
import org.json.JSONObject
import java.io.EOFException
import java.net.SocketException
import java.net.URI
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
// This class is responsible for forwarding xray's HTTP/WS requests through okhttp,
// so that its TLS and traffic characteristics are okhttp instead of golang/utls.
// Only WS and xhttp package-up are supported for now.
// ws:
// DialerNativeService connects to the control WebSocket provided by the xray core.
// Then xray sends a task message with method "WS" and the server URL to the control WebSocket.
// DialerNativeService opens a WebSocket connection to the server URL, and forwards messages between the control WebSocket and the target WebSocket.
// xhttp(package-up):
// A task message with streaming down (method == "GET" and streamResponse == true), let's call it Task A.
// A task message with unary down (streamResponse == false), let's call it Task B.
// 1. DialerNativeService connects to the control WebSocket provided by the xray core.
// 2. Xray sends Task A, DialerNativeService sends "ok" and connects to the target URL, then sends the "GET" request to the server; let's call this "Connection A".
// 3. Xray sends a Task B, called B_1. DialerNativeService sends "ok" and sends the data body to the target URL. Whatever the response is, DialerNativeService sends "ok" or "fail" back to the control WebSocket and closes the connection for Task B_1.
// 4. The server returns the response for Task B_1 through Connection A, and DialerNativeService forwards the response body to the control WebSocket.
// 5. Xray sends another Task B, called B_2.
// ...
// Finally, the xray client core sends all data through the B_1, B_2, ... tasks.
// The server closes Connection A, and DialerNativeService closes Task A.
// The above is a complete cycle.
class DialerNativeService : IDialerService {
companion object {
private const val DEBUG_LOG = false
private val NEXT_SOCKET_ID = AtomicLong(0L)
private const val CONTROL_SOCKET_IDLE = 0
private const val CONTROL_SOCKET_OPENING = 1
private const val CONTROL_LOOP_DELAY_MS = 1000L
private const val UNARY_BODY_WAIT_TIMEOUT_MS = 15_000L
private val TOKEN_REGEX = Regex("""/websocket\?token=([^"'\s]+)""")
private val METHODS_WITHOUT_BODY = setOf("GET", "HEAD")
private val HEADERS_BLACKLIST = hashSetOf(
// AI suggest:
"host",
"content-length",
"transfer-encoding",
"content-encoding",
"connection",
"upgrade",
"sec-websocket-key",
"sec-websocket-version",
"sec-websocket-protocol",
"Timing-Allow-Origin",
// xray
"Set-Cookie",
"Cookie",
"Origin",
"Sec-CH-UA",
"Sec-CH-UA-Mobile",
"Sec-CH-UA-Platform",
"DNT",
"User-Agent",
"Accept-Language",
"Cache-Control",
"Upgrade-Insecure-Requests",
"Sec-Fetch-Site",
"Sec-Fetch-Mode",
"Sec-Fetch-User",
"Sec-Fetch-Dest",
"Referer",
"Accept",
"Priority",
"Pragma",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Expose-Headers",
"Access-Control-Max-Age",
)
}
@Volatile
private var serviceJob = SupervisorJob()
@Volatile
private var scope = CoroutineScope(serviceJob + Dispatchers.IO)
private val running = AtomicBoolean(false)
private val controlSocketState = AtomicInteger(CONTROL_SOCKET_IDLE)
private val controlSockets = ConcurrentHashMap.newKeySet<WebSocket>()
@Volatile
private var controlUrl: String? = null
private var loopJob: Job? = null
private var client: OkHttpClient? = null
@Suppress("UNUSED_PARAMETER")
override fun start(context: Context, dialerAddr: String) {
stop()
serviceJob = SupervisorJob()
scope = CoroutineScope(serviceJob + Dispatchers.IO)
if (dialerAddr.isEmpty()) return
val nativeClient = OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.pingInterval(25, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS) // Disable read timeout for long-running streams
.build()
client = nativeClient
running.set(true)
loopJob = scope.launch {
val resolvedControlUrl = resolveControlWsUrl(dialerAddr, nativeClient)
if (resolvedControlUrl == null) {
debug(
"BrowserDialer: failed to resolve control url from dialer endpoint: $dialerAddr"
)
running.set(false)
return@launch
}
controlUrl = resolvedControlUrl
debug("BrowserDialer: started dialerAddr=$dialerAddr controlUrl=$resolvedControlUrl idleGate=${controlSocketState.get()}")
maintainControlSocketPool()
while (isActive && running.get()) {
maintainControlSocketPool()
delay(CONTROL_LOOP_DELAY_MS)
}
}
}
override fun stop() {
running.set(false)
loopJob?.cancel()
loopJob = null
serviceJob.cancel()
debug("BrowserDialer: stopping ${poolState()} controlUrl=$controlUrl")
controlSockets.toTypedArray().forEach { socket ->
runCatching { socket.close(1000, "stopped") }
}
controlSockets.clear()
controlSocketState.set(CONTROL_SOCKET_IDLE)
controlUrl = null
serviceJob = SupervisorJob()
scope = CoroutineScope(serviceJob + Dispatchers.IO)
val oldClient = client
client = null
oldClient?.dispatcher?.cancelAll()
oldClient?.connectionPool?.evictAll()
}
private fun maintainControlSocketPool() {
debug("BrowserDialer: maintaining single idle control socket ${poolState()}")
openControlSocket()
}
private fun openControlSocket(): Boolean {
val localClient = client ?: return false
if (!running.get()) return false
val url = controlUrl ?: return false
if (!controlSocketState.compareAndSet(
CONTROL_SOCKET_IDLE,
CONTROL_SOCKET_OPENING
)
) return false
val request = Request.Builder().url(url).build()
return runCatching {
val socket = localClient.newWebSocket(request, ControlSocketListener(url))
controlSockets.add(socket)
debug("BrowserDialer: opening control socket url=$url ${poolState()}")
true
}.getOrElse {
controlSocketState.set(CONTROL_SOCKET_IDLE)
debug("BrowserDialer: failed to open control socket", it)
false
}
}
private fun poolState(): String {
return "idleGate=${controlSocketState.get()} liveSockets=${controlSockets.size}"
}
private fun debug(message: String, throwable: Throwable? = null) {
@Suppress("KotlinConstantConditions")
if (!DEBUG_LOG) return
if (throwable == null) {
LogUtil.d(AppConfig.TAG, message)
} else {
LogUtil.d(AppConfig.TAG, message, throwable)
}
}
private fun resolveControlWsUrl(rawAddr: String, probeClient: OkHttpClient): String? {
val uri = parseDialerUri(rawAddr) ?: return null
val probeUrl = buildDialerProbeUrl(rawAddr) ?: return null
val request = Request.Builder().url(probeUrl).get().build()
val token = runCatching {
probeClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) return null
extractControlToken(response.body.string())
}
}.getOrNull() ?: return null
val host = uri.host ?: return null
return URI(
"ws",
uri.userInfo,
host,
uri.port,
"/websocket",
"token=$token",
null
).toString()
}
private fun buildDialerProbeUrl(rawAddr: String): String? {
val normalized = rawAddr.trim()
if (normalized.isEmpty()) return null
val uri = parseDialerUri(normalized) ?: return null
val host = uri.host ?: return null
val probeScheme = when (uri.scheme?.lowercase()) {
"https", "wss" -> "https"
else -> "http"
}
return URI(probeScheme, uri.userInfo, host, uri.port, "/", null, null).toString()
}
private fun parseDialerUri(rawAddr: String): URI? {
val normalized = rawAddr.trim()
if (normalized.isEmpty()) return null
return runCatching {
if (normalized.contains("://")) URI(normalized) else URI("http://$normalized")
}.getOrNull()
}
private fun extractControlToken(html: String): String? {
val match = TOKEN_REGEX.find(html) ?: return null
return match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }
}
private inner class ControlSocketListener(
private val controlUrl: String
) : WebSocketListener() {
private val socketId = if (DEBUG_LOG) NEXT_SOCKET_ID.incrementAndGet() else 0L
private val taskAccepted = AtomicBoolean(false)
private val closed = AtomicBoolean(false)
private val taskStartedAtMs = AtomicLong(0L)
@Volatile
private var taskKind = "none"
private var upstreamSocket: WebSocket? = null
private var upstreamCall: Call? = null
private var timeoutJob: Job? = null
private var binaryHandler: ((ByteArray) -> Unit)? = null
private var textHandler: ((String) -> Unit)? = null
override fun onOpen(webSocket: WebSocket, response: Response) {
debug("BrowserDialer: control socket opened socketId=$socketId url=$controlUrl ${poolState()}")
}
override fun onMessage(webSocket: WebSocket, text: String) {
if (taskAccepted.compareAndSet(false, true)) {
controlSocketState.set(CONTROL_SOCKET_IDLE)
debug(
"BrowserDialer: control socket accepted task socketId=$socketId url=$controlUrl textSize=${text.length} ${poolState()}"
)
tryOpenNextControlSocket()
handleTask(webSocket, BrowserDialerTask.parse(text))
return
}
textHandler?.invoke(text)
}
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
if (!taskAccepted.get()) {
failAndClose(webSocket, 1002, "task must be text json")
return
}
binaryHandler?.invoke(bytes.toByteArray())
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
cleanup(webSocket)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
val status = response?.code?.toString() ?: "no-http-response"
val stateText = "${poolState()} accepted=${taskAccepted.get()} closed=${closed.get()}"
if (isExpectedControlFailure(t, status)) {
debug(
"BrowserDialer: control socket closed socketId=$socketId url=$controlUrl status=$status cause=${t.javaClass.simpleName} $stateText"
)
debug(
"BrowserDialer: control socket failure detail socketId=$socketId url=$controlUrl status=$status $stateText",
t
)
} else {
debug(
"BrowserDialer: control socket failure socketId=$socketId url=$controlUrl status=$status $stateText",
t
)
}
cleanup(webSocket)
}
private fun isExpectedControlFailure(t: Throwable, status: String): Boolean {
if (!running.get() || closed.get()) return true
if (status != "no-http-response") return false
if (t is EOFException) return true
if (t is SocketException) {
val message = t.message.orEmpty().lowercase()
if ("socket closed" in message || "software caused connection abort" in message) {
return true
}
}
val message = t.message.orEmpty().lowercase()
return "canceled" in message || "cancelled" in message
}
private fun tryOpenNextControlSocket() {
scope.launch {
if (running.get()) maintainControlSocketPool()
}
}
private fun cleanup(webSocket: WebSocket) {
if (!closed.compareAndSet(false, true)) return
val removed = controlSockets.remove(webSocket)
if (!taskAccepted.get()) {
controlSocketState.set(CONTROL_SOCKET_IDLE)
tryOpenNextControlSocket()
}
val started = taskStartedAtMs.get()
val duration =
if (started > 0L) (System.currentTimeMillis() - started).coerceAtLeast(0L) else -1L
debug(
"BrowserDialer: cleanup socketId=$socketId url=$controlUrl task=$taskKind taskAccepted=${taskAccepted.get()} removed=$removed durationMs=$duration ${poolState()}"
)
binaryHandler = null
textHandler = null
timeoutJob?.cancel()
timeoutJob = null
upstreamCall?.cancel()
upstreamCall = null
upstreamSocket?.close(1000, "control closed")
upstreamSocket = null
taskKind = "closed"
taskStartedAtMs.set(0L)
}
private fun handleTask(webSocket: WebSocket, task: BrowserDialerTask?) {
if (task == null) {
failAndClose(webSocket, 1007, "invalid task")
return
}
taskStartedAtMs.set(System.currentTimeMillis())
taskKind = when {
task.method == "WS" -> "ws"
task.method == "GET" && task.streamResponse -> "streaming_get"
!task.streamResponse -> "unary_${task.method.lowercase()}"
else -> "unsupported"
}
when {
task.method == "WS" -> handleWsTask(webSocket, task)
task.method == "GET" && task.streamResponse -> handleStreamingGetTask(
webSocket,
task
)
!task.streamResponse -> handleUnaryTask(webSocket, task)
else -> {
failAndClose(webSocket, 1003, "unsupported task")
}
}
}
private fun handleWsTask(controlSocket: WebSocket, task: BrowserDialerTask) {
val localClient = client ?: run {
failAndClose(controlSocket, 1011, "client unavailable")
return
}
debug("BrowserDialer: handling WS task socketId=$socketId url=${task.url} protocols=${task.extra.protocols.size}")
val requestBuilder = Request.Builder().url(task.url)
if (task.extra.protocols.isNotEmpty()) {
requestBuilder.header(
"Sec-WebSocket-Protocol",
task.extra.protocols.joinToString(",")
)
}
val opened = AtomicBoolean(false)
upstreamSocket =
localClient.newWebSocket(requestBuilder.build(), object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
opened.set(true)
try {
controlSocket.send("ok")
} catch (e: Exception) {
debug(
"BrowserDialer: failed to send ok for WS task",
e
)
webSocket.close(1000, "control failed")
}
}
override fun onMessage(webSocket: WebSocket, text: String) {
try {
controlSocket.send(text)
} catch (e: Exception) {
debug(
"BrowserDialer: control socket closed during WS message transfer socketId=$socketId",
e
)
webSocket.close(1000, "control closed")
}
}
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
try {
controlSocket.send(bytes)
} catch (e: Exception) {
debug(
"BrowserDialer: control socket closed during WS binary transfer socketId=$socketId",
e
)
webSocket.close(1000, "control closed")
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
try {
controlSocket.close(1000, "upstream closed")
} catch (_: Exception) {
debug("BrowserDialer: control socket already closed")
}
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?
) {
if (!opened.get()) {
try {
controlSocket.send("fail")
} catch (e: Exception) {
debug(
"BrowserDialer: control socket send failed socketId=$socketId",
e
)
}
}
try {
controlSocket.close(1011, "upstream failure")
} catch (_: Exception) {
debug(
"BrowserDialer: control socket already closed socketId=$socketId"
)
}
}
})
textHandler = { message ->
try {
upstreamSocket?.send(message)
} catch (e: Exception) {
debug(
"BrowserDialer: upstream socket send failed socketId=$socketId",
e
)
}
}
binaryHandler = { data ->
try {
upstreamSocket?.send(data.toByteString())
} catch (e: Exception) {
debug(
"BrowserDialer: upstream socket binary send failed socketId=$socketId",
e
)
}
}
}
private fun handleStreamingGetTask(controlSocket: WebSocket, task: BrowserDialerTask) {
val localClient = client ?: run {
failAndClose(controlSocket, 1011, "client unavailable")
return
}
debug("BrowserDialer: handling streaming GET task socketId=$socketId url=${task.url}")
val request = buildRequest(task, null)
try {
controlSocket.send("ok")
} catch (e: Exception) {
debug(
"BrowserDialer: failed to send ok for streaming GET socketId=$socketId",
e
)
return
}
scope.launch {
val call = localClient.newCall(request)
upstreamCall = call
try {
call.execute().use { response ->
val source = response.body.source()
val buffer = Buffer()
while (running.get() && !closed.get()) {
try {
val read = source.read(buffer, DEFAULT_BUFFER_SIZE.toLong())
if (read < 0) break
// Send data, catch exception if WebSocket is closed
try {
controlSocket.send(buffer.readByteString())
} catch (e: Exception) {
debug(
"BrowserDialer: WebSocket send failed during streaming, stopping stream socketId=$socketId",
e
)
break
}
} catch (_: Exception) {
// Error reading from source, stop streaming
break
}
}
}
} catch (e: Exception) {
debug(
"BrowserDialer: streaming GET failed socketId=$socketId",
e
)
try {
controlSocket.send("fail")
} catch (_: Exception) {
// WebSocket may already be closed
}
} finally {
upstreamCall = null
try {
controlSocket.close(1000, "streaming done")
} catch (_: Exception) {
debug(
"BrowserDialer: WebSocket already closed socketId=$socketId"
)
}
}
}
}
private fun handleUnaryTask(controlSocket: WebSocket, task: BrowserDialerTask) {
val localClient = client ?: run {
failAndClose(controlSocket, 1011, "client unavailable")
return
}
debug("BrowserDialer: handling unary task socketId=$socketId method=${task.method} url=${task.url}")
try {
controlSocket.send("ok")
} catch (e: Exception) {
debug(
"BrowserDialer: failed to send ok for unary task socketId=$socketId",
e
)
return
}
val done = AtomicBoolean(false)
timeoutJob = scope.launch {
delay(UNARY_BODY_WAIT_TIMEOUT_MS)
if (done.compareAndSet(false, true)) {
binaryHandler = null
textHandler = null
debug("BrowserDialer: unary task timed out waiting for payload socketId=$socketId method=${task.method} url=${task.url}")
failAndClose(controlSocket, 1000, "unary payload timeout")
}
}
val executeRequest: (ByteArray?) -> Unit = { payload ->
if (done.compareAndSet(false, true)) {
timeoutJob?.cancel()
timeoutJob = null
binaryHandler = null
textHandler = null
scope.launch {
val request = buildRequest(task, payload)
val call = localClient.newCall(request)
upstreamCall = call
try {
call.execute().use { response ->
try {
controlSocket.send(if (response.isSuccessful) "ok" else "fail")
} catch (e: Exception) {
debug(
"BrowserDialer: WebSocket send failed for unary response socketId=$socketId",
e
)
}
}
} catch (e: Exception) {
debug(
"BrowserDialer: unary request failed socketId=$socketId",
e
)
try {
controlSocket.send("fail")
} catch (_: Exception) {
// WebSocket may already be closed
}
} finally {
upstreamCall = null
try {
controlSocket.close(1000, "request done")
} catch (_: Exception) {
debug(
"BrowserDialer: WebSocket already closed socketId=$socketId"
)
}
}
}
}
}
binaryHandler = { body -> executeRequest(body.takeIf { it.isNotEmpty() }) }
textHandler = { body -> executeRequest(body.toByteArray().takeIf { it.isNotEmpty() }) }
}
private fun buildRequest(task: BrowserDialerTask, payload: ByteArray?): Request {
val requestBuilder = Request.Builder().url(task.url)
// task.extra.headers.forEach { (key, value) -> requestBuilder.header(key, value) }
// Just set no cache headers
requestBuilder.header("Cache-Control", "no-cache, no-store, must-revalidate")
task.extra.referrer?.takeIf { it.isNotBlank() }
?.let { requestBuilder.header("Referer", it) }
val taskHeaders = task.extra.headers.filterKeys { key ->
val lowerKey = key.lowercase()
!HEADERS_BLACKLIST.any { blackKey -> blackKey.equals(lowerKey, ignoreCase = true) }
}
taskHeaders.forEach { (key, value) -> requestBuilder.header(key, value) }
val method = task.method.uppercase()
val methodAllowsBody = method !in METHODS_WITHOUT_BODY
val body = when {
methodAllowsBody && payload != null && payload.isNotEmpty() -> payload.toRequestBody(null)
methodAllowsBody -> ByteArray(0).toRequestBody(null)
else -> null
}
requestBuilder.method(method, body)
return requestBuilder.build()
}
private fun failAndClose(socket: WebSocket, code: Int, reason: String) {
try {
socket.send("fail")
} catch (e: Exception) {
debug("BrowserDialer: failed to send fail message", e)
}
try {
socket.close(code, reason)
} catch (e: Exception) {
debug("BrowserDialer: failed to close socket", e)
}
}
}
private data class BrowserDialerTask(
val method: String,
val url: String,
val streamResponse: Boolean,
val extra: Extra
) {
data class Extra(
val headers: Map<String, String> = emptyMap(),
// val cookies: Map<String, String> = emptyMap(),
val protocols: List<String> = emptyList(),
val referrer: String? = null
)
companion object {
fun parse(payload: String): BrowserDialerTask? {
return runCatching {
val root = JSONObject(payload)
val method = root.optString("method")
val url = root.optString("url")
if (method.isBlank() || url.isBlank()) return null
val streamResponse = root.optBoolean("streamResponse", false)
val extraObject = root.optJSONObject("extra")
val headers = extraObject.optStringMap("headers")
// val cookies = extraObject.optStringMap("cookies")
val referrer = extraObject?.optString("referrer")?.takeIf { it.isNotBlank() }
val protocols = extraObject.optProtocols()
BrowserDialerTask(
method = method,
url = url,
streamResponse = streamResponse,
extra = Extra(
headers = headers,
// cookies = cookies,
protocols = protocols,
referrer = referrer
)
)
}.getOrNull()
}
private fun JSONObject?.optStringMap(name: String): Map<String, String> {
val child = this?.optJSONObject(name) ?: return emptyMap()
val map = LinkedHashMap<String, String>()
val iter = child.keys()
while (iter.hasNext()) {
val key = iter.next()
map[key] = child.optString(key)
}
return map
}
private fun JSONObject?.optProtocols(): List<String> {
val raw = this?.opt("protocol") ?: return emptyList()
return when (raw) {
is String -> raw.takeIf { it.isNotBlank() }?.let { listOf(it) } ?: emptyList()
is JSONArray -> buildList {
for (i in 0 until raw.length()) {
val item = raw.optString(i)
if (item.isNotBlank()) {
add(item)
}
}
}
else -> emptyList()
}
}
}
}
}
@@ -0,0 +1,73 @@
package com.v2ray.ang.service
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
class DialerWebviewService : IDialerService {
private var webView: WebView? = null
private val handler = Handler(Looper.getMainLooper())
private val keepAliveInterval = 30_000L // 30 seconds
private val keepAliveRunnable = object : Runnable {
override fun run() {
webView?.let {
it.resumeTimers()
it.onResume()
}
handler.postDelayed(this, keepAliveInterval)
}
}
/**
* Starts the WebView.
* @param context Service context
*/
@SuppressLint("SetJavaScriptEnabled")
override fun start(context: Context, dialerAddr: String) {
if (webView != null) stop()
if (dialerAddr.isEmpty()) return
val dialerUrl = "http://$dialerAddr/"
webView = WebView(context.applicationContext).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
// Allow JS to run even if not triggered by user
mediaPlaybackRequiresUserGesture = false
// Prevent aggressive caching issues
cacheMode = WebSettings.LOAD_DEFAULT
}
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
view?.onResume()
view?.resumeTimers()
}
}
loadUrl(dialerUrl)
}
handler.post(keepAliveRunnable)
}
override fun stop() {
handler.removeCallbacks(keepAliveRunnable)
webView?.apply {
stopLoading()
pauseTimers()
// Important to call onPause to stop internal Chromium threads properly
onPause()
destroy()
}
webView = null
}
}
@@ -0,0 +1,8 @@
package com.v2ray.ang.service
import android.content.Context
interface IDialerService {
fun start(context: Context, dialerAddr: String)
fun stop()
}
@@ -1,10 +1,10 @@
package com.v2ray.ang.service
import android.content.Context
import com.v2ray.ang.core.CoreConfigManager
import com.v2ray.ang.core.CoreNativeManager
import com.v2ray.ang.dto.RealPingEvent
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.core.CoreNativeManager
import com.v2ray.ang.core.CoreConfigManager
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
@@ -4,8 +4,8 @@ import android.os.Bundle
import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.core.CoreNativeManager
import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.util.Utils
class AboutActivity : BaseActivity() {
@@ -14,7 +14,6 @@ import com.v2ray.ang.databinding.ActivityAppPickerBinding
import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.PackageUidResolver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -12,7 +12,7 @@ import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBackupBinding
import com.v2ray.ang.databinding.DialogWebdavBinding
import com.v2ray.ang.dto.WebDavConfig
import com.v2ray.ang.dto.entities.WebDavConfig
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
@@ -6,6 +6,7 @@ import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreNativeManager
import com.v2ray.ang.databinding.ActivityCheckUpdateBinding
import com.v2ray.ang.dto.CheckUpdateResult
import com.v2ray.ang.extension.toast
@@ -13,7 +14,6 @@ import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.UpdateCheckerManager
import com.v2ray.ang.core.CoreNativeManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.launch
@@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
@@ -16,13 +17,14 @@ import com.v2ray.ang.R
import com.v2ray.ang.contracts.MainAdapterListener
import com.v2ray.ang.databinding.FragmentGroupServerBinding
import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.viewmodel.MainViewModel
@@ -44,6 +46,11 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
private val share_method_more: Array<out String> by lazy {
ownerActivity.resources.getStringArray(R.array.share_method_more)
}
private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (SettingsChangeManager.consumeRestartService() && mainViewModel.isRunning.value == true) {
ownerActivity.restartV2Ray()
}
}
companion object {
private const val ARG_SUB_ID = "subscriptionId"
@@ -168,27 +175,20 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
* @param profile The server configuration
*/
private fun editServer(guid: String, profile: ProfileItem) {
val intent = Intent().putExtra("guid", guid)
val activityClass = when (profile.configType) {
EConfigType.CUSTOM -> ServerCustomConfigActivity::class.java
EConfigType.POLICYGROUP -> ServerGroupActivity::class.java
EConfigType.PROXYCHAIN -> ServerProxyChainActivity::class.java
else -> ServerActivity::class.java
}
val intent = Intent(ownerActivity, activityClass)
.putExtra("guid", guid)
.putExtra("isRunning", mainViewModel.isRunning.value)
.putExtra("createConfigType", profile.configType.value)
.putExtra("subscriptionId", subId)
when (profile.configType) {
EConfigType.CUSTOM -> {
ownerActivity.startActivity(intent.setClass(ownerActivity, ServerCustomConfigActivity::class.java))
}
EConfigType.POLICYGROUP -> {
ownerActivity.startActivity(intent.setClass(ownerActivity, ServerGroupActivity::class.java))
}
EConfigType.PROXYCHAIN -> {
ownerActivity.startActivity(intent.setClass(ownerActivity, ServerProxyChainActivity::class.java))
}
else -> {
ownerActivity.startActivity(intent.setClass(ownerActivity, ServerActivity::class.java))
}
}
launcher.launch(intent)
}
/**
@@ -22,6 +22,7 @@ import com.google.android.material.navigation.NavigationView
import com.google.android.material.tabs.TabLayoutMediator
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.PermissionType
@@ -32,7 +33,6 @@ import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.SubscriptionUpdater
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel
@@ -12,8 +12,8 @@ import com.v2ray.ang.R
import com.v2ray.ang.contracts.MainAdapterListener
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.ServersCache
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
@@ -14,6 +14,7 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBypassListBinding
import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.dto.UrlContentRequest
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.extension.v2RayApplication
@@ -189,12 +190,25 @@ class PerAppProxyActivity : BaseActivity() {
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000)
var content = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000
)
)
if (content.isNullOrEmpty()) {
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: ""
content = HttpUtil.getUrlContent(
UrlContentRequest(
url = url,
timeout = 5000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
)
) ?: ""
}
launch(Dispatchers.Main) {
//LogUtil.i(AppConfig.TAG, content)
@@ -11,7 +11,7 @@ import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
@@ -20,7 +20,7 @@ import com.v2ray.ang.AppConfig.TLS
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.extension.isNotNullEmpty
@@ -29,6 +29,7 @@ import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils
@@ -84,6 +85,9 @@ class ServerActivity : BaseActivity() {
private val xhttpMode: Array<out String> by lazy {
resources.getStringArray(R.array.xhttp_mode)
}
private val browserDialerModes: Array<out String> by lazy {
resources.getStringArray(R.array.browser_dialer_mode)
}
// Kotlin synthetics was used, but since it is removed in 1.8. We switch to old manual approach.
@@ -139,6 +143,8 @@ class ServerActivity : BaseActivity() {
private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
private val et_pinned_ca256: EditText? by lazy { findViewById(R.id.et_pinned_ca256) }
private val container_pinned_ca256: LinearLayout? by lazy { findViewById(R.id.lay_pinned_ca256) }
private val layout_browser_dialer: LinearLayout? by lazy { findViewById(R.id.layout_browser_dialer) }
private val sp_browser_dialer_mode: Spinner? by lazy { findViewById(R.id.sp_browser_dialer_mode) }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -254,6 +260,13 @@ class ServerActivity : BaseActivity() {
NetworkType.XHTTP.type -> View.VISIBLE
else -> View.GONE
}
layout_browser_dialer?.visibility =
when (networks[position]) {
NetworkType.WS.type -> View.VISIBLE
NetworkType.XHTTP.type -> View.VISIBLE
else -> View.GONE
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@@ -411,6 +424,12 @@ class ServerActivity : BaseActivity() {
if (network >= 0) {
sp_network?.setSelection(network)
}
val browserDialerMode = Utils.arrayFind(browserDialerModes, config.browserDialerMode.orEmpty())
if (browserDialerMode >= 0) {
sp_browser_dialer_mode?.setSelection(browserDialerMode)
}
return true
}
@@ -439,6 +458,7 @@ class ServerActivity : BaseActivity() {
et_local_address?.text =
Utils.getEditable(WIREGUARD_LOCAL_ADDRESS_V4)
et_local_mtu?.text = Utils.getEditable(WIREGUARD_LOCAL_MTU)
sp_browser_dialer_mode?.setSelection(0)
return true
}
@@ -507,6 +527,9 @@ class ServerActivity : BaseActivity() {
}
//LogUtil.i(AppConfig.TAG, JsonUtil.toJsonPretty(config) ?: "")
MmkvManager.encodeServerConfig(editGuid, config)
if (isRunning) {
SettingsChangeManager.makeRestartService()
}
toastSuccess(R.string.toast_success)
finish()
return true
@@ -568,6 +591,16 @@ class ServerActivity : BaseActivity() {
profileItem.finalMask = et_fm?.text?.toString()?.trim()?.nullIfBlank()
profileItem.kcpMtu = et_kcp_mtu?.text?.toString()?.toIntOrNull()
profileItem.kcpTti = et_kcp_tti?.text?.toString()?.toIntOrNull()
if (networks[network] == NetworkType.WS.type || networks[network] == NetworkType.XHTTP.type) {
val browserDialerMode = browserDialerModes[sp_browser_dialer_mode?.selectedItemPosition ?: 0]
if (browserDialerMode != browserDialerModes[0]) {
profileItem.browserDialerMode = browserDialerMode
} else {
profileItem.browserDialerMode = null
}
} else {
profileItem.browserDialerMode = null
}
}
private fun saveTls(config: ProfileItem) {
@@ -656,17 +689,9 @@ class ServerActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_server, menu)
val delButton = menu.findItem(R.id.del_config)
val saveButton = menu.findItem(R.id.save_config)
if (editGuid.isNotEmpty()) {
if (isRunning) {
delButton?.isVisible = false
saveButton?.isVisible = false
}
} else {
delButton?.isVisible = false
}
val delButton = menu.findItem(R.id.del_config)
delButton?.isVisible = editGuid.isNotEmpty() && !isRunning
return super.onCreateOptionsMenu(menu)
}
@@ -10,13 +10,14 @@ import com.blacksquircle.ui.language.json.JsonLanguage
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
@@ -94,6 +95,9 @@ class ServerCustomConfigActivity : BaseActivity() {
MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
if (isRunning) {
SettingsChangeManager.makeRestartService()
}
toastSuccess(R.string.toast_success)
finish()
return true
@@ -119,17 +123,9 @@ class ServerCustomConfigActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_server, menu)
val delButton = menu.findItem(R.id.del_config)
val saveButton = menu.findItem(R.id.save_config)
if (editGuid.isNotEmpty()) {
if (isRunning) {
delButton?.isVisible = false
saveButton?.isVisible = false
}
} else {
delButton?.isVisible = false
}
val delButton = menu.findItem(R.id.del_config)
delButton?.isVisible = editGuid.isNotEmpty() && !isRunning
return super.onCreateOptionsMenu(menu)
}
@@ -8,12 +8,13 @@ import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerGroupBinding
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.util.Utils
class ServerGroupActivity : BaseActivity() {
@@ -100,6 +101,9 @@ class ServerGroupActivity : BaseActivity() {
config.description = "${binding.spPolicyGroupType.selectedItem} - ${binding.spPolicyGroupSubId.selectedItem} - ${config.policyGroupFilter}"
MmkvManager.encodeServerConfig(editGuid, config)
if (isRunning) {
SettingsChangeManager.makeRestartService()
}
toastSuccess(R.string.toast_success)
finish()
return true
@@ -143,17 +147,9 @@ class ServerGroupActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_server, menu)
val delButton = menu.findItem(R.id.del_config)
val saveButton = menu.findItem(R.id.save_config)
if (editGuid.isNotEmpty()) {
if (isRunning) {
delButton?.isVisible = false
saveButton?.isVisible = false
}
} else {
delButton?.isVisible = false
}
val delButton = menu.findItem(R.id.del_config)
delButton?.isVisible = editGuid.isNotEmpty() && !isRunning
return super.onCreateOptionsMenu(menu)
}
@@ -11,13 +11,14 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ActivityServerProxyChainBinding
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.Utils
class ServerProxyChainActivity : BaseActivity() {
@@ -133,6 +134,9 @@ class ServerProxyChainActivity : BaseActivity() {
}
MmkvManager.encodeServerConfig(editGuid, config)
if (isRunning) {
SettingsChangeManager.makeRestartService()
}
toastSuccess(R.string.toast_success)
finish()
return true
@@ -195,17 +199,9 @@ class ServerProxyChainActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_server, menu)
val delButton = menu.findItem(R.id.del_config)
val saveButton = menu.findItem(R.id.save_config)
if (editGuid.isNotEmpty()) {
if (isRunning) {
delButton?.isVisible = false
saveButton?.isVisible = false
}
} else {
delButton?.isVisible = false
}
val delButton = menu.findItem(R.id.del_config)
delButton?.isVisible = editGuid.isNotEmpty() && !isRunning
return super.onCreateOptionsMenu(menu)
}
@@ -1,10 +1,10 @@
package com.v2ray.ang.ui
import android.graphics.Color
import android.widget.ArrayAdapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ItemRecyclerProxyChainMemberBinding
@@ -4,12 +4,15 @@ import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.ImageButton
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
import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
@@ -33,6 +36,7 @@ class SubEditActivity : BaseActivity() {
//setContentView(binding.root)
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_sub_setting))
setupProfileRemarkInputs()
SettingsChangeManager.makeSetupGroupTab()
val subItem = MmkvManager.decodeSubscription(editSubId)
if (subItem != null) {
@@ -73,6 +77,34 @@ class SubEditActivity : BaseActivity() {
return true
}
private fun setupProfileRemarkInputs() {
val suggestions = MmkvManager.decodeAllServerList()
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
.filter { it.isNotBlank() }
.distinct()
setupProfileRemarkInput(binding.etPreProfile, binding.btnPreProfileDropdown, suggestions)
setupProfileRemarkInput(binding.etNextProfile, binding.btnNextProfileDropdown, suggestions)
}
private fun setupProfileRemarkInput(
input: AutoCompleteTextView,
dropdownButton: ImageButton,
suggestions: List<String>
) {
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
input.setAdapter(adapter)
input.threshold = 0
dropdownButton.setOnClickListener {
input.requestFocus()
input.showDropDown()
}
input.setOnClickListener {
input.showDropDown()
}
}
/**
* save server config
*/
@@ -15,7 +15,7 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.contracts.BaseAdapterListener
import com.v2ray.ang.databinding.ActivityUserAssetBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.entities.AssetUrlItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
@@ -8,7 +8,7 @@ import androidx.appcompat.app.AlertDialog
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.entities.AssetUrlItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
@@ -3,6 +3,10 @@ package com.v2ray.ang.util
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.dto.UrlContentRequest
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.IOException
import java.net.IDN
@@ -14,9 +18,6 @@ import java.net.Proxy
import java.net.URI
import java.net.URL
import java.util.concurrent.TimeUnit
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
object HttpUtil {
@@ -108,20 +109,15 @@ object HttpUtil {
* @param httpPort The HTTP port to use.
* @return The content of the URL as a string.
*/
fun getUrlContent(
url: String,
timeout: Int,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String? {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
fun getUrlContent(request: UrlContentRequest): String? {
val url = request.url ?: return null
val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder()
.url(url)
.get()
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
}
try {
client.newCall(requestBuilder.build()).execute().use { response ->
@@ -147,33 +143,29 @@ object HttpUtil {
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class)
fun getUrlContentWithUserAgent(
url: String?,
userAgent: String?,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String {
var currentUrl = url
fun getUrlContentWithUserAgent(request: UrlContentRequest): String {
var currentUrl = request.url
var redirects = 0
val maxRedirects = 3
while (redirects++ < maxRedirects) {
if (currentUrl == null) continue
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = false)
val finalUserAgent = if (userAgent.isNullOrBlank()) {
val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = false)
val finalUserAgent = if (request.userAgent.isNullOrBlank()) {
"v2rayNG/${BuildConfig.VERSION_NAME}"
} else {
userAgent
request.userAgent
}
val requestBuilder = Request.Builder()
.url(currentUrl)
.get()
.header("User-agent", finalUserAgent)
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
applyEmbeddedBasicAuthHeader(currentUrl, requestBuilder)
if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
}
client.newCall(requestBuilder.build()).execute().use { response ->
@@ -203,6 +195,20 @@ object HttpUtil {
throw IOException("Too many redirects")
}
private fun applyEmbeddedBasicAuthHeader(rawUrl: String, requestBuilder: Request.Builder) {
val parsed = runCatching { URL(rawUrl) }.getOrNull() ?: return
parsed.userInfo?.let { userInfo ->
val colon = userInfo.indexOf(':')
val user = runCatching {
Utils.decodeURIComponent(if (colon >= 0) userInfo.substring(0, colon) else userInfo)
}.getOrDefault(if (colon >= 0) userInfo.substring(0, colon) else userInfo)
val pass = runCatching {
Utils.decodeURIComponent(if (colon >= 0) userInfo.substring(colon + 1) else "")
}.getOrDefault(if (colon >= 0) userInfo.substring(colon + 1) else "")
requestBuilder.header("Authorization", Credentials.basic(user, pass))
}
}
private fun buildOkHttpClient(
timeout: Int,
httpPort: Int,
@@ -254,20 +260,17 @@ object HttpUtil {
}
fun downloadToFile(
url: String,
targetFile: File,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
request: UrlContentRequest,
targetFile: File
): Boolean {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
val url = request.url ?: return false
val client = buildOkHttpClient(request.timeout, request.httpPort, request.proxyUsername, request.proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder()
.url(url)
.get()
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
if (request.httpPort != 0 && !request.proxyUsername.isNullOrBlank() && !request.proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(request.proxyUsername, request.proxyPassword))
}
return try {
@@ -290,4 +293,3 @@ object HttpUtil {
}
}
}
@@ -33,7 +33,7 @@ object PackageUidResolver {
private fun resolveUid(context: Context, packageName: String): String? {
// Special token for connections whose UID cannot be resolved (mapped to -1)
if (packageName == AppConfig.UNIDENTIFIED_PACKAGE) {
val uid = "-1"
val uid = "-1"
LogUtil.d(AppConfig.TAG, "Special package: $packageName -> UID: $uid")
return uid
}
@@ -488,6 +488,16 @@ object Utils {
throw IOException("no free port found")
}
/**
* Find a random free port.
*
* @return A random free port.
* @throws IOException If no free port is found.
*/
fun findRandomFreePort(): Int {
return ServerSocket(0).use { it.localPort }
}
/**
* Check if a string is a valid subscription URL.
*
@@ -14,12 +14,11 @@ import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.GroupMapItem
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.entities.ServersCache
import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionUpdateResult
import com.v2ray.ang.dto.TestServiceMessage
import com.v2ray.ang.extension.matchesPattern
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
@@ -457,7 +456,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
AppConfig.MSG_STATE_START_FAILURE -> {
getApplication<AngApplication>().toastError(R.string.toast_services_failure)
val errorMessage = intent.getStringExtra("content")
if (!errorMessage.isNullOrBlank()) {
getApplication<AngApplication>().toastError(errorMessage)
} else {
getApplication<AngApplication>().toastError(R.string.toast_services_failure)
}
isRunning.value = false
}
@@ -471,7 +475,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val content = intent.getStringExtra("content")
updateListAction.value = getPosition(content?: "")
updateListAction.value = getPosition(content ?: "")
}
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
@@ -1,7 +1,7 @@
package com.v2ray.ang.viewmodel
import androidx.lifecycle.ViewModel
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
@@ -1,8 +1,8 @@
package com.v2ray.ang.viewmodel
import androidx.lifecycle.ViewModel
import com.v2ray.ang.dto.SubscriptionCache
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.dto.entities.SubscriptionCache
import com.v2ray.ang.dto.entities.SubscriptionItem
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.handler.SettingsManager
@@ -2,8 +2,9 @@ package com.v2ray.ang.viewmodel
import androidx.lifecycle.ViewModel
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.AssetUrlCache
import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.entities.AssetUrlCache
import com.v2ray.ang.dto.entities.AssetUrlItem
import com.v2ray.ang.dto.UrlContentRequest
import com.v2ray.ang.extension.concatUrl
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.HttpUtil
@@ -92,7 +93,18 @@ class UserAssetViewModel : ViewModel() {
val targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks)
try {
if (HttpUtil.downloadToFile(item.url, targetTemp, 15000, httpPort, proxyUsername, proxyPassword)) {
if (
HttpUtil.downloadToFile(
UrlContentRequest(
url = item.url,
timeout = 15000,
httpPort = httpPort,
proxyUsername = proxyUsername,
proxyPassword = proxyPassword
),
targetTemp
)
) {
targetTemp.renameTo(target)
return true
}
@@ -209,12 +209,33 @@
android:layout_height="wrap_content"
android:text="@string/sub_setting_pre_profile" />
<EditText
android:id="@+id/et_pre_profile"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/sub_setting_pre_profile_tip"
android:inputType="text" />
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8">
<AutoCompleteTextView
android:id="@+id/et_pre_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:completionThreshold="0"
android:hint="@string/sub_setting_pre_profile_tip"
android:inputType="text"
android:imeOptions="actionDone"
android:paddingEnd="40dp" />
<ImageButton
android:id="@+id/btn_pre_profile_dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/sub_setting_pre_profile"
android:src="@drawable/ic_arrow_drop_down" />
</RelativeLayout>
</LinearLayout>
@@ -229,12 +250,33 @@
android:layout_height="wrap_content"
android:text="@string/sub_setting_next_profile" />
<EditText
android:id="@+id/et_next_profile"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/sub_setting_pre_profile_tip"
android:inputType="text" />
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8">
<AutoCompleteTextView
android:id="@+id/et_next_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:completionThreshold="0"
android:hint="@string/sub_setting_pre_profile_tip"
android:inputType="text"
android:imeOptions="actionDone"
android:paddingEnd="40dp" />
<ImageButton
android:id="@+id/btn_next_profile_dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/sub_setting_next_profile"
android:src="@drawable/ic_arrow_drop_down" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
@@ -161,25 +161,54 @@
android:minLines="4" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_fm"
<LinearLayout
android:id="@+id/layout_fm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_final_mask" />
<EditText
android:id="@+id/et_fm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
android:gravity="top"
android:inputType="textMultiLine"
android:maxLines="20"
android:minLines="4" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_final_mask" />
<LinearLayout
android:id="@+id/layout_browser_dialer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp16"
android:orientation="vertical">
<EditText
android:id="@+id/et_fm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:inputType="textMultiLine"
android:maxLines="20"
android:minLines="4" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_browser_dialer" />
<Spinner
android:id="@+id/sp_browser_dialer_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp16"
android:entries="@array/browser_dialer_mode" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp4"
android:text="@string/server_lab_browser_dialer_tip"
android:textColor="@android:color/darker_gray"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
@@ -14,7 +14,7 @@
<string name="notification_action_stop_v2ray">واڌاشتن</string>
<string name="toast_permission_denied">گرؽڌن موجوز مومکن نؽڌ</string>
<string name="toast_permission_denied_notification">گرؽڌن موجوز وارسۊوی مومکن نؽڌ</string>
<string name="notification_action_more">سی گرؽڌن دۉسمندیا بیشتر کیلیک کوݩ</string>
<string name="notification_action_more">سی گرؽڌن دووسمندیا قلوه کیلیک کوݩ</string>
<string name="toast_services_start">ره وستن خدمات</string>
<string name="toast_services_stop">واڌاشتن خدمات</string>
<string name="toast_services_success">ره وستن خدمات وا مووفقیت ٱنجوم وابی</string>
@@ -30,7 +30,7 @@
<string name="menu_item_import_config_clipboard">و من ٱووردن کانفیگ ز کلیپ بورد</string>
<string name="menu_item_import_config_local">و من ٱووردن کانفیگ ز مهلی</string>
<string name="menu_item_import_config_policy_group">ٱووردن [بونکۊ سیاست]</string>
<string name="menu_item_import_config_proxy_chain">Add [Proxy chain]</string>
<string name="menu_item_import_config_proxy_chain">ٱووردن [زنجیره پروکسی]</string>
<string name="menu_item_import_config_manually_vmess">هؽل دستی[VMess]</string>
<string name="menu_item_import_config_manually_vless">هؽل دستی[VLESS]</string>
<string name="menu_item_import_config_manually_ss">هؽل دستی[Shadowsocks]</string>
@@ -86,8 +86,8 @@
<string name="server_lab_spider_x">SpiderX</string>
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
<string name="server_lab_secret_key">کیلیت سیخومی</string>
<string name="server_lab_reserved">Reserved(اختیاری، وا کاما ز یک جوڌا ابۊن)</string>
<string name="server_lab_local_address">نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما ز یک جوڌا ابۊن)</string>
<string name="server_lab_reserved">Reserved(اختیاری، وا کاما (,) ز یک جوڌا ابۊن)</string>
<string name="server_lab_local_address">نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما (,) ز یک جوڌا ابۊن)</string>
<string name="server_lab_local_mtu">Mtu(اختیاری، پؽش فرز 1420)</string>
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پؽش فرز هسته 1350)</string>
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پؽش فرز هسته 50)</string>
@@ -177,16 +177,16 @@
<string name="title_pref_speed_enabled">ره وندن نشووݩ داڌن سورعت</string>
<string name="summary_pref_speed_enabled">نشووݩ داڌن سورعت هیم سکویی من وارسۊویا. نماڌ وارسۊوی و ری و کار گرؽڌن کانفیگ آلشت ابۊ.</string>
<string name="title_pref_sniffing_enabled">ره وندن Sniffing</string>
<string name="summary_pref_sniffing_enabled">دامنه sniff ن ز کتن امتهووݩ کۊنین (پؽش فرز رۊشن)</string>
<string name="title_pref_sniffing_enabled">ره وندن تجزیه وو تئلیل کتنا (Sniffing)</string>
<string name="summary_pref_sniffing_enabled">و کار گرؽڌن تشخیس نوم دامنه (Sniff) من کتنا (پؽش فرز رۊشن)</string>
<string name="title_pref_route_only_enabled">ره وندن routeOnly</string>
<string name="summary_pref_route_only_enabled">ز نوم دامنه sniffed تینا سی تور جوستن استفاڌه کۊنین وو نشۊوی موورد نزرن و عونوان نشۊوی IP ووردارین.</string>
<string name="summary_pref_route_only_enabled">نوم دامنه sniffed ن تینا سی تور جوستن و کار بگیرین وو نشۊوی موورد نزرن و عونوان نشۊوی IP ووردارین.</string>
<string name="title_pref_local_dns_enabled">ره وندن DNS مهلی</string>
<string name="summary_pref_local_dns_enabled">درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین فعال بۊ)</string>
<string name="summary_pref_local_dns_enabled">درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین ره ونده بۊ)</string>
<string name="title_pref_fake_dns_enabled">ره وندن DNS جئلی</string>
<string name="summary_pref_fake_dns_enabled">DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من یقرد ز برنومه یل کار نکونه)</string>
<string name="summary_pref_fake_dns_enabled">DNS مهلی نشۊویا IP جئلی ن وورگنه (زل تر، ٱما گاشڌ من ی قرده ز برنومه یل کار نکونه)</string>
<string name="title_pref_ipv6_enabled">ره وندن IPv6</string>
<string name="summary_pref_ipv6_enabled">نشۊویا IPv6 وو تورا IPv6 ن ز رابت VPN ره ونین</string>
@@ -211,21 +211,21 @@
<string name="title_pref_delay_test_url">نشۊوی اینترنتی آزمایش تئخیر واقعی</string>
<string name="summary_pref_delay_test_url">نشۊوی اینترنتی</string>
<string name="title_pref_real_ping_concurrency">هموورگی آزمایش تئخیر واقعی</string>
<string name="title_pref_real_ping_concurrency">تعداد هوم زمووی تست تئخیر واقعی</string>
<string name="title_pref_ip_api_url">نشۊوی اینترنتی آزمایش دووسمندیا منپیز هیم سکویی</string>
<string name="summary_pref_ip_api_url">نشۊوی اینترنتی</string>
<string name="title_pref_proxy_sharing_enabled">هشتن منپیزا ز شبکه مهلی</string>
<string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا نشۊوی IP ایسا، ز ره socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل اعتماد فعال بۊ تا ز منپیز غیر موجاز جلو گری بۊ.</string>
<string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا نشۊوی IP ایسا، ز ره socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل ائتماد ره ونده بۊ تا ز منپیز قیر موجاز جلو گری بۊ.</string>
<string name="toast_warning_pref_proxysharing_short">منپیزا ز شبکه مهلی ن موجار کۊنین، موتمعن بۊین ک من ی شبکه قابل ائتماڌ هڌین.</string>
<string name="title_pref_allow_insecure">اجازه نا ٱمن</string>
<string name="summary_pref_allow_insecure">مجال و کار بوردن TLS ب تۉر پؽش فرز، موجوز نا ٱمن فعال هڌ.</string>
<string name="summary_pref_allow_insecure">مجال و کار بوردن TLS ب تۉر پؽش فرز، موجوز نا ٱمن ره ونده هڌ.</string>
<string name="title_pref_socks_port">پورت پروکسی مهلی</string>
<string name="title_pref_enable_local_proxy">ره وندن پروکسی مهلی</string>
<string name="summary_pref_enable_local_proxy">هرسا قیرفعال بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو ورۊ رسۊوی یل اشتراک وو کارا جۊر یو نترن ز پروکسی استفاڌه کونن.</string>
<string name="summary_pref_enable_local_proxy">هرسا ره نوسته بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو ورۊ رسۊوی یل اشتراک وو کارا جۊر یو نترن پروکسی ن و کار گرن.</string>
<string name="title_pref_socks_enable_udp">SOCKS5 UDP</string>
<string name="summary_pref_socks_enable_udp">ائراز هۊویت socks5 مجال استفاڌه ز UDP کاملن ایمن نؽ</string>
<string name="summary_pref_socks_port">پورت پروکسی مهلی</string>
@@ -249,9 +249,9 @@
<string name="summary_pref_append_http_proxy">پروکسی HTTP ن موسقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.</string>
<string name="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</string>
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینۉ ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
<string name="summary_pref_double_column_display">نومگه نمایه یل من دو سۊتۊن نشووݩ داڌه ابۊن وو چینوو ترین موئتوا بیشتری ن سیل کۊنین. سی ره وستن، وا برنومه ن ز نۊ ره ونین.</string>
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن فعال کۊنین</string>
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن ره ونین</string>
<string name="summary_pref_group_all_display">ی بلگه «پوی بونکۊ یل کانفیگ» ازاف کۊنین</string>
<!-- AboutActivity -->
<string name="title_pref_feedback">فشناڌن منشڌ</string>
@@ -268,18 +268,18 @@
<string name="title_pref_promotion">تبلیقات</string>
<string name="title_pref_auto_update_subscription">ورۊ کردن خوتکار اشتراکا</string>
<string name="summary_pref_auto_update_subscription">اشتراکا خوتۉ ن و تۉر خوتکار وا فاسله زمۊوی من پس زمینه ورۊ کۊنین. ای ویژیی من پوی دسگایل گاشڌ همیشه کار نکونه</string>
<string name="summary_pref_auto_update_subscription">اشتراکا خوتووݩ ن و تۉر خوتکار وا فاسله زمۊوی من پس زمینه ورۊ کۊنین. ای ویژیی من پوی دسگایل گاشڌ همیشه کار نکونه</string>
<string name="title_pref_auto_update_interval">فاسله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ)</string>
<string name="title_core_loglevel">سئت گوزارشا</string>
<string name="title_outbound_domain_resolve_method">بارت پؽش هل دامنه دری</string>
<string name="title_mode">هالت</string>
<string name="title_mode_help">سی دووسمندیا وو هیاری بیشتر، ری ای هؽل بزݩ</string>
<string name="title_mode_help">سی دووسمندیا وو هیاری قلوه، ری ای هؽل بزݩ</string>
<string name="title_language">زووݩ</string>
<string name="title_ui_settings">سامووا رابت منتوری</string>
<string name="title_pref_ui_mode_night">سامووا هالت رابت منتوری</string>
<string name="title_pref_use_hev_tunnel">فعال کردن ویژیی Hev TUN</string>
<string name="summary_pref_use_hev_tunnel">مجالی ک فعال بۊ، TUN ز hev-socks5-tunnel استفاڌه اکونه؛ ٱر فعال نبۊ، زهمو xray-core استفاڌه اکونه.</string>
<string name="title_pref_use_hev_tunnel">ره وندن ویژیی Hev TUN</string>
<string name="summary_pref_use_hev_tunnel">مجالی ک ره ونده بۊ، TUN ایا hev-socks5-tunnel ن و کار اگره؛ ٱر ره نوۊفته بۊ، همو xray-core ن و کار اگره.</string>
<string name="title_pref_hev_tunnel_loglevel">Hev Tun سئت گوزارشا</string>
<string name="title_pref_hev_tunnel_rw_timeout">زمووݩ مندیر بیڌن خوندن/هؽل کردن Hev Tun (سانیه) (پؽش فرز tcp، udp 300،60)</string>
@@ -290,19 +290,19 @@
<string name="title_del_all_config">پاک کردن پوی کانفیگا بونکۊ سکویی</string>
<string name="title_del_duplicate_config">پاک کردن کانفیگا تکراری بونکۊ سکویی</string>
<string name="title_del_invalid_config">پاک کردن کانفیگا نا موئتبر بونکۊ سکویی</string>
<string name="title_export_all">و در کشیڌن کانفیگا غیر سفارشی بونکۊ سکویی من کلیپ بورد</string>
<string name="title_export_all">و در کشیڌن کانفیگا قیر سفارشی بونکۊ سکویی من کلیپ بورد</string>
<string name="title_sub_setting">سامووا بونکۊ اشتراک</string>
<string name="sub_setting_remarks">نیشتنا</string>
<string name="sub_setting_url">نشۊوی اینترنتی اختیاری</string>
<string name="sub_setting_user_agent">User Agent</string>
<string name="sub_setting_filter">نوم موستعار فیلتر</string>
<string name="sub_setting_enable">فعال بیڌن ورۊ کردن</string>
<string name="sub_auto_update">فعال بیڌن ورۊ کردن خوتکار</string>
<string name="sub_setting_enable">ره وندن ورۊ کردن</string>
<string name="sub_auto_update">ره وندن ورۊ کردن خوتکار</string>
<string name="sub_allow_insecure_url">موجاز کردن نشۊوی HTTP نا ٱمن</string>
<string name="sub_setting_pre_profile">نوم موستعار پروکسی دیندایی</string>
<string name="sub_setting_next_profile">نوم موستعار پروکسی نیایی</string>
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار هڌس وو جۊرس نی</string>
<string name="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. اقل مقدار 15 دؽقه هڌ.</string>
<string name="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. هدقل مقدار 15 دؽقه هڌ.</string>
<string name="title_sub_update">ورۊ کردن اشتراک بونکۊ سکویی</string>
<string name="title_ping_all_server">Tcping کانفیگا بونکۊ سکویی</string>
<string name="title_real_ping_all_server">تئخیر واقعی کانفیگا بونکۊ سکویی</string>
@@ -328,17 +328,17 @@
<!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">نشقه دامنه</string>
<string name="routing_settings_title">سامووا تور جوستن</string>
<string name="routing_settings_tips">وا کاما ز یک جوڌا ابۊن (,)؛ یکی ن بزنین: domain، ip یا process</string>
<string name="routing_settings_tips">وا کاما (,) ز یک جوڌا ابۊن؛ یکی ن بزنین: domain، ip یا process</string>
<string name="routing_settings_save">زفت کردن</string>
<string name="routing_settings_delete">روفتن</string>
<string name="routing_settings_rule_title">سامووا قانۉݩ تور جوستن</string>
<string name="routing_settings_add_rule">ازاف کردن قانۉݩ</string>
<string name="routing_settings_rule_title">سامووا قانووݩ تور جوستن</string>
<string name="routing_settings_add_rule">ازاف کردن قانووݩ</string>
<string name="routing_settings_import_predefined_rulesets">و من ٱووردن قانووا</string>
<string name="routing_settings_import_rulesets_tip">قانووایی ک هیم سکو هڌسۉݩ پاک ابۊن، هنی هم اخۊی پاکسۉݩ کۊنی؟</string>
<string name="routing_settings_import_rulesets_tip">قانووایی ک هیم سکو هڌسووݩ پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟</string>
<string name="routing_settings_import_rulesets_from_clipboard">و من ٱووردن قانووا ز کلیپ بورد</string>
<string name="routing_settings_import_rulesets_from_qrcode">و من ٱووردن قانووا ز QRcode</string>
<string name="routing_settings_export_rulesets_to_clipboard">و در کشیڌن قانووا وو زفت من کلیپ بورد</string>
<string name="routing_settings_locked">چفت هڌ، ای قانؤنن مجال و من ٱووردن ز پؽش سامووا زفت کۊنین</string>
<string name="routing_settings_locked">چفت هڌ، ای قانووݩ ن مجال و من ٱووردن ز پؽش سامووا زفت کۊنین</string>
<string name="routing_settings_domain" translatable="false">domain</string>
<string name="routing_settings_ip" translatable="false">ip</string>
<string name="routing_settings_port" translatable="false">port</string>
@@ -377,12 +377,12 @@
<string name="title_policy_group_type">نوع بونکۊ سیاست</string>
<string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string>
<string name="title_policy_group_subscription_filter">توزیهات فیلتر معمۊلی</string>
<string name="server_proxy_chain_members">اعضای زنجیره پروکسی</string>
<string name="server_proxy_chain_pick_members">اینجا بزنین تا عضو اِنتخاب اکه</string>
<string name="server_proxy_chain_member_unselected">یه عضو اِنتخاب کۊنین</string>
<string name="server_proxy_chain_members_unselected">لطفا سی هر ردیو زنجیره، remark اِنتخاب کۊنین</string>
<string name="server_proxy_chain_members_insufficient">تعداد عضوها کافی نیس</string>
<string name="server_proxy_chain_members_invalid">اعضای نامعتبر زنجیره: %1$s</string>
<string name="server_proxy_chain_members">ٱئزا زنجیره پروکسی</string>
<string name="server_proxy_chain_pick_members">سی پسند عوزو ایچو بزنین</string>
<string name="server_proxy_chain_member_unselected">ی عوزو پسند کۊنین</string>
<string name="server_proxy_chain_members_unselected">سی هر ردیف زنجیره remark پسند کۊنین</string>
<string name="server_proxy_chain_members_insufficient">تعداد ٱئزا بس نؽ</string>
<string name="server_proxy_chain_members_invalid">ٱئزا ناموئتبر زنجیره: %1$s</string>
<!-- BackupActivity -->
<string name="title_configuration_backup_restore">لادراری گرؽڌن &amp; وورگندن</string>
+19 -16
View File
@@ -30,15 +30,15 @@
<string name="menu_item_import_config_clipboard">Импорт из буфера обмена</string>
<string name="menu_item_import_config_local">Импорт из файла</string>
<string name="menu_item_import_config_policy_group">Добавить группу политик</string>
<string name="menu_item_import_config_proxy_chain">Add [Proxy chain]</string>
<string name="menu_item_import_config_manually_vmess">Ручной ввод VMess</string>
<string name="menu_item_import_config_manually_vless">Ручной ввод VLESS</string>
<string name="menu_item_import_config_manually_ss">Ручной ввод Shadowsocks</string>
<string name="menu_item_import_config_manually_socks">Ручной ввод SOCKS</string>
<string name="menu_item_import_config_manually_http">Ручной ввод HTTP</string>
<string name="menu_item_import_config_manually_trojan">Ручной ввод Trojan</string>
<string name="menu_item_import_config_manually_wireguard">Ручной ввод WireGuard</string>
<string name="menu_item_import_config_manually_hysteria2">Ручной ввод Hysteria2</string>
<string name="menu_item_import_config_proxy_chain">Добавить цепочку прокси</string>
<string name="menu_item_import_config_manually_vmess">Добавить VMess</string>
<string name="menu_item_import_config_manually_vless">Добавить VLESS</string>
<string name="menu_item_import_config_manually_ss">Добавить Shadowsocks</string>
<string name="menu_item_import_config_manually_socks">Добавить SOCKS</string>
<string name="menu_item_import_config_manually_http">Добавить HTTP</string>
<string name="menu_item_import_config_manually_trojan">Добавить Trojan</string>
<string name="menu_item_import_config_manually_wireguard">Добавить WireGuard</string>
<string name="menu_item_import_config_manually_hysteria2">Добавить Hysteria2</string>
<string name="del_config_comfirm">Подтверждаете удаление?</string>
<string name="del_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string>
<string name="server_lab_remarks">Название</string>
@@ -70,7 +70,7 @@
<string name="server_lab_stream_security">TLS</string>
<string name="server_lab_stream_fingerprint">Отпечаток</string>
<string name="server_lab_stream_alpn">ALPN</string>
<string name="server_lab_allow_insecure">Разрешать небезопасные</string>
<string name="server_lab_allow_insecure">Разрешать небезопасные соединения</string>
<string name="server_lab_sni">SNI</string>
<string name="server_lab_address3">Адрес</string>
<string name="server_lab_port3">Порт</string>
@@ -116,8 +116,10 @@
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
<string name="server_lab_final_mask">Необработанный JSON FinalMask, формат: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_config_list">ECHConfigList</string>
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
<string name="server_lab_browser_dialer">Использовать переадресацию браузера</string>
<string name="server_lab_browser_dialer_tip">Поддерживаются только исходящие соединения XHTTP (packet-up) и WS; настройки, связанные с TLS, могут быть проигнорированы или конфликтовать</string>
<!-- UserAssetActivity -->
<string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string>
@@ -213,7 +215,7 @@
<string name="title_pref_delay_test_url">Сервис проверки задержки</string>
<string name="summary_pref_delay_test_url">URL</string>
<string name="title_pref_real_ping_concurrency">Параллельность теста задержки</string>
<string name="title_pref_real_ping_concurrency">Параллельная проверка задержки</string>
<string name="title_pref_ip_api_url">Сервис проверки текущего соединения</string>
<string name="summary_pref_ip_api_url">URL</string>
@@ -305,7 +307,7 @@
<string name="sub_setting_pre_profile">Предыдущий профиль прокси</string>
<string name="sub_setting_next_profile">Следующий профиль прокси</string>
<string name="sub_setting_pre_profile_tip">Профиль должен быть уникальным</string>
<string name="toast_invalid_update_interval">Неверный интервал обновления. Минимум 15 минут.</string>
<string name="toast_invalid_update_interval">Неправильный интервал обновления. Минимум 15 минут.</string>
<string name="title_sub_update">Обновить подписку</string>
<string name="title_ping_all_server">Проверить профили</string>
<string name="title_real_ping_all_server">Проверить задержку профилей</string>
@@ -343,7 +345,7 @@
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string>
<string name="routing_settings_domain">Домен</string>
<string name="routing_settings_ip">IP</string>
<string name="routing_settings_process">Процесс (название пакета; поддерживается только при использовании Xray TUN, включённой функции routeOnly и ОС Android 10+)</string>
<string name="routing_settings_process">Процесс (название пакета; поддерживается только при использовании Xray TUN, включённой функции «Домен только для маршрутизации» и ОС Android 10+)</string>
<string name="routing_settings_port">Порт</string>
<string name="routing_settings_protocol">Протокол</string>
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
@@ -380,10 +382,11 @@
<string name="title_policy_group_type">Тип группы политик</string>
<string name="title_policy_group_subscription_id">Из группы подписки</string>
<string name="title_policy_group_subscription_filter">Название фильтра</string>
<string name="server_proxy_chain_members">Участники цепочки прокси</string>
<string name="server_proxy_chain_pick_members">Нажмите здесь, чтобы выбрать участника</string>
<string name="server_proxy_chain_member_unselected">Выберите участника</string>
<string name="server_proxy_chain_members_unselected">Пожалуйста, выберите участника для каждой строки цепочки</string>
<string name="server_proxy_chain_member_unselected">Выбор участника</string>
<string name="server_proxy_chain_members_unselected">Выберите участника для каждого звена цепочки</string>
<string name="server_proxy_chain_members_insufficient">Недостаточно участников</string>
<string name="server_proxy_chain_members_invalid">Недопустимые участники цепочки: %1$s</string>
@@ -118,6 +118,8 @@
<string name="server_lab_final_mask">FinalMask 原始 JSON 格式: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
<string name="server_lab_browser_dialer">启用浏览器转发</string>
<string name="server_lab_browser_dialer_tip">仅支持 xhttp (packet-up) 和 ws。与优选域名冲突,utls, alpn, ech 等 TLS 设置将被忽略</string>
<!-- UserAssetActivity -->
<string name="toast_asset_copy_failed">失败, 请使用文件管理器</string>
@@ -117,6 +117,12 @@
<item>VPN</item>
<item>Proxy only</item>
</string-array>
<string-array name="browser_dialer_mode_value" translatable="false">
<item>Disable</item>
<item>OkHttp</item>
<item>WebView</item>
</string-array>
<string-array name="hev_tunnel_loglevel" translatable="false">
<item>error</item>
@@ -119,6 +119,8 @@
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
<string name="server_lab_browser_dialer">Enable Browser Dialer</string>
<string name="server_lab_browser_dialer_tip">Only supports xhttp (packet-up) and ws outbound; TLS-related settings may be ignored or conflict</string>
<!-- UserAssetActivity -->
<string name="toast_asset_copy_failed">File copy failed, please use File Manager</string>
@@ -427,6 +429,12 @@
<item>Proxy only</item>
</string-array>
<string-array name="browser_dialer_mode">
<item>Disable</item>
<item>OkHttp</item>
<item>WebView</item>
</string-array>
<string-array name="ui_mode_night">
<item>Follow system</item>
<item>Light</item>
@@ -3,7 +3,7 @@ package com.v2ray.ang.fmt
import android.util.Base64
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.entities.ProfileItem
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
+1 -1
View File
@@ -1,5 +1,5 @@
[versions]
agp = "9.2.0"
agp = "9.2.1"
desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8"
kotlin = "2.3.10"