Compare commits

...

10 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
58 changed files with 475 additions and 241 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 24
targetSdk = 36
versionCode = 727
versionName = "2.1.7"
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
@@ -6,8 +6,8 @@ import com.google.gson.JsonArray
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.CoreConfigContext
import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.RulesetItem
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.enums.CoreResolvedType
import com.v2ray.ang.enums.EConfigType
@@ -3,7 +3,7 @@ package com.v2ray.ang.core
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.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
@@ -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
@@ -257,7 +258,7 @@ object CoreServiceManager {
}
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
NotificationManager.startSpeedNotification(currentConfig)
NotificationManager.startSpeedNotification()
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
}
@@ -299,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
}
/**
@@ -488,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()
}
}
}
@@ -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
)
@@ -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
@@ -80,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
@@ -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.NetworkType
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.util.HttpUtil
@@ -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
@@ -2,6 +2,7 @@ package com.v2ray.ang.handler
import com.v2ray.ang.AppConfig
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
@@ -88,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,7 +13,7 @@ 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
@@ -5,6 +5,7 @@ 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
@@ -23,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
@@ -61,6 +61,45 @@ class DialerNativeService : IDialerService {
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
@@ -630,6 +669,11 @@ class DialerNativeService : IDialerService {
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
@@ -663,7 +707,7 @@ class DialerNativeService : IDialerService {
val extra: Extra
) {
data class Extra(
// val headers: Map<String, String> = emptyMap(),
val headers: Map<String, String> = emptyMap(),
// val cookies: Map<String, String> = emptyMap(),
val protocols: List<String> = emptyList(),
val referrer: String? = null
@@ -679,7 +723,7 @@ class DialerNativeService : IDialerService {
val streamResponse = root.optBoolean("streamResponse", false)
val extraObject = root.optJSONObject("extra")
// val headers = extraObject.optStringMap("headers")
val headers = extraObject.optStringMap("headers")
// val cookies = extraObject.optStringMap("cookies")
val referrer = extraObject?.optString("referrer")?.takeIf { it.isNotBlank() }
val protocols = extraObject.optProtocols()
@@ -689,7 +733,7 @@ class DialerNativeService : IDialerService {
url = url,
streamResponse = streamResponse,
extra = Extra(
// headers = headers,
headers = headers,
// cookies = cookies,
protocols = protocols,
referrer = referrer
@@ -698,16 +742,16 @@ class DialerNativeService : IDialerService {
}.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?.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()
@@ -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
@@ -17,7 +17,7 @@ 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
@@ -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
@@ -10,7 +10,7 @@ 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
@@ -8,7 +8,7 @@ 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
@@ -11,7 +11,7 @@ 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
@@ -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,7 @@ 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
@@ -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,25 +143,18 @@ 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)
@@ -175,8 +164,8 @@ object HttpUtil {
applyEmbeddedBasicAuthHeader(currentUrl, requestBuilder)
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))
}
client.newCall(requestBuilder.build()).execute().use { response ->
@@ -271,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 {
@@ -14,8 +14,8 @@ 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
@@ -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>
@@ -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>
@@ -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