Compare commits

...

12 Commits

Author SHA1 Message Date
2dust f9f0c905a4 up 2.0.9 2026-02-07 11:06:16 +08:00
2dust f997335113 Update libs.versions.toml 2026-02-07 11:05:47 +08:00
AmirMohammad Yazdanmanesh ed344cd281 Improve accessibility: mark decorative icons across remaining layouts (#5265)
Add importantForAccessibility="no" to decorative ImageViews in check
update, bypass list, routing, subscription, user asset, and widget
layouts. Add contentDescription to QR code ImageView.
2026-02-07 10:46:10 +08:00
2dust 94775f120a Migrate Hysteria2 pinSHA256 to pinnedCA256
Rename and migrate Hysteria2 certificate pin field from pinSHA256 to pinnedCA256 across code and UI, and add a one-time migration at startup.

Changes:
- Added SettingsManager.migrateHysteria2PinSHA256() and call in AngApplication to copy existing profile.pinSHA256 -> profile.pinnedCA256 and clear the old field, guarded by a migration flag.
- Updated Hysteria2Fmt to read/write the query param "pinSHA256" into the new ProfileItem.pinnedCA256 field.
- Updated ProfileItem equality to compare pinnedCA256 instead of pinSHA256.
- Removed the old EditText binding and reads/writes for et_pinsha256 in ServerActivity; UI now uses et_pinned_ca256 (layout id renamed).
- Updated layout and string resources: renamed the label/id to reflect pinned_ca256 and removed legacy pinSHA256 string entries in several locale files.

This ensures existing Hysteria2 profiles keep their pin data while the code and UI use the new field name.
2026-02-07 10:42:19 +08:00
2dust abd23b3227 up 2.0.8 2026-02-06 20:08:17 +08:00
2dust 7d3e002140 Update hev-socks5-tunnel 2026-02-06 19:45:41 +08:00
2dust 1adb22944a Update AndroidLibXrayLite 2026-02-06 19:45:34 +08:00
DHR60 f48253371d Follow encodeURIComponent rules strictly (#5250) 2026-02-06 19:38:45 +08:00
AmirMohammad Yazdanmanesh 74f701096e Improve accessibility: mark decorative icons as not important for accessibility (#5247) 2026-02-06 19:37:25 +08:00
DHR60 8eca5fe8af Add finalmask support (#5242) 2026-02-06 19:27:02 +08:00
solokot ff40b53e2f Update Russian translation (#5238) 2026-02-01 18:14:38 +08:00
2dust 5080d94bb7 Add description field to ProfileItem and generate descriptions 2026-01-30 17:24:13 +08:00
41 changed files with 207 additions and 87 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "com.v2ray.ang" applicationId = "com.v2ray.ang"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 707 versionCode = 709
versionName = "2.0.7" versionName = "2.0.9"
multiDexEnabled = true multiDexEnabled = true
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';') val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
@@ -41,6 +41,7 @@ class AngApplication : MultiDexApplication() {
WorkManager.initialize(this, workManagerConfiguration) WorkManager.initialize(this, workManagerConfiguration)
SettingsManager.initRoutingRulesets(this) SettingsManager.initRoutingRulesets(this)
SettingsManager.migrateHysteria2PinSHA256()
es.dmoral.toasty.Toasty.Config.getInstance() es.dmoral.toasty.Toasty.Config.getInstance()
.setGravity(android.view.Gravity.BOTTOM, 0, 200) .setGravity(android.view.Gravity.BOTTOM, 0, 200)
@@ -15,6 +15,7 @@ data class ProfileItem(
var addedTime: Long = System.currentTimeMillis(), var addedTime: Long = System.currentTimeMillis(),
var remarks: String = "", var remarks: String = "",
var description: String? = null,
var server: String? = null, var server: String? = null,
var serverPort: String? = null, var serverPort: String? = null,
@@ -123,7 +124,7 @@ data class ProfileItem(
&& this.obfsPassword == obj.obfsPassword && this.obfsPassword == obj.obfsPassword
&& this.portHopping == obj.portHopping && this.portHopping == obj.portHopping
&& this.portHoppingInterval == obj.portHoppingInterval && this.portHoppingInterval == obj.portHoppingInterval
&& this.pinSHA256 == obj.pinSHA256 && this.pinnedCA256 == obj.pinnedCA256
) )
} }
} }
@@ -162,7 +162,7 @@ data class V2rayConfig(
var realitySettings: TlsSettingsBean? = null, var realitySettings: TlsSettingsBean? = null,
var grpcSettings: GrpcSettingsBean? = null, var grpcSettings: GrpcSettingsBean? = null,
var hysteriaSettings: HysteriaSettingsBean? = null, var hysteriaSettings: HysteriaSettingsBean? = null,
var udpmasks: List<UdpMasksBean>? = null, var finalmask: FinalMaskBean? = null,
val dsSettings: Any? = null, val dsSettings: Any? = null,
var sockopt: SockoptBean? = null var sockopt: SockoptBean? = null
) { ) {
@@ -202,15 +202,8 @@ data class V2rayConfig(
var downlinkCapacity: Int = 100, var downlinkCapacity: Int = 100,
var congestion: Boolean = false, var congestion: Boolean = false,
var readBufferSize: Int = 1, var readBufferSize: Int = 1,
var writeBufferSize: Int = 1, var writeBufferSize: Int = 1
var header: HeaderBean = HeaderBean(), )
var seed: String? = null
) {
data class HeaderBean(
var type: String = "none",
var domain: String? = null
)
}
data class WsSettingsBean( data class WsSettingsBean(
var path: String? = null, var path: String? = null,
@@ -310,13 +303,19 @@ data class V2rayConfig(
) )
} }
data class UdpMasksBean( data class FinalMaskBean(
var type: String, var tcp: List<MaskBean>? = null,
var settings: UdpMasksSettingsBean? = null var udp: List<MaskBean>? = null
) { ) {
data class UdpMasksSettingsBean( data class MaskBean(
var password: String? = null var type: String,
) var settings: MaskSettingsBean? = null
) {
data class MaskSettingsBean(
var password: String? = null,
var domain: String? = null
)
}
} }
} }
@@ -22,17 +22,17 @@ open class FmtBase {
val query = if (dicQuery != null) val query = if (dicQuery != null)
"?" + dicQuery.toList().joinToString( "?" + dicQuery.toList().joinToString(
separator = "&", separator = "&",
transform = { it.first + "=" + Utils.urlEncode(it.second) }) transform = { it.first + "=" + Utils.encodeURIComponent(it.second) })
else "" else ""
val url = String.format( val url = String.format(
"%s@%s:%s", "%s@%s:%s",
Utils.urlEncode(userInfo ?: ""), Utils.encodeURIComponent(userInfo ?: ""),
Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())), Utils.getIpv6Address(HttpUtil.toIdnDomain(config.server.orEmpty())),
config.serverPort config.serverPort
) )
return "${url}${query}#${Utils.urlEncode(config.remarks)}" return "${url}${query}#${Utils.encodeURIComponent(config.remarks)}"
} }
/** /**
@@ -43,7 +43,7 @@ open class FmtBase {
*/ */
fun getQueryParam(uri: URI): Map<String, String> { fun getQueryParam(uri: URI): Map<String, String> {
return uri.rawQuery.split("&") return uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } } .associate { it.split("=").let { (k, v) -> k to Utils.decodeURIComponent(v) } }
} }
/** /**
@@ -5,8 +5,7 @@ import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.NetworkType import com.v2ray.ang.enums.NetworkType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.V2rayConfig.OutboundBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean.FinalMaskBean
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean.UdpMasksBean.UdpMasksSettingsBean
import com.v2ray.ang.extension.idnHost import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.nullIfBlank import com.v2ray.ang.extension.nullIfBlank
@@ -27,7 +26,7 @@ object Hysteria2Fmt : FmtBase() {
val config = ProfileItem.create(EConfigType.HYSTERIA2) val config = ProfileItem.create(EConfigType.HYSTERIA2)
val uri = URI(Utils.fixIllegalUrl(str)) val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.password = uri.userInfo config.password = uri.userInfo
@@ -45,7 +44,7 @@ object Hysteria2Fmt : FmtBase() {
if (config.portHopping.isNotNullEmpty()) { if (config.portHopping.isNotNullEmpty()) {
config.portHoppingInterval = queryParam["mportHopInt"] config.portHoppingInterval = queryParam["mportHopInt"]
} }
config.pinSHA256 = queryParam["pinSHA256"] config.pinnedCA256 = queryParam["pinSHA256"]
} }
@@ -76,8 +75,8 @@ object Hysteria2Fmt : FmtBase() {
if (config.portHoppingInterval.isNotNullEmpty()) { if (config.portHoppingInterval.isNotNullEmpty()) {
dicQuery["mportHopInt"] = config.portHoppingInterval.orEmpty() dicQuery["mportHopInt"] = config.portHoppingInterval.orEmpty()
} }
if (config.pinSHA256.isNotNullEmpty()) { if (config.pinnedCA256.isNotNullEmpty()) {
dicQuery["pinSHA256"] = config.pinSHA256.orEmpty() dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
} }
return toUri(config, config.password, dicQuery) return toUri(config, config.password, dicQuery)
@@ -109,11 +108,13 @@ object Hysteria2Fmt : FmtBase() {
} }
if (profileItem.obfsPassword.isNotNullEmpty()) { if (profileItem.obfsPassword.isNotNullEmpty()) {
outboundBean.streamSettings?.udpmasks = mutableListOf( outboundBean.streamSettings?.finalmask = FinalMaskBean(
StreamSettingsBean.UdpMasksBean( udp = listOf(
type = "salamander", FinalMaskBean.MaskBean(
settings = UdpMasksSettingsBean( type = "salamander",
password = profileItem.obfsPassword settings = FinalMaskBean.MaskBean.MaskSettingsBean(
password = profileItem.obfsPassword
)
) )
) )
) )
@@ -36,7 +36,7 @@ object ShadowsocksFmt : FmtBase() {
if (uri.port <= 0) return null if (uri.port <= 0) return null
if (uri.userInfo.isNullOrEmpty()) return null if (uri.userInfo.isNullOrEmpty()) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
@@ -83,7 +83,7 @@ object ShadowsocksFmt : FmtBase() {
if (indexSplit > 0) { if (indexSplit > 0) {
try { try {
config.remarks = config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length)) Utils.decodeURIComponent(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e) Log.e(AppConfig.TAG, "Failed to decode remarks in SS legacy URL", e)
} }
@@ -23,7 +23,7 @@ object SocksFmt : FmtBase() {
if (uri.idnHost.isEmpty()) return null if (uri.idnHost.isEmpty()) return null
if (uri.port <= 0) return null if (uri.port <= 0) return null
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
@@ -23,7 +23,7 @@ object TrojanFmt : FmtBase() {
val config = ProfileItem.create(EConfigType.TROJAN) val config = ProfileItem.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str)) val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.password = uri.userInfo config.password = uri.userInfo
@@ -26,7 +26,7 @@ object VlessFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri) val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.password = uri.userInfo config.password = uri.userInfo
@@ -163,7 +163,7 @@ object VmessFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri) val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
config.password = uri.userInfo config.password = uri.userInfo
@@ -26,7 +26,7 @@ object WireguardFmt : FmtBase() {
if (uri.rawQuery.isNullOrEmpty()) return null if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = getQueryParam(uri) val queryParam = getQueryParam(uri)
config.remarks = Utils.urlDecode(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } } config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
config.server = uri.idnHost config.server = uri.idnHost
config.serverPort = uri.port.toString() config.serverPort = uri.port.toString()
@@ -271,13 +271,14 @@ object AngConfigManager {
) { ) {
try { try {
val serverList: Array<Any> = val serverList: Array<Any> =
JsonUtil.fromJson(server, Array<Any>::class.java)?: arrayOf() JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
if (serverList.isNotEmpty()) { if (serverList.isNotEmpty()) {
var count = 0 var count = 0
for (srv in serverList.reversed()) { for (srv in serverList.reversed()) {
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
config.subscriptionId = subid config.subscriptionId = subid
config.description = generateDescription(config)
val key = MmkvManager.encodeServerConfig("", config) val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "") MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv) ?: "")
count += 1 count += 1
@@ -292,6 +293,7 @@ object AngConfigManager {
// For compatibility // For compatibility
val config = CustomFmt.parse(server) ?: return 0 val config = CustomFmt.parse(server) ?: return 0
config.subscriptionId = subid config.subscriptionId = subid
config.description = generateDescription(config)
val key = MmkvManager.encodeServerConfig("", config) val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server) MmkvManager.encodeServerRaw(key, server)
return 1 return 1
@@ -302,6 +304,7 @@ object AngConfigManager {
} else if (server.startsWith("[Interface]") && server.contains("[Peer]")) { } else if (server.startsWith("[Interface]") && server.contains("[Peer]")) {
try { try {
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
config.description = generateDescription(config)
val key = MmkvManager.encodeServerConfig("", config) val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server) MmkvManager.encodeServerRaw(key, server)
return 1 return 1
@@ -363,6 +366,7 @@ object AngConfigManager {
} }
config.subscriptionId = subid config.subscriptionId = subid
config.description = generateDescription(config)
val guid = MmkvManager.encodeServerConfig("", config) val guid = MmkvManager.encodeServerConfig("", config)
if (removedSelectedServer != null && if (removedSelectedServer != null &&
config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort config.server == removedSelectedServer.server && config.serverPort == removedSelectedServer.serverPort
@@ -493,4 +497,25 @@ object AngConfigManager {
MmkvManager.encodeSubscription("", subItem) MmkvManager.encodeSubscription("", subItem)
return 1 return 1
} }
/** Generates a description for the profile.
*
* @param profile The profile item.
* @return The generated description.
*/
fun generateDescription(profile: ProfileItem): String {
// Hide xxx:xxx:***/xxx.xxx.xxx.***
val server = profile.server
val port = profile.serverPort
if (server.isNullOrBlank() && port.isNullOrBlank()) return ""
val addrPart = server?.let {
if (it.contains(":"))
it.split(":").take(2).joinToString(":", postfix = ":***")
else
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
} ?: ""
return "$addrPart : ${port ?: ""}"
}
} }
@@ -16,6 +16,7 @@ import com.v2ray.ang.enums.Language
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.enums.RoutingType import com.v2ray.ang.enums.RoutingType
import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.enums.VpnInterfaceAddressConfig import com.v2ray.ang.enums.VpnInterfaceAddressConfig
import com.v2ray.ang.handler.MmkvManager.decodeServerConfig import com.v2ray.ang.handler.MmkvManager.decodeServerConfig
@@ -422,4 +423,30 @@ object SettingsManager {
MmkvManager.encodeSettings(key, default) MmkvManager.encodeSettings(key, default)
} }
} }
fun migrateHysteria2PinSHA256() {
// Check if migration has already been done
val migrationKey = "hysteria2_pin_sha256_migrated"
if (MmkvManager.decodeSettingsBool(migrationKey, false)) {
return
}
val serverList = decodeServerList()
for (guid in serverList) {
val profile = decodeServerConfig(guid) ?: continue
if (profile.configType != EConfigType.HYSTERIA2) {
continue
}
if (profile.pinSHA256.isNullOrEmpty() || !profile.pinnedCA256.isNullOrEmpty()) {
continue
}
profile.pinnedCA256 = profile.pinSHA256
profile.pinSHA256 = null
MmkvManager.encodeServerConfig(guid, profile)
}
MmkvManager.encodeSettings(migrationKey, true)
}
} }
@@ -1183,19 +1183,39 @@ object V2rayConfigManager {
} }
NetworkType.KCP.type -> { NetworkType.KCP.type -> {
val kcpsetting = StreamSettingsBean.KcpSettingsBean() streamSettings.kcpSettings = StreamSettingsBean.KcpSettingsBean()
kcpsetting.header.type = headerType ?: "none" val udpMaskList = mutableListOf<StreamSettingsBean.FinalMaskBean.MaskBean>()
if (!headerType.isNullOrEmpty() && headerType != "none") {
val kcpHeaderType = when {
headerType == "wechat-video" -> "header-wechat"
else -> "header-$headerType"
}
udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean(
type = kcpHeaderType,
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
domain = host
)
} else {
null
}
))
}
if (seed.isNullOrEmpty()) { if (seed.isNullOrEmpty()) {
kcpsetting.seed = null udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean(
type = "mkcp-original"
))
} else { } else {
kcpsetting.seed = seed udpMaskList.add(StreamSettingsBean.FinalMaskBean.MaskBean(
type = "mkcp-aes128gcm",
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
password = seed
)
))
} }
if (host.isNullOrEmpty()) { streamSettings.finalmask = StreamSettingsBean.FinalMaskBean(
kcpsetting.header.domain = null udp = udpMaskList.toList()
} else { )
kcpsetting.header.domain = host
}
streamSettings.kcpSettings = kcpsetting
} }
NetworkType.WS.type -> { NetworkType.WS.type -> {
@@ -14,6 +14,8 @@ import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
import com.v2ray.ang.databinding.ItemRecyclerMainBinding import com.v2ray.ang.databinding.ItemRecyclerMainBinding
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServersCache import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.extension.nullIfBlank
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperAdapter
import com.v2ray.ang.helper.ItemTouchHelperViewHolder import com.v2ray.ang.helper.ItemTouchHelperViewHolder
@@ -121,19 +123,7 @@ class MainRecyclerAdapter(
* @return Formatted address string * @return Formatted address string
*/ */
private fun getAddress(profile: ProfileItem): String { private fun getAddress(profile: ProfileItem): String {
// Hide xxx:xxx:***/xxx.xxx.xxx.*** return profile.description.nullIfBlank() ?: AngConfigManager.generateDescription(profile)
val server = profile.server
val port = profile.serverPort
if (server.isNullOrBlank() && port.isNullOrBlank()) return ""
val addrPart = server?.let {
if (it.contains(":"))
it.split(":").take(2).joinToString(":", postfix = ":***")
else
it.split('.').dropLast(1).joinToString(".", postfix = ".***")
} ?: ""
return "$addrPart : ${port ?: ""}"
} }
/** /**
@@ -26,6 +26,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -128,7 +129,6 @@ class ServerActivity : BaseActivity() {
private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) } private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) } private val et_port_hop: EditText? by lazy { findViewById(R.id.et_port_hop) }
private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) } private val et_port_hop_interval: EditText? by lazy { findViewById(R.id.et_port_hop_interval) }
private val et_pinsha256: EditText? by lazy { findViewById(R.id.et_pinsha256) }
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) } private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) } private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) } private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
@@ -366,7 +366,6 @@ class ServerActivity : BaseActivity() {
et_obfs_password?.text = Utils.getEditable(config.obfsPassword) et_obfs_password?.text = Utils.getEditable(config.obfsPassword)
et_port_hop?.text = Utils.getEditable(config.portHopping) et_port_hop?.text = Utils.getEditable(config.portHopping)
et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval) et_port_hop_interval?.text = Utils.getEditable(config.portHoppingInterval)
et_pinsha256?.text = Utils.getEditable(config.pinSHA256)
et_bandwidth_down?.text = Utils.getEditable(config.bandwidthDown) et_bandwidth_down?.text = Utils.getEditable(config.bandwidthDown)
et_bandwidth_up?.text = Utils.getEditable(config.bandwidthUp) et_bandwidth_up?.text = Utils.getEditable(config.bandwidthUp)
} }
@@ -494,6 +493,8 @@ class ServerActivity : BaseActivity() {
saveStreamSettings(config) saveStreamSettings(config)
saveTls(config) saveTls(config)
config.description = AngConfigManager.generateDescription(config)
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty() config.subscriptionId = subscriptionId.orEmpty()
} }
@@ -533,7 +534,6 @@ class ServerActivity : BaseActivity() {
config.obfsPassword = et_obfs_password?.text?.toString() config.obfsPassword = et_obfs_password?.text?.toString()
config.portHopping = et_port_hop?.text?.toString() config.portHopping = et_port_hop?.text?.toString()
config.portHoppingInterval = et_port_hop_interval?.text?.toString() config.portHoppingInterval = et_port_hop_interval?.text?.toString()
config.pinSHA256 = et_pinsha256?.text?.toString()
config.bandwidthDown = et_bandwidth_down?.text?.toString() config.bandwidthDown = et_bandwidth_down?.text?.toString()
config.bandwidthUp = et_bandwidth_up?.text?.toString() config.bandwidthUp = et_bandwidth_up?.text?.toString()
} }
@@ -16,6 +16,7 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.fmt.CustomFmt
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -89,6 +90,7 @@ class ServerCustomConfigActivity : BaseActivity() {
} }
config.server = profileItem?.server config.server = profileItem?.server
config.serverPort = profileItem?.serverPort config.serverPort = profileItem?.serverPort
config.description = AngConfigManager.generateDescription(config)
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString())
@@ -10,8 +10,10 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerGroupBinding import com.v2ray.ang.databinding.ActivityServerGroupBinding
import com.v2ray.ang.enums.EConfigType import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.AngConfigManager
import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.util.Utils import com.v2ray.ang.util.Utils
@@ -66,6 +68,11 @@ class ServerGroupActivity : BaseActivity() {
private fun clearServer(): Boolean { private fun clearServer(): Boolean {
binding.etRemarks.text = null binding.etRemarks.text = null
binding.etPolicyGroupFilter.text = null binding.etPolicyGroupFilter.text = null
if (subscriptionId.isNotNullEmpty()) {
val pos = subIds.indexOf(subscriptionId).let { if (it >= 0) it else 0 }
binding.spPolicyGroupSubId.setSelection(pos)
}
return true return true
} }
@@ -90,6 +97,9 @@ class ServerGroupActivity : BaseActivity() {
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) { if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty() config.subscriptionId = subscriptionId.orEmpty()
} }
config.description = "${binding.spPolicyGroupType.selectedItem} - ${binding.spPolicyGroupSubId.selectedItem} - ${config.policyGroupFilter}"
MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerConfig(editGuid, config)
toastSuccess(R.string.toast_success) toastSuccess(R.string.toast_success)
finish() finish()
@@ -328,13 +328,48 @@ object Utils {
*/ */
fun urlEncode(url: String): String { fun urlEncode(url: String): String {
return try { return try {
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20") URLEncoder.encode(url, Charsets.UTF_8.toString())
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to encode URL", e) Log.e(AppConfig.TAG, "Failed to encode URL", e)
url url
} }
} }
/**
* Decode a "encodeURIComponent" string.
*
* @param url The "encodeURIComponent" string.
* @return The decoded string, or the original string if decoding fails.
*/
fun decodeURIComponent(url: String): String {
return try {
// Decode strictly according to RFC 3986 / encodeURIComponent semantics.
// '+' is a literal plus and MUST NOT be interpreted as space.
// Inputs using '+' for spaces are non-conforming and rejected deliberately
// to avoid cross-language interoperability issues.
URLDecoder.decode(url.replace("+", "%2B"), Charsets.UTF_8.toString())
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode encodeURIComponent", e)
url
}
}
/**
* Encode a string to "encodeURIComponent" format.
*
* @param url The string to encode.
* @return The "encodeURIComponent" encoded string, or the original string if encoding fails.
*/
fun encodeURIComponent(url: String): String {
return try {
// Replace '+' with '%20' to conform to encodeURIComponent semantics.
URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20")
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to encode encodeURIComponent", e)
url
}
}
/** /**
* Read text from an asset file. * Read text from an asset file.
* *
@@ -29,6 +29,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_source_code_24dp" /> app:srcCompat="@drawable/ic_source_code_24dp" />
<TextView <TextView
@@ -53,6 +54,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/license_24px" /> app:srcCompat="@drawable/license_24px" />
<TextView <TextView
@@ -77,6 +79,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_feedback_24dp" /> app:srcCompat="@drawable/ic_feedback_24dp" />
<TextView <TextView
@@ -102,6 +105,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_telegram_24dp" /> app:srcCompat="@drawable/ic_telegram_24dp" />
<TextView <TextView
@@ -126,6 +130,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_privacy_24dp" /> app:srcCompat="@drawable/ic_privacy_24dp" />
<TextView <TextView
@@ -75,6 +75,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_about_24dp" /> app:srcCompat="@drawable/ic_about_24dp" />
</LinearLayout> </LinearLayout>
@@ -22,6 +22,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_source_code_24dp" /> app:srcCompat="@drawable/ic_source_code_24dp" />
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
@@ -49,6 +50,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_check_update_24dp" /> app:srcCompat="@drawable/ic_check_update_24dp" />
<TextView <TextView
@@ -11,6 +11,7 @@
android:id="@+id/iv_qcode" android:id="@+id/iv_qcode"
android:layout_width="336dp" android:layout_width="336dp"
android:layout_height="336dp" android:layout_height="336dp"
android:contentDescription="@string/title_qr_code"
android:scaleType="fitXY" android:scaleType="fitXY"
app:srcCompat="@drawable/ic_fab_check" /> app:srcCompat="@drawable/ic_fab_check" />
</LinearLayout> </LinearLayout>
@@ -43,6 +43,7 @@
android:layout_height="@dimen/padding_spacing_dp16" android:layout_height="@dimen/padding_spacing_dp16"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="@dimen/padding_spacing_dp16" android:layout_marginStart="@dimen/padding_spacing_dp16"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_lock_24dp" /> app:srcCompat="@drawable/ic_lock_24dp" />
</LinearLayout> </LinearLayout>
@@ -87,6 +88,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp" /> app:srcCompat="@drawable/ic_edit_24dp" />
</LinearLayout> </LinearLayout>
@@ -67,6 +67,7 @@
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_share_24dp" /> app:srcCompat="@drawable/ic_share_24dp" />
</LinearLayout> </LinearLayout>
@@ -85,6 +86,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp" /> app:srcCompat="@drawable/ic_edit_24dp" />
</LinearLayout> </LinearLayout>
@@ -103,6 +105,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_delete_24dp" /> app:srcCompat="@drawable/ic_delete_24dp" />
</LinearLayout> </LinearLayout>
@@ -64,6 +64,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp" /> app:srcCompat="@drawable/ic_edit_24dp" />
</LinearLayout> </LinearLayout>
@@ -79,6 +80,7 @@
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24" android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_delete_24dp" /> app:srcCompat="@drawable/ic_delete_24dp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -80,10 +80,10 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/server_lab_stream_pinsha256" /> android:text="@string/server_lab_pinned_ca256" />
<EditText <EditText
android:id="@+id/et_pinsha256" android:id="@+id/et_pinned_ca256"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text" /> android:inputType="text" />
@@ -18,6 +18,7 @@
android:id="@+id/image_switch" android:id="@+id/image_switch"
android:layout_width="45dp" android:layout_width="45dp"
android:layout_height="45dp" android:layout_height="45dp"
android:importantForAccessibility="no"
android:padding="@dimen/padding_spacing_dp16" android:padding="@dimen/padding_spacing_dp16"
app:srcCompat="@drawable/ic_stat_name" /> app:srcCompat="@drawable/ic_stat_name" />
</LinearLayout> </LinearLayout>
@@ -109,7 +109,6 @@
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping</string> <string name="server_lab_port_hop">Port Hopping</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string> <string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string>
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
@@ -108,7 +108,6 @@
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping</string> <string name="server_lab_port_hop">Port Hopping</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string> <string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string>
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
@@ -108,7 +108,6 @@
<string name="server_obfs_password">رزم obfs</string> <string name="server_obfs_password">رزم obfs</string>
<string name="server_lab_port_hop">پورت گوم (درگا سرورن ز نۊ هؽل اکونه)</string> <string name="server_lab_port_hop">پورت گوم (درگا سرورن ز نۊ هؽل اکونه)</string>
<string name="server_lab_port_hop_interval">فاسله پورت گوم (سانیه)</string> <string name="server_lab_port_hop_interval">فاسله پورت گوم (سانیه)</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<string name="server_lab_bandwidth_down">ب لم ٱووڌن پئنا باند (واهڌ)</string> <string name="server_lab_bandwidth_down">ب لم ٱووڌن پئنا باند (واهڌ)</string>
<string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string> <string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string>
<string name="server_lab_xhttp_mode">هالت XHTTP</string> <string name="server_lab_xhttp_mode">هالت XHTTP</string>
@@ -108,7 +108,6 @@
<string name="server_obfs_password">رمز عبور obfs</string> <string name="server_obfs_password">رمز عبور obfs</string>
<string name="server_lab_port_hop">پورت پرش (درگاه سرور را بازنویسی می کند)</string> <string name="server_lab_port_hop">پورت پرش (درگاه سرور را بازنویسی می کند)</string>
<string name="server_lab_port_hop_interval">فاصله پورت پرش (ثانیه)</string> <string name="server_lab_port_hop_interval">فاصله پورت پرش (ثانیه)</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<string name="server_lab_bandwidth_down">کاهش پهنای باند (واحد)</string> <string name="server_lab_bandwidth_down">کاهش پهنای باند (واحد)</string>
<string name="server_lab_bandwidth_up">افزایش پهنای باند (واحد)</string> <string name="server_lab_bandwidth_up">افزایش پهنای باند (واحد)</string>
<string name="server_lab_xhttp_mode">حالت XHTTP</string> <string name="server_lab_xhttp_mode">حالت XHTTP</string>
@@ -108,14 +108,13 @@
<string name="server_obfs_password">Пароль obfs</string> <string name="server_obfs_password">Пароль obfs</string>
<string name="server_lab_port_hop">Смена портов (переопределяет порт)</string> <string name="server_lab_port_hop">Смена портов (переопределяет порт)</string>
<string name="server_lab_port_hop_interval">Интервал смены портов</string> <string name="server_lab_port_hop_interval">Интервал смены портов</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<string name="server_lab_bandwidth_down">Входящая пропускная способность (допускаются: k/m/g/t)</string> <string name="server_lab_bandwidth_down">Входящая пропускная способность (допускаются: k/m/g/t)</string>
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">Режим XHTTP</string> <string name="server_lab_xhttp_mode">Режим XHTTP</string>
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string> <string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string> <string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
<!-- UserAssetActivity --> <!-- UserAssetActivity -->
<string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string> <string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string>
@@ -415,7 +414,7 @@
<string-array name="policy_group_type"> <string-array name="policy_group_type">
<item>Наименьшая задержка</item> <item>Наименьшая задержка</item>
<item>Наименьшая нагрузка</item> <item>Наименьшая нагрузка</item>
<item>Случайный</item> <item>Случайно</item>
<item>По очереди</item> <item>По очереди</item>
</string-array> </string-array>
@@ -108,7 +108,6 @@
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping</string> <string name="server_lab_port_hop">Port Hopping</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string> <string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string>
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
@@ -108,7 +108,6 @@
<string name="server_obfs_password">混淆密码</string> <string name="server_obfs_password">混淆密码</string>
<string name="server_lab_port_hop">跳跃端口 (会覆盖服务器端口)</string> <string name="server_lab_port_hop">跳跃端口 (会覆盖服务器端口)</string>
<string name="server_lab_port_hop_interval">端口跳跃间隔 (秒)</string> <string name="server_lab_port_hop_interval">端口跳跃间隔 (秒)</string>
<string name="server_lab_stream_pinsha256">SHA256 证书指纹</string>
<string name="server_lab_bandwidth_down">带宽下行 (支持的单位 k/m/g/t)</string> <string name="server_lab_bandwidth_down">带宽下行 (支持的单位 k/m/g/t)</string>
<string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string> <string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP 模式</string> <string name="server_lab_xhttp_mode">XHTTP 模式</string>
@@ -108,7 +108,6 @@
<string name="server_obfs_password">混淆密碼</string> <string name="server_obfs_password">混淆密碼</string>
<string name="server_lab_port_hop">跳躍連接埠 (會覆蓋伺服器連接埠)</string> <string name="server_lab_port_hop">跳躍連接埠 (會覆蓋伺服器連接埠)</string>
<string name="server_lab_port_hop_interval">連接埠跳躍間隔 (秒)</string> <string name="server_lab_port_hop_interval">連接埠跳躍間隔 (秒)</string>
<string name="server_lab_stream_pinsha256">SHA256 憑證指紋</string>
<string name="server_lab_bandwidth_down">頻寬下行 (支持的單位 k/m/g/t)</string> <string name="server_lab_bandwidth_down">頻寬下行 (支持的單位 k/m/g/t)</string>
<string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string> <string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP 模式</string> <string name="server_lab_xhttp_mode">XHTTP 模式</string>
+1 -1
View File
@@ -109,7 +109,6 @@
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping(will override the port)</string> <string name="server_lab_port_hop">Port Hopping(will override the port)</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string> <string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_stream_pinsha256">pinSHA256</string>
<string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string>
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> <string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
@@ -244,6 +243,7 @@
<string name="summary_pref_tg_group">Join Telegram Group</string> <string name="summary_pref_tg_group">Join Telegram Group</string>
<string name="toast_tg_app_not_found">Telegram app not found</string> <string name="toast_tg_app_not_found">Telegram app not found</string>
<string name="title_privacy_policy">Privacy policy</string> <string name="title_privacy_policy">Privacy policy</string>
<string name="title_qr_code">QR code</string>
<string name="title_about">About</string> <string name="title_about">About</string>
<string name="title_source_code">Source code</string> <string name="title_source_code">Source code</string>
<string name="title_oss_license">Open Source licenses</string> <string name="title_oss_license">Open Source licenses</string>
+3 -3
View File
@@ -2,14 +2,14 @@
agp = "9.0.0" agp = "9.0.0"
desugarJdkLibs = "2.1.5" desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8" gradleLicensePlugin = "0.9.8"
kotlin = "2.3.0" kotlin = "2.3.10"
coreKtx = "1.17.0" coreKtx = "1.17.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
appcompat = "1.7.1" appcompat = "1.7.1"
material = "1.13.0" material = "1.13.0"
activity = "1.12.2" activity = "1.12.3"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
mmkvStatic = "1.3.16" mmkvStatic = "1.3.16"
gson = "2.13.2" gson = "2.13.2"
@@ -21,7 +21,7 @@ swiperefreshlayout = "1.2.0"
toasty = "1.5.2" toasty = "1.5.2"
editorkit = "2.9.0" editorkit = "2.9.0"
core = "3.5.4" core = "3.5.4"
workRuntimeKtx = "2.11.0" workRuntimeKtx = "2.11.1"
lifecycleViewmodelKtx = "2.10.0" lifecycleViewmodelKtx = "2.10.0"
multidex = "2.0.1" multidex = "2.0.1"
mockitoMockitoInline = "5.2.0" mockitoMockitoInline = "5.2.0"